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