[api] don't crash if request does not exist
[opensuse:build-service.git] / src / api / app / controllers / request_controller.rb
1 class RequestController < ApplicationController
2   #TODO: request schema validation
3
4   # GET /request
5   alias_method :index, :pass_to_backend
6
7   # POST /request?cmd=create
8   alias_method :create, :dispatch_command
9
10   # GET /request/:id
11   def show
12     # parse and rewrite the request to latest format
13     data = Suse::Backend.get("/request/#{params[:id]}").body
14     req = BsRequest.new(data)
15     send_data(req.dump_xml, :type => "text/xml")
16   end
17
18   # POST /request/:id? :cmd :newstate
19   alias_method :modify, :dispatch_command
20
21   # PUT /request/:id
22   def update
23     params[:user] = @http_user.login if @http_user
24     
25     #TODO: allow PUT for non-admins
26     unless @http_user.is_admin?
27       render_error :status => 403, :errorcode => 'put_request_no_permission',
28         :message => "PUT on requests currently requires admin privileges"
29       return
30     end
31
32     path = request.path
33     path << build_query_from_hash(params, [:user])
34     pass_to_backend path
35   end
36
37   # DELETE /request/:id
38   #def destroy
39   # Do we want to allow to delete requests at all ?
40   #end
41
42   private
43
44   #
45   # find default reviewers of a project/package via role
46   # 
47   def find_reviewers(obj)
48     # obj can be a project or package object
49     reviewers = Array.new(0)
50     prj = nil
51
52     # check for reviewers in a package first
53     if obj.class == DbProject
54       prj = obj
55     elsif obj.class == DbPackage
56       if defined? obj.package_user_role_relationships
57         obj.package_user_role_relationships.find(:all, :conditions => ["role_id = ?", Role.find_by_title("reviewer").id] ).each do |r|
58           reviewers << User.find_by_id(r.bs_user_id)
59         end
60       end
61       prj = obj.db_project
62     else
63     end
64
65     # add reviewers of project in any case
66     if defined? prj.project_user_role_relationships
67       prj.project_user_role_relationships.find(:all, :conditions => ["role_id = ?", Role.find_by_title("reviewer").id] ).each do |r|
68         reviewers << User.find_by_id(r.bs_user_id)
69       end
70     end
71     return reviewers
72   end
73
74   def find_review_groups(obj)
75     # obj can be a project or package object
76     review_groups = Array.new(0)
77     prj = nil
78
79     # check for reviewers in a package first
80     if obj.class == DbProject
81       prj = obj
82     elsif obj.class == DbPackage
83       if defined? obj.package_group_role_relationships
84         obj.package_group_role_relationships.find(:all, :conditions => ["role_id = ?", Role.find_by_title("reviewer").id] ).each do |r|
85           review_groups << User.find_by_id(r.bs_user_id)
86         end
87       end
88       prj = obj.db_project
89     else
90     end
91
92     # add reviewers of project in any case
93     if defined? prj.project_group_role_relationships
94       prj.project_group_role_relationships.find(:all, :conditions => ["role_id = ?", Role.find_by_title("reviewer").id] ).each do |r|
95         review_groups << User.find_by_id(r.bs_user_id)
96       end
97     end
98     return review_groups
99   end
100
101   # POST /request?cmd=create
102   def create_create
103     req = BsRequest.new(request.body.read)
104
105     req.each_action do |action|
106       # find objects if specified or report error
107       role=nil
108       sprj=nil
109       spkg=nil
110       tprj=nil
111       tpkg=nil
112       if action.has_element? 'person'
113         unless User.find_by_login(action.person.data.attributes["name"])
114           render_error :status => 404, :errorcode => 'unknown_person',
115             :message => "Unknown person  #{action.person.data.attributes["name"]}"
116           return
117         end
118         role = action.person.data.attributes["role"] if action.person.has_attribute? 'role'
119       end
120       if action.has_element? 'group'
121         unless Group.find_by_title(action.group.data.attributes["name"])
122           render_error :status => 404, :errorcode => 'unknown_group',
123             :message => "Unknown group  #{action.group.data.attributes["name"]}"
124           return
125         end
126         role = action.group.data.attributes["role"] if action.group.has_attribute? 'role'
127       end
128       if role
129         unless Role.find_by_title(role)
130           render_error :status => 404, :errorcode => 'unknown_role',
131             :message => "Unknown role  #{role}"
132           return
133         end
134       end
135       if action.has_element? 'source'
136         if action.source.has_attribute? 'project'
137           sprj = DbProject.find_by_name action.source.project
138           unless sprj
139             render_error :status => 404, :errorcode => 'unknown_project',
140               :message => "Unknown source project #{action.source.project}"
141             return
142           end
143         end
144         if action.source.has_attribute? 'package'
145           spkg = sprj.db_packages.find_by_name action.source.package
146           unless spkg
147             render_error :status => 404, :errorcode => 'unknown_package',
148               :message => "Unknown source package #{action.source.package} in project #{action.source.project}"
149             return
150           end
151         end
152       end
153       if action.has_element? 'target'
154         if action.target.has_attribute? 'project'
155           tprj = DbProject.find_by_name action.target.project
156           unless tprj
157             render_error :status => 404, :errorcode => 'unknown_project',
158               :message => "Unknown target project #{action.target.project}"
159             return
160           end
161         end
162         if action.target.has_attribute? 'package' and action.data.attributes["type"] != "submit"
163           tpkg = tprj.db_packages.find_by_name action.target.package
164           unless tpkg
165             render_error :status => 404, :errorcode => 'unknown_package',
166               :message => "Unknown target package #{action.target.package} in project #{action.target.project}"
167             return
168           end
169         end
170       end
171
172       # Type specific checks
173       if action.data.attributes["type"] == "delete" or action.data.attributes["type"] == "add_role" or action.data.attributes["type"] == "set_bugowner"
174         #check existence of target
175         unless tprj
176           if DbProject.find_remote_project(action.target.project)
177             render_error :status => 404, :errorcode => 'unknown_package',
178               :message => "Project is on remote instance, #{action.data.attributes["type"]} not possible  #{action.target.project}"
179             return
180           end
181           render_error :status => 404, :errorcode => 'unknown_project',
182             :message => "No target project specified"
183           return
184         end
185         if action.data.attributes["type"] == "add_role"
186           unless role
187             render_error :status => 404, :errorcode => 'unknown_role',
188               :message => "No role specified"
189             return
190           end
191         end
192       elsif action.data.attributes["type"] == "submit" or action.data.attributes["type"] == "change_devel"
193         #check existence of source
194         unless sprj
195           # no support for remote projects yet, it needs special support during accept as well
196           render_error :status => 404, :errorcode => 'unknown_project',
197             :message => "No source project specified"
198           return
199         end
200
201         if action.data.attributes["type"] == "submit"
202           # source package is required for submit, but optional for change_devel
203           unless spkg
204             render_error :status => 404, :errorcode => 'unknown_package',
205               :message => "No source package specified"
206             return
207           end
208         end
209
210         # source update checks
211         if action.data.attributes["type"] == "submit"
212           sourceupdate = nil
213           if action.has_element? 'options' and action.options.has_element? 'sourceupdate'
214              sourceupdate = action.options.sourceupdate.text
215           end
216           # cleanup implicit home branches, should be done in client with 2.0
217           if not sourceupdate and action.has_element? :target
218              if "home:#{@http_user.login}:branches:#{action.target.project}" == action.source.project
219                if not action.has_element? 'options'
220                  action.add_element 'options'
221                end
222                sourceupdate = 'cleanup'
223                e = action.options.add_element 'sourceupdate'
224                e.text = sourceupdate
225              end
226           end
227           # allow cleanup only, if no devel package reference
228           if sourceupdate == 'cleanup'
229             unless spkg.develpackages.empty?
230               msg = "Unable to delete package #{spkg.name}; following packages use this package as devel package: "
231               msg += spkg.develpackages.map {|dp| dp.db_project.name+"/"+dp.name}.join(", ")
232               render_error :status => 400, :errorcode => 'develpackage_dependency',
233                 :message => msg
234               return
235             end
236           end
237         end
238
239         if action.data.attributes["type"] == "change_devel"
240           unless tpkg
241             render_error :status => 404, :errorcode => 'unknown_package',
242               :message => "No target package specified"
243             return
244           end
245         end
246
247         # We only allow submit/change_devel requests from projects where people have write access
248         # to avoid that random people can submit versions without talking to the maintainers 
249         if spkg
250           unless @http_user.can_modify_package? spkg
251             render_error :status => 403, :errorcode => "create_request_no_permission",
252               :message => "No permission to create request for package '#{spkg.name}' in project '#{sprj.name}'"
253             return
254           end
255         else
256           unless @http_user.can_modify_project? sprj
257             render_error :status => 403, :errorcode => "create_request_no_permission",
258               :message => "No permission to create request based on project '#{sprj.name}'"
259             return
260           end
261         end
262
263       else
264         render_error :status => 403, :errorcode => "create_unknown_request",
265           :message => "Request type is unknown '#{action.data.attributes["type"]}'"
266         return
267       end
268     end
269
270
271     params[:user] = @http_user.login if @http_user
272     path = request.path
273     path << build_query_from_hash(params, [:cmd, :user, :comment])
274     # forward_path is not working here, because we may modify the request.
275     # can get cleaned up when we moved this to the client
276     response = backend_post( path, req.dump_xml )
277
278     # check targets for defined default reviewers
279     reviewers = []
280     review_groups = []
281
282     req = BsRequest.new(response.to_s)
283     req.each_action do |action|
284       tprj = DbProject.find_by_name action.target.project
285       if action.target.has_attribute? 'package'
286         tpkg = tprj.db_packages.find_by_name action.target.package
287         reviewers += find_reviewers(tpkg)
288         review_groups += find_review_groups(tpkg)
289       else
290         reviewers += find_reviewers(tprj)
291         review_groups += find_review_groups(tprj)
292       end
293     end
294
295     # apply reviewers
296     if reviewers.length > 0
297       reviewers.each do |r|
298         p = {}
299         p[:cmd]     = "addreview"
300         p[:by_user] = r.login
301         path = "/request/" + req.id + build_query_from_hash(p, [:cmd, :by_user])
302         r = backend_post( path, "" )
303       end
304     end
305     if review_groups.length > 0
306       review_groups.each do |r|
307         p = {}
308         p[:cmd]     = "addreview"
309         p[:by_user] = r.login
310         path = "/request/" + req.id + build_query_from_hash(p, [:cmd, :by_group])
311         r = backend_post( path, "" )
312       end
313     end
314
315     send_data( response, :disposition => "inline" )
316     return
317   end
318
319   def modify_addreview
320      modify_changestate# :cmd => "addreview",
321                        # :by_user => params[:by_user], :by_group => params[:by_group]
322   end
323   def modify_changereviewstate
324      modify_changestate # :cmd => "changereviewstate", :newstate => params[:newstate], :comment => params[:comment],
325                         #:by_user => params[:by_user], :by_group => params[:by_group]
326   end
327   def modify_changestate
328     req = BsRequest.find params[:id]
329     if req.nil?
330       render_error :status => 404, :message => "No such request", :errorcode => "no_such_request"
331       return
332     end
333     if not @http_user or not @http_user.login
334       render_error :status => 403, :errorcode => "post_request_no_permission",
335                :message => "Action requires authentifacted user."
336       return
337     end
338     params[:user] = @http_user.login
339
340     # transform request body into query parameter 'comment'
341     # the query parameter is preferred if both are set
342     if params[:comment].blank? and request.body
343       params[:comment] = request.body.read
344     end
345
346     if req.has_element? 'submit' and req.has_attribute? 'type'
347       # old style, convert to new style on the fly
348       node = req.submit
349       node.data.name = 'action'
350       node.data.attributes['type'] = 'submit'
351       req.delete_attribute('type')
352     end
353     path = request.path + build_query_from_hash(params, [:cmd, :user, :newstate, :by_user, :by_group, :superseded_by, :comment])
354
355     # do not allow direct switches from accept to decline or vice versa or double actions
356     if params[:newstate] == "accepted" or params[:newstate] == "declined" or params[:newstate] == "superseded"
357        if req.state.name == "accepted" or req.state.name == "declined" or req.state.name == "superseded"
358           render_error :status => 403, :errorcode => "post_request_no_permission",
359             :message => "set state to #{params[:newstate]} from accepted, superseded or declined is not allowed."
360           return
361        end
362     end
363     # Do not accept to skip the review, except force argument is given
364     if params[:newstate] == "accepted"
365        if req.state.name == "review" and not params[:force]
366           render_error :status => 403, :errorcode => "post_request_no_permission",
367             :message => "Request is in review state."
368           return
369        end
370     end
371
372     # generic permission check
373     permission_granted = false
374     if @http_user.is_admin?
375       permission_granted = true
376     elsif params[:newstate] == "deleted"
377       render_error :status => 403, :errorcode => "post_request_no_permission",
378                :message => "Deletion of a request is only permitted for administrators. Please revoke the request instead."
379       return
380     elsif params[:newstate] == "superseded" and not params[:superseded_by]
381       render_error :status => 403, :errorcode => "post_request_missing_parameter",
382                :message => "Supersed a request requires a 'superseded_by' parameter with the request id."
383       return
384     elsif (params[:cmd] == "addreview" and req.creator == @http_user.login)
385       # allow request creator to add further reviewers
386       permission_granted = true
387 #    elsif (params[:cmd] == "changereviewstate" and params[:by_group] == # FIXME: support groups
388 #      permission_granted = true
389     elsif (params[:cmd] == "changereviewstate" and params[:by_user] == @http_user.login)
390       permission_granted = true
391     elsif (req.state.name == "new" or req.state.name == "review") and (params[:newstate] == "superseded" or params[:newstate] == "revoked") and req.creator == @http_user.login
392       # allow new -> revoked state change to creators of request
393       permission_granted = true
394     else # check this for changestate (of request) and addreview command
395        # permission check for each request inside
396        req.each_action do |action|
397          if action.data.attributes["type"] == "submit" or action.data.attributes["type"] == "change_devel"
398            source_project = DbProject.find_by_name(action.source.project)
399            target_project = DbProject.find_by_name(action.target.project)
400            if target_project.nil?
401              render_error :status => 403, :errorcode => "post_request_no_permission",
402                :message => "Target project is missing for request #{req.id} (type #{action.data.attributes['type']})"
403              return
404            end
405            if action.target.package.nil? and action.data.attributes["type"] == "change_devel"
406              render_error :status => 403, :errorcode => "post_request_no_permission",
407                :message => "Target package is missing in request #{req.id} (type #{action.data.attributes['type']})"
408              return
409            end
410            if params[:newstate] != "declined" and params[:newstate] != "revoked"
411              if source_project.nil?
412                render_error :status => 403, :errorcode => "post_request_no_permission",
413                  :message => "Source project is missing for request #{req.id} (type #{action.data.attributes['type']})"
414                return
415              else
416                source_package = source_project.db_packages.find_by_name(action.source.package)
417              end
418              if source_package.nil? and params[:newstate] != "revoked"
419                render_error :status => 403, :errorcode => "post_request_no_permission",
420                  :message => "Source package is missing for request #{req.id} (type #{action.data.attributes['type']})"
421                return
422              end
423            end
424            if action.target.has_attribute? :package
425              target_package = target_project.db_packages.find_by_name(action.target.package)
426            else
427              target_package = target_project.db_packages.find_by_name(action.source.package)
428            end
429            if ( target_package and @http_user.can_modify_package? target_package ) or
430               ( not target_package and @http_user.can_modify_project? target_project )
431               permission_granted = true
432            elsif source_project and req.state.name == "new" and params[:newstate] == "revoked" 
433               # source project owners should be able to revoke submit requests as well
434               source_package = source_project.db_packages.find_by_name(action.source.package)
435               if ( source_package and @http_user.can_modify_package? source_package ) or
436                  ( not source_package and @http_user.can_modify_project? source_project )
437                 permission_granted = true
438               else
439                 render_error :status => 403, :errorcode => "post_request_no_permission",
440                   :message => "No permission to revoke request #{req.id} (type #{action.data.attributes['type']})"
441                 return
442               end
443            else
444              render_error :status => 403, :errorcode => "post_request_no_permission",
445                :message => "No permission to change state of request #{req.id} to #{params[:newstate]} (type #{action.data.attributes['type']})"
446              return
447            end
448     
449          elsif action.data.attributes["type"] == "delete" or action.data.attributes["type"] == "add_role" or action.data.attributes["type"] == "set_bugowner"
450            # check permissions for delete
451            project = DbProject.find_by_name(action.target.project)
452            package = nil
453            if action.target.has_attribute? :package
454               package = project.db_packages.find_by_name(action.target.package)
455               if @http_user.can_modify_package? package
456                  permission_granted = true
457               end
458            else
459               if @http_user.can_modify_project? project
460                  permission_granted = true
461               end
462            end
463            unless permission_granted == true
464              render_error :status => 403, :errorcode => "post_request_no_permission",
465                :message => "No permission to change state of request #{req.id} (type #{action.data.attributes['type']})"
466              return
467            end
468          else
469            render_error :status => 403, :errorcode => "post_request_no_permission",
470              :message => "Unknown request type #{params[:newstate]} of request #{req.id} (type #{action.data.attributes['type']})"
471            return
472          end
473       end
474     end
475
476     # at this point permissions should be granted, but let's double check
477     unless permission_granted == true
478       render_error :status => 403, :errorcode => "post_request_no_permission",
479         :message => "No permission to change state of request #{req.id} (INTERNAL ERROR, PLEASE REPORT ! )"
480       return
481     end
482
483     unless params[:newstate] == "accepted"
484       pass_to_backend path
485       return
486     end
487
488     # We have permission to change all requests inside, now execute
489     req.each_action do |action|
490       if action.data.attributes["type"] == "set_bugowner"
491           object = DbProject.find_by_name(action.target.project)
492           bugowner = Role.find_by_title("bugowner")
493           if action.target.package
494              object = object.db_packages.find_by_name(action.target.package)
495              PackageUserRoleRelationship.find(:all, :conditions => ["db_package_id = ? AND role_id = ?", object, bugowner]).each do |r|
496                 r.destroy
497              end
498           else
499              ProjectUserRoleRelationship.find(:all, :conditions => ["db_project_id = ? AND role_id = ?", object, bugowner]).each do |r|
500                 r.destroy
501              end
502           end
503           object.add_user( action.person.data.attributes["name"], bugowner )
504           object.store
505           render_ok
506       elsif action.data.attributes["type"] == "add_role"
507           object = DbProject.find_by_name(action.target.project)
508           if action.target.package
509              object = object.db_packages.find_by_name(action.target.package)
510           end
511           if action.has_element? 'person'
512              object.add_user( action.person.data.attributes["name"], action.person.data.attributes["role"] )
513           end
514           if action.has_element? 'group'
515              object.add_group( action.group.data.attributes["name"], action.group.data.attributes["role"] )
516           end
517           object.store
518           render_ok
519       elsif action.data.attributes["type"] == "change_devel"
520           target_project = DbProject.find_by_name(action.target.project)
521           target_package = target_project.db_packages.find_by_name(action.target.package)
522           target_package.develpackage = DbPackage.find_by_project_and_name(action.source.project, action.source.package)
523           begin
524             target_package.resolve_devel_package
525             target_package.store
526           rescue DbPackage::CycleError => e
527             render_error :status => 403, :errorcode => "devel_cycle", :message => e.message
528             return
529           end
530           render_ok
531       elsif action.data.attributes["type"] == "submit"
532           sourceupdate = nil
533           if action.has_element? 'options' and action.options.has_element? 'sourceupdate'
534             sourceupdate = action.options.sourceupdate.text
535           end
536           src = action.source
537           comment = "Copy from #{src.project}/#{src.package} via accept of submit request #{params[:id]}"
538           comment += " revision #{src.rev}" if src.has_attribute? :rev
539           comment += ".\n"
540           comment += "Request was accepted with message:\n#{params[:comment]}\n" if params[:comment]
541           cp_params = {
542             :cmd => "copy",
543             :user => @http_user.login,
544             :oproject => src.project,
545             :opackage => src.package,
546             :requestid => params[:id],
547             :comment => comment
548           }
549           cp_params[:orev] = src.rev if src.has_attribute? :rev
550           cp_params[:dontupdatesource] = 1 if sourceupdate == "noupdate"
551
552           #create package unless it exists already
553           target_project = DbProject.find_by_name(action.target.project)
554           if action.target.has_attribute? :package
555             target_package = target_project.db_packages.find_by_name(action.target.package)
556           else
557             target_package = target_project.db_packages.find_by_name(action.source.package)
558           end
559           unless target_package
560             source_project = DbProject.find_by_name(action.source.project)
561             source_package = source_project.db_packages.find_by_name(action.source.package)
562             target_package = Package.new(source_package.to_axml, :project => action.target.project)
563             target_package.name = action.target.package
564             target_package.remove_all_persons
565             target_package.remove_all_flags
566             target_package.remove_devel_project
567             target_package.save
568           end
569
570           cp_path = "/source/#{action.target.project}/#{action.target.package}"
571           cp_path << build_query_from_hash(cp_params, [:cmd, :user, :oproject, :opackage, :orev, :expand, :comment, :requestid, :dontupdatesource])
572           Suse::Backend.post cp_path, nil
573
574           # cleanup source project
575           if sourceupdate == "cleanup"
576             source_project = DbProject.find_by_name(action.source.project)
577             source_package = source_project.db_packages.find_by_name(action.source.package)
578             # check for devel package defines
579             unless source_package.develpackages.empty?
580               msg = "Unable to delete package #{source_package.name}; following packages use this package as devel package: "
581               msg += source_package.develpackages.map {|dp| dp.db_project.name+"/"+dp.name}.join(", ")
582               render_error :status => 400, :errorcode => 'develpackage_dependency',
583                 :message => msg
584               return
585             end
586             if source_project.db_packages.count == 1
587               #find linking repos
588               lreps = Array.new
589               source_project.repositories.each do |repo|
590                 repo.linking_repositories.each do |lrep|
591                   lreps << lrep
592                 end
593               end
594               if lreps.length > 0
595                 #replace links to this projects with links to the "deleted" project
596                 del_repo = DbProject.find_by_name("deleted").repositories[0]
597                 lreps.each do |link_rep|
598                   link_rep.path_elements.find(:all, :include => ["link"]) do |pe|
599                     next unless Repository.find_by_id(pe.repository_id).db_project_id == source_project.id
600                     pe.link = del_repo
601                     pe.save
602                     #update backend
603                     link_prj = link_rep.db_project
604                     logger.info "updating project '#{link_prj.name}'"
605                     Suse::Backend.put_source "/source/#{link_prj.name}/_meta", link_prj.to_axml
606                   end
607                 end
608               end
609
610               # remove source project, if this is the only package
611               source_project.destroy
612               Suse::Backend.delete "/source/#{action.source.project}"
613             else
614               # just remove package
615               source_package.destroy
616               Suse::Backend.delete "/source/#{action.source.project}/#{action.source.package}"
617             end
618           end
619           render_ok
620       elsif action.data.attributes["type"] == "delete"
621           project = DbProject.find_by_name(action.target.project)
622           unless project
623             msg = "Unable to delete project #{action.target.project}; it does not exist."
624             render_error :status => 400, :errorcode => 'not_existing_target',
625               :message => msg
626             return
627           end
628           if not action.target.has_attribute? :package
629             project.destroy
630             Suse::Backend.delete "/source/#{action.target.project}"
631           else
632             DbPackage.transaction do
633               package = project.db_packages.find_by_name(action.target.package)
634               unless package
635                 msg = "Unable to delete package #{action.target.project}/#{action.target.package}; it does not exist."
636                 render_error :status => 400, :errorcode => 'not_existing_target',
637                   :message => msg
638                 return
639               end
640               package.destroy
641               Suse::Backend.delete "/source/#{action.target.project}/#{action.target.package}"
642             end
643           end
644           render_ok
645       else
646         render_error :status => 403, :errorcode => "post_request_no_permission",
647           :message => "Failed to execute request state change of request #{req.id} (type #{action.data.attributes['type']})"
648         return
649       end
650     end
651     pass_to_backend path
652   end
653 end