[webui] Removed broken XML check that was cp'ed from master.
[opensuse:build-service.git] / src / api / app / controllers / application_controller.rb
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.
3
4 require 'opensuse/permission'
5 require 'opensuse/backend'
6 require 'opensuse/validator'
7 require 'xpath_engine'
8 require 'rexml/document'
9
10 class InvalidHttpMethodError < Exception; end
11 class MissingParameterError < Exception; end
12
13 class ApplicationController < ActionController::Base
14
15   # Do never use a layout here since that has impact on every controller
16   layout nil
17   # session :disabled => true
18
19   @user_permissions = nil
20   @http_user = nil
21
22   helper RbacHelper
23
24   if Rails.env.test?
25     before_filter :start_test_backend
26   end
27
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
32
33   #contains current authentification method, one of (:ichain, :basic)
34   attr_accessor :auth_method
35   
36   hide_action :auth_method
37   hide_action 'auth_method='
38
39   @@backend = nil
40   def start_test_backend
41     return if @@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}"
45     while true do
46       line = @@backend.gets
47       raise RuntimeError.new('Backend died') unless line
48       break if line =~ /DONE NOW/
49       logger.debug line.strip
50     end
51     ActiveXML::Config.global_write_through = true
52     at_exit do
53       logger.debug "kill #{@@backend.pid}"
54       Process.kill "INT", @@backend.pid
55       @@backend = nil
56     end
57   end
58   hide_action :start_test_backend
59
60   protected
61   def restrict_admin_pages
62      if params[:controller] =~ /^active_rbac/ or params[:controller] =~ /^admin/
63         return require_admin
64      end
65   end
66
67   def require_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'
72       return false
73     end
74     return true
75   end
76
77   def extract_user
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']
81       if ichain_user
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}"
86       end
87
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
90       if ichain_user
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?
93
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.
96         if @http_user == nil
97           @http_user = User.find :first, :conditions => ['login = ?', ichain_user ]
98           if @http_user == nil
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."
102           else
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>"
108             else
109               render_error :message => "Your user is either invalid or net yet confirmed (state #{@http_user.state}).",
110                 :status => 403,
111                 :errorcode => "unconfirmed_user",
112                 :details => "Please contact the openSUSE admin team <admin@opensuse.org>"
113             end
114           end
115           return false
116         end
117       else
118         if CONFIG['allow_anonymous']
119           @http_user = User.find_by_login( "_nobody_" )
120           @user_permissions = Suse::Permission.new( @http_user )
121           return true
122         end
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
125       end
126     else
127       #active_rbac is used for authentication
128       @auth_method = :basic
129
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
139       end
140
141       logger.debug( "AUTH: #{authorization}" )
142
143       if authorization and authorization[0] == "Basic"
144         # logger.debug( "AUTH2: #{authorization}" )
145         login, passwd = Base64.decode64(authorization[1]).split(':')[0..1]
146
147         #set password to the empty string in case no password is transmitted in the auth string
148         passwd ||= ""
149       else
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 )
153           return true
154         else
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."
159             return false
160           end
161         end
162         logger.debug "no authentication string was sent"
163         render_error( :message => "Authentication required", :status => 401 ) and return false
164       end
165
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
169       end
170
171       if defined?( LDAP_MODE ) && LDAP_MODE == :on
172         begin
173           require 'ldap'
174           logger.debug( "Using LDAP to find #{login}" )
175           ldap_info = User.find_with_ldap( login, passwd )
176         rescue Exception
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
179         end
180
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 )
184           if @http_user
185             # Check for ldap updates
186             if @http_user.email != ldap_info[0]
187               @http_user.email = ldap_info[0]
188               @http_user.save
189             end
190           else
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(
198               :login => login,
199               :password => fakepw,
200               :password_confirmation => fakepw,
201               :email => ldap_info[0] )
202             unless newuser.errors.empty?
203               errstr = String.new
204               logger.debug("Creating User failed with: ")
205               newuser.errors.each_full do |msg|
206                 errstr = errstr+msg
207                 logger.debug(msg)
208               end
209               render_error( :message => "Cannot create ldap userid: '#{login}' on OBS<br>#{errstr}",
210                 :status => 401 )
211               @http_user=nil
212               return false
213             end
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
219
220             logger.debug( "saving new user..." )
221             newuser.save
222
223             @http_user = newuser
224           end
225
226           session[:rbac_user_id] = @http_user.id
227         else
228           logger.debug( "User not found with LDAP, falling back to database" )
229           @http_user = User.find_with_credentials login, passwd
230         end
231
232       else
233         @http_user = User.find_with_credentials login, passwd
234       end
235     end
236
237     if @http_user.nil?
238       render_error( :message => "Unknown user '#{login}' or invalid password", :status => 401 ) and return false
239     else
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>"
244         return false
245       end
246
247       if @http_user.state == 2
248         logger.debug "USER found: #{@http_user.login}"
249         @user_permissions = Suse::Permission.new( @http_user )
250         return true
251       end
252     end
253
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>"
257     return false
258   end
259
260   hide_action :setup_backend  
261   def setup_backend
262     # initialize backend on every request
263     Suse::Backend.source_host = SOURCE_HOST
264     Suse::Backend.source_port = SOURCE_PORT
265   end
266
267   hide_action :add_api_version
268   def add_api_version
269     response.headers["X-Opensuse-APIVersion"] = "#{CONFIG['version']}"
270   end
271
272   hide_action :forward_from_backend
273   def forward_from_backend(path)
274
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']
279       head(200)
280       return
281     end
282
283     logger.debug "[backend] VOLLEY: #{path}"
284     backend_http = Net::HTTP.new(SOURCE_HOST, SOURCE_PORT)
285     backend_http.read_timeout = 1000
286
287     file = Tempfile.new 'volley'
288     type = nil
289
290     opts = { :url_based_filename => true }
291     
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|
296         file.write(segment)
297       end
298     end
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)
303     file.close
304   end
305
306   hide_action :download_request
307   def download_request
308     file = Tempfile.new 'volley'
309     b = request.body
310     buffer = String.new
311     while b.read(40960, buffer)
312       file.write(buffer)
313     end
314     file.close
315     file.open
316     file
317   end
318
319   def pass_to_backend( path = nil )
320     unless path
321       path = request.path+'?'+request.query_string
322     end
323
324     case request.method
325     when :get
326       forward_from_backend( path )
327       return
328     when :post
329       file = download_request
330       response = Suse::Backend.post( path, file )
331       file.close!
332     when :put
333       file = download_request
334       response = Suse::Backend.put( path, file )
335       file.close!
336     when :delete
337       response = Suse::Backend.delete( path )
338     end
339
340     send_data( response.body, :type => response.fetch( "content-type" ),
341       :disposition => "inline" )
342   end
343   public :pass_to_backend
344
345   def rescue_action_locally( exception )
346     rescue_action_in_public( exception )
347   end
348
349   def rescue_action_in_public( exception )
350     logger.error "rescue_action: caught #{exception.class}: #{exception.message}"
351     case exception
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"
357       end
358       xml_text = String.new
359       xml.write xml_text
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"
381     else
382       if send_exception_mail?
383         ExceptionNotifier.deliver_exception_notification(exception, self, request, {})
384       end
385       render_error :message => "uncaught exception: #{exception.message}", :status => 400
386     end
387   end
388
389   def send_exception_mail?
390     return false unless ExceptionNotifier.exception_recipients
391     return !local_request? && !Rails.env.development?
392   end
393
394   def permissions
395     return @user_permissions
396   end
397
398   def user
399     return @http_user
400   end
401
402   def required_parameters(*parameters)
403     parameters.each do |parameter|
404       unless params.include? parameter.to_s
405         raise MissingParameterError, "Required Parameter #{parameter} missing"
406       end
407     end
408   end
409
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}"
414     end
415   end
416
417   def render_error( opt = {} )
418     if opt[:status]
419       if opt[:status].to_i == 401
420         response.headers["WWW-Authenticate"] = 'basic realm="API login"'
421       end
422     else
423       opt[:status] = 400
424     end
425
426     @exception = opt[:exception]
427     @details = opt[:details]
428
429     @summary = "Internal Server Error"
430     if opt[:message]
431       @summary = opt[:message]
432     elsif @exception
433       @summary = @exception.message
434     end
435
436     if opt[:errorcode]
437       @errorcode = opt[:errorcode]
438     elsif @exception
439       @errorcode = 'uncaught_exception'
440     else
441       @errorcode = 'unknown'
442     end
443
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?
452     #      end
453     #    end
454
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'
459
460     logger.info "errorcode '#{@errorcode}' - #{@summary}"
461     response.headers['X-Opensuse-Errorcode'] = @errorcode
462     render :template => 'status', :status => opt[:status], :layout => false
463   end
464
465   def render_ok(opt={})
466     # keep compatible to old call style
467     opt = {:details => opt} if opt.kind_of? String
468
469     @errorcode = "ok"
470     @summary = "Ok"
471     @details = opt[:details] if opt[:details]
472     @data = opt[:data] if opt[:data]
473     render :template => 'status', :status => 200, :layout => false
474   end
475
476   def backend
477     @backend ||= ActiveXML::Config.transport_for :bsrequest
478   end
479
480   def backend_get( path )
481     # TODO: check why not using SUSE:Backend::get
482     backend.direct_http( URI(path) )
483   end
484
485   def backend_put( path, data )
486     backend.direct_http( URI(path), :method => "PUT", :data => data )
487   end
488
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")
493     return response
494   end
495
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>
499   #
500   # Example:
501   #
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'
504   #
505   def dispatch_command(opt={})
506     defaults = {
507       :cmd_param => :cmd
508     }
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]}'"
513       return
514     end
515
516     cmd_handler = "#{params[:action]}_#{params[opt[:cmd_param]]}"
517     logger.debug "dispatch_command: trying to call method '#{cmd_handler}'"
518
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}"
522       return
523     end
524
525     __send__ cmd_handler
526   end
527   public :dispatch_command
528   hide_action :dispatch_command
529
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)
534     end
535
536     if query.empty?
537       return ""
538     else
539       return "?"+query.compact.join('&')
540     end
541   end
542
543   def query_parms_missing?(*list)
544     missing = Array.new
545     for param in list
546       missing << param unless params.has_key? param
547     end
548
549     if missing.length > 0
550       render_error :status => 400, :errorcode => "missing_query_parameters",
551         :message => "missing query parameters: #{missing.join ', '}"
552     end
553     return false
554   end
555
556   def min_votes_for_rating
557     MIN_VOTES_FOR_RATING
558   end
559
560   private
561   def shutup_rails
562     Rails.cache.silence!
563   end
564
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
568     par = params
569     par.delete 'controller'
570     par.delete 'action'
571     pairs = []
572     par.sort.each { |pair| pairs << pair.join('=') }
573     url_for( options ).split('://').last + "/"+ pairs.join(',').gsub(' ', '-')
574   end
575
576 end