1 # Filters added to this controller will be run for all controllers in the application.
2 # Likewise, all the methods added will be available for all controllers.
4 require 'opensuse/permission'
5 require 'opensuse/backend'
6 require 'opensuse/validator'
8 require 'rexml/document'
10 class InvalidHttpMethodError < Exception; end
11 class MissingParameterError < Exception; end
13 class ApplicationController < ActionController::Base
15 # Do never use a layout here since that has impact on every controller
17 # session :disabled => true
19 @user_permissions = nil
25 before_filter :start_test_backend
28 # skip the filter for the user stuff
29 before_filter :extract_user, :except => :register
30 before_filter :setup_backend, :add_api_version, :restrict_admin_pages
31 before_filter :shutup_rails
33 #contains current authentification method, one of (:ichain, :basic)
34 attr_accessor :auth_method
36 hide_action :auth_method
37 hide_action 'auth_method='
40 def start_test_backend
42 logger.debug "Starting test backend..."
43 @@backend = IO.popen("#{RAILS_ROOT}/script/start_test_backend")
44 logger.debug "Test backend started with pid: #{@@backend.pid}"
47 raise RuntimeError.new('Backend died') unless line
48 break if line =~ /DONE NOW/
49 logger.debug line.strip
51 ActiveXML::Config.global_write_through = true
53 logger.debug "kill #{@@backend.pid}"
54 Process.kill "INT", @@backend.pid
58 hide_action :start_test_backend
61 def restrict_admin_pages
62 if params[:controller] =~ /^active_rbac/ or params[:controller] =~ /^admin/
68 logger.debug "Checking for Admin role for user #{@http_user.login}"
69 unless @http_user.has_role? 'Admin'
70 logger.debug "not granted!"
71 render :template => 'permerror'
78 if ICHAIN_MODE == :on || ICHAIN_MODE == :simulate # configured in the the environment file
79 @auth_method = :ichain
80 ichain_user = request.env['HTTP_X_USERNAME']
82 logger.info "iChain user extracted from header: #{ichain_user}"
83 elsif ICHAIN_MODE == :simulate
84 ichain_user = ICHAIN_TEST_USER
85 logger.debug "iChain user extracted from config: #{ichain_user}"
88 # we're using iChain, there is no need to authenticate the user from the credentials
89 # However we have to care for the status of the user that must not be unconfirmed or ichain requested
91 @http_user = User.find :first, :conditions => [ 'login = ? AND state=2', ichain_user ]
92 @http_user.update_user_info_from_ichain_env(request.env) unless @http_user.nil?
94 # If we do not find a User here, we need to create a user and wait for
95 # the confirmation by the user and the BS Admin Team.
97 @http_user = User.find :first, :conditions => ['login = ?', ichain_user ]
99 render_error :message => "iChain user not yet registered", :status => 403,
100 :errorcode => "unregistered_ichain_user",
101 :details => "Please register your user via the web application #{CONFIG['webui_url']} once."
103 if @http_user.state == 5 or @http_user.state == 1
104 render_error :message => "iChain user #{ichain_user} is registered but not yet approved.", :status => 403,
105 :errorcode => "registered_ichain_but_unapproved",
106 :details => "<p>Your account is a registered iChain account, but it is not yet approved for the buildservice.</p>"+
107 "<p>Please stay tuned until you get approval message.</p>"
109 render_error :message => "Your user is either invalid or net yet confirmed (state #{@http_user.state}).",
111 :errorcode => "unconfirmed_user",
112 :details => "Please contact the openSUSE admin team <admin@opensuse.org>"
118 if CONFIG['allow_anonymous']
119 @http_user = User.find_by_login( "_nobody_" )
120 @user_permissions = Suse::Permission.new( @http_user )
123 logger.error "No X-username header from iChain! Are we really using iChain?"
124 render_error( :message => "No iChain user found!", :status => 401 ) and return false
127 #active_rbac is used for authentication
128 @auth_method = :basic
130 if request.env.has_key? 'X-HTTP_AUTHORIZATION'
131 # try to get it where mod_rewrite might have put it
132 authorization = request.env['X-HTTP_AUTHORIZATION'].to_s.split
133 elsif request.env.has_key? 'Authorization'
134 # for Apace/mod_fastcgi with -pass-header Authorization
135 authorization = request.env['Authorization'].to_s.split
136 elsif request.env.has_key? 'HTTP_AUTHORIZATION'
137 # this is the regular location
138 authorization = request.env['HTTP_AUTHORIZATION'].to_s.split
141 logger.debug( "AUTH: #{authorization}" )
143 if authorization and authorization[0] == "Basic"
144 # logger.debug( "AUTH2: #{authorization}" )
145 login, passwd = Base64.decode64(authorization[1]).split(':')[0..1]
147 #set password to the empty string in case no password is transmitted in the auth string
150 if @http_user.nil? and CONFIG['allow_anonymous'] and CONFIG['webui_host'] and [ request.env['REMOTE_HOST'], request.env['REMOTE_ADDR'] ].include?( CONFIG['webui_host'] ) and request.env['HTTP_USER_AGENT'].match(/^obs-webui/)
151 @http_user = User.find_by_login( "_nobody_" )
152 @user_permissions = Suse::Permission.new( @http_user )
155 if @http_user.nil? and login
156 render_error :message => "User not yet registered", :status => 403,
157 :errorcode => "unregistered_user",
158 :details => "Please register your user via the web application #{CONFIG['webui_url']} once."
162 logger.debug "no authentication string was sent"
163 render_error( :message => "Authentication required", :status => 401 ) and return false
166 # disallow empty passwords to prevent LDAP lockouts
167 if !passwd or passwd == ""
168 render_error( :message => "User '#{login}' did not provide a password", :status => 401 ) and return false
171 if defined?( LDAP_MODE ) && LDAP_MODE == :on
174 logger.debug( "Using LDAP to find #{login}" )
175 ldap_info = User.find_with_ldap( login, passwd )
177 logger.debug "LDAP_MODE selected but 'ruby-ldap' module not installed."
178 ldap_info = nil # now fall through as if we'd not found a user
181 if not ldap_info.nil?
182 # We've found an ldap authenticated user - find or create an OBS userDB entry.
183 @http_user = User.find_by_login( login )
185 # Check for ldap updates
186 if @http_user.email != ldap_info[0]
187 @http_user.email = ldap_info[0]
191 logger.debug( "No user found in database, creating" )
192 logger.debug( "Email: #{ldap_info[0]}" )
193 logger.debug( "Name : #{ldap_info[1]}" )
194 # Generate and store a fake pw in the OBS DB that no-one knows
195 chars = ["A".."Z","a".."z","0".."9"].collect { |r| r.to_a }.join
196 fakepw = (1..24).collect { chars[rand(chars.size)] }.pack("C*")
197 newuser = User.create(
200 :password_confirmation => fakepw,
201 :email => ldap_info[0] )
202 unless newuser.errors.empty?
204 logger.debug("Creating User failed with: ")
205 newuser.errors.each_full do |msg|
209 render_error( :message => "Cannot create ldap userid: '#{login}' on OBS<br>#{errstr}",
214 newuser.realname = ldap_info[1]
215 newuser.state = User.states['confirmed']
216 newuser.adminnote = "User created via LDAP"
217 user_role = Role.find_by_title("User")
218 newuser.roles << user_role
220 logger.debug( "saving new user..." )
226 session[:rbac_user_id] = @http_user.id
228 logger.debug( "User not found with LDAP, falling back to database" )
229 @http_user = User.find_with_credentials login, passwd
233 @http_user = User.find_with_credentials login, passwd
238 render_error( :message => "Unknown user '#{login}' or invalid password", :status => 401 ) and return false
240 if @http_user.state == 5 or @http_user.state == 1
241 render_error :message => "User is registered but not yet approved.", :status => 403,
242 :errorcode => "unconfirmed_user",
243 :details => "<p>Your account is a registered account, but it is not yet approved for the OBS by admin.</p>"
247 if @http_user.state == 2
248 logger.debug "USER found: #{@http_user.login}"
249 @user_permissions = Suse::Permission.new( @http_user )
254 render_error :message => "User is registered but not in confirmed state.", :status => 403,
255 :errorcode => "inactive_user",
256 :details => "<p>Your account is a registered account, but it is in a not active state.</p>"
260 hide_action :setup_backend
262 # initialize backend on every request
263 Suse::Backend.source_host = SOURCE_HOST
264 Suse::Backend.source_port = SOURCE_PORT
267 hide_action :add_api_version
269 response.headers["X-Opensuse-APIVersion"] = "#{CONFIG['version']}"
272 hide_action :forward_from_backend
273 def forward_from_backend(path)
275 if CONFIG['x_rewrite_host']
276 logger.debug "[backend] VOLLEY(light): #{path}"
277 headers['X-Rewrite-URI'] = path
278 headers['X-Rewrite-Host'] = CONFIG['x_rewrite_host']
283 logger.debug "[backend] VOLLEY: #{path}"
284 backend_http = Net::HTTP.new(SOURCE_HOST, SOURCE_PORT)
285 backend_http.read_timeout = 1000
287 file = Tempfile.new 'volley'
290 opts = { :url_based_filename => true }
292 backend_http.request_get(path) do |res|
293 opts[:status] = res.code
294 opts[:type] = res['Content-Type']
295 res.read_body do |segment|
299 opts[:length] = file.length
300 # streaming makes it very hard for test cases to verify output
301 opts[:stream] = false if Rails.env.test?
302 send_file(file.path, opts)
306 hide_action :download_request
308 file = Tempfile.new 'volley'
311 while b.read(40960, buffer)
319 def pass_to_backend( path = nil )
321 path = request.path+'?'+request.query_string
326 forward_from_backend( path )
329 file = download_request
330 response = Suse::Backend.post( path, file )
333 file = download_request
334 response = Suse::Backend.put( path, file )
337 response = Suse::Backend.delete( path )
340 send_data( response.body, :type => response.fetch( "content-type" ),
341 :disposition => "inline" )
343 public :pass_to_backend
345 def rescue_action_locally( exception )
346 rescue_action_in_public( exception )
349 def rescue_action_in_public( exception )
350 logger.error "rescue_action: caught #{exception.class}: #{exception.message}"
352 when Suse::Backend::HTTPError
353 xml = REXML::Document.new( exception.message.body )
354 http_status = xml.root.attributes['code']
355 unless xml.root.attributes.include? 'origin'
356 xml.root.add_attribute "origin", "backend"
358 xml_text = String.new
360 render :text => xml_text, :status => http_status
361 when ActiveXML::Transport::NotFoundError
362 render_error :message => exception.message, :status => 404
363 when Suse::ValidationError
364 render_error :message => exception.message, :status => 400, :errorcode => 'validation_failed'
365 when InvalidHttpMethodError
366 render_error :message => exception.message, :errorcode => "invalid_http_method", :status => 400
367 when DbPackage::SaveError
368 render_error :message => "error saving package: #{exception.message}", :errorcode => "package_save_error", :status => 400
369 when DbProject::SaveError
370 render_error :message => "error saving project: #{exception.message}", :errorcode => "project_save_error", :status => 400
371 when ActionController::RoutingError, ActiveRecord::RecordNotFound
372 render_error :message => exception.message, :status => 404, :errorcode => "not_found"
373 when ActionController::UnknownAction
374 render_error :message => exception.message, :status => 403, :errorcode => "unknown_action"
375 when ActionView::MissingTemplate
376 render_error :message => exception.message, :status => 404, :errorcode => "not_found"
377 when MissingParameterError
378 render_error :status => 400, :message => exception.message, :errorcode => "missing_parameter"
379 when DbProject::CycleError
380 render_error :status => 400, :message => exception.message, :errorcode => "project_cycle"
382 if send_exception_mail?
383 ExceptionNotifier.deliver_exception_notification(exception, self, request, {})
385 render_error :message => "uncaught exception: #{exception.message}", :status => 400
389 def send_exception_mail?
390 return false unless ExceptionNotifier.exception_recipients
391 return !local_request? && !Rails.env.development?
395 return @user_permissions
402 def required_parameters(*parameters)
403 parameters.each do |parameter|
404 unless params.include? parameter.to_s
405 raise MissingParameterError, "Required Parameter #{parameter} missing"
410 def valid_http_methods(*methods)
411 list = methods.map {|x| x.to_s.downcase.to_s}
412 unless methods.include? request.method
413 raise InvalidHttpMethodError, "Invalid HTTP Method: #{request.method.to_s.upcase}"
417 def render_error( opt = {} )
419 if opt[:status].to_i == 401
420 response.headers["WWW-Authenticate"] = 'basic realm="API login"'
426 @exception = opt[:exception]
427 @details = opt[:details]
429 @summary = "Internal Server Error"
431 @summary = opt[:message]
433 @summary = @exception.message
437 @errorcode = opt[:errorcode]
439 @errorcode = 'uncaught_exception'
441 @errorcode = 'unknown'
444 # if the exception was raised inside a template (-> @template.first_render != nil),
445 # the instance variables created in here will not be injected into the template
446 # object, so we have to do it manually
447 # This is commented out, since it does not work with Rails 2.3 anymore and is also not needed there
448 # if @template.first_render
449 # logger.debug "injecting error instance variables into template object"
450 # %w{@summary @errorcode @exception}.each do |var|
451 # @template.instance_variable_set var, eval(var) if @template.instance_variable_get(var).nil?
455 # on some occasions the status template doesn't receive the instance variables it needs
456 # unless render_to_string is called before (which is an ugly workaround but I don't have any
457 # idea where to start searching for the real problem)
458 render_to_string :template => 'status'
460 logger.info "errorcode '#{@errorcode}' - #{@summary}"
461 response.headers['X-Opensuse-Errorcode'] = @errorcode
462 render :template => 'status', :status => opt[:status], :layout => false
465 def render_ok(opt={})
466 # keep compatible to old call style
467 opt = {:details => opt} if opt.kind_of? String
471 @details = opt[:details] if opt[:details]
472 @data = opt[:data] if opt[:data]
473 render :template => 'status', :status => 200, :layout => false
477 @backend ||= ActiveXML::Config.transport_for :bsrequest
480 def backend_get( path )
481 # TODO: check why not using SUSE:Backend::get
482 backend.direct_http( URI(path) )
485 def backend_put( path, data )
486 backend.direct_http( URI(path), :method => "PUT", :data => data )
489 def backend_post( path, data )
490 backend.set_additional_header("Content-Length", data.size.to_s())
491 response = backend.direct_http( URI(path), :method => "POST", :data => data )
492 backend.delete_additional_header("Content-Length")
496 # Passes control to subroutines determined by action and a request parameter. By
497 # default the parameter assumed to contain the command is ':cmd'. Looks for a method
498 # named <action>_<command>
502 # If you call dispatch_command from an action 'index' with the query parameter cmd
503 # having the value 'show', it will call the method 'index_show'
505 def dispatch_command(opt={})
509 opt = defaults.merge opt
510 unless params.has_key? opt[:cmd_param]
511 render_error :status => 400, :errorcode => "missing_parameter'",
512 :message => "missing parameter '#{opt[:cmd_param]}'"
516 cmd_handler = "#{params[:action]}_#{params[opt[:cmd_param]]}"
517 logger.debug "dispatch_command: trying to call method '#{cmd_handler}'"
519 if not self.respond_to? cmd_handler, true
520 render_error :status => 400, :errorcode => "unknown_command",
521 :message => "Unknown command '#{params[opt[:cmd_param]]}' for path #{request.path}"
527 public :dispatch_command
528 hide_action :dispatch_command
530 def build_query_from_hash(hash, key_list=nil)
531 key_list ||= hash.keys
532 query = key_list.map do |key|
533 [hash[key]].flatten.map {|x| "#{key}=#{CGI.escape hash[key].to_s}"}.join("&") if hash.has_key?(key)
539 return "?"+query.compact.join('&')
543 def query_parms_missing?(*list)
546 missing << param unless params.has_key? param
549 if missing.length > 0
550 render_error :status => 400, :errorcode => "missing_query_parameters",
551 :message => "missing query parameters: #{missing.join ', '}"
556 def min_votes_for_rating
565 def action_fragment_key( options )
566 # this is for customizing the path/filename of cached files (cached by the
567 # action_cache plugin). here we want to include params in the filename
569 par.delete 'controller'
572 par.sort.each { |pair| pairs << pair.join('=') }
573 url_for( options ).split('://').last + "/"+ pairs.join(',').gsub(' ', '-')