api: fix typo
[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_source
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     end
30
31     path = request.path
32     path << build_query_from_hash(params, [:user])
33     forward_data path, :method => :put, :data => request.body
34   end
35
36   # DELETE /request/:id
37   #def destroy
38   #TODO: implement HTTP DELETE as state change to 'deleted'
39   #end
40
41   private
42
43   # POST /request?cmd=create
44   def create_create
45     req = BsRequest.new(request.body.read)
46
47     req.each_action do |action|
48       if action.data.attributes["type"] == "delete"
49         #check existence of target
50         tprj = DbProject.find_by_name action.target.project
51         if tprj
52           if action.target.has_attribute? 'package'
53             tpkg = tprj.db_packages.find_by_name action.target.package
54             unless tpkg
55               render_error :status => 404, :errorcode => 'unknown_package',
56                 :message => "Unknown package  #{action.target.project} / #{action.target.package}"
57               return
58             end
59           end
60         else
61           unless DbProject.find_remote_project(action.target.project)
62             render_error :status => 404, :errorcode => 'unknown_package',
63               :message => "Project is on remote instance, delete not possible  #{action.target.project}"
64             return
65           end
66           render_error :status => 404, :errorcode => 'unknown_project',
67             :message => "Unknown project #{action.target.project}"
68           return
69         end
70       elsif action.data.attributes["type"] == "submit" or action.data.attributes["type"] == "change_devel"
71         #check existence of source
72         sprj = DbProject.find_by_name action.source.project
73 #        unless sprj or DbProject.find_remote_project(action.source.project)
74         unless sprj
75           render_error :status => 404, :errorcode => 'unknown_project',
76             :message => "Unknown source project #{action.source.project}"
77           return
78         end
79
80         unless action.data.attributes["type"] == "change_devel" and action.source.package.nil?
81           # source package is required for submit, but optional for change_devel
82           spkg = sprj.db_packages.find_by_name action.source.package
83 #          unless spkg or DbProject.find_remote_project(action.source.package)
84           unless spkg
85             render_error :status => 404, :errorcode => 'unknown_package',
86               :message => "Unknown source package #{action.source.package} in project #{action.source.project}"
87             return
88           end
89         end
90
91         # source update checks
92         if action.data.attributes["type"] == "submit"
93           sourceupdate = nil
94           if action.has_element? 'options' and action.options.has_element? 'sourceupdate'
95              sourceupdate = action.options.sourceupdate.text
96           end
97           # cleanup implicit home branches, should be done in client with 2.0
98           if not sourceupdate and action.has_element? :target
99              if "home:#{@http_user.login}:branches:#{action.target.project}" == action.source.project
100                if not action.has_element? 'options'
101                  action.add_element 'options'
102                end
103                sourceupdate = 'cleanup'
104                e = action.options.add_element 'sourceupdate'
105                e.text = sourceupdate
106              end
107           end
108           # allow cleanup only, if no devel package reference
109           if sourceupdate == 'cleanup'
110             unless spkg.develpackages.empty?
111               msg = "Unable to delete package #{spkg.name}; following packages use this package as devel package: "
112               msg += spkg.develpackages.map {|dp| dp.db_project.name+"/"+dp.name}.join(", ")
113               render_error :status => 400, :errorcode => 'develpackage_dependency',
114                 :message => msg
115               return
116             end
117           end
118         end
119
120         if action.data.attributes["type"] != "submit" or action.has_element? 'target'
121           # target is required for change_devel, but optional for submit
122           tprj = DbProject.find_by_name action.target.project
123 #          unless sprj or DbProject.find_remote_project(action.source.project)
124           unless tprj
125             render_error :status => 404, :errorcode => 'unknown_project',
126               :message => "Unknown target project #{action.target.project}"
127             return
128           end
129           if action.data.attributes["type"] == "change_devel"
130             tpkg = tprj.db_packages.find_by_name action.target.package
131             unless tpkg
132               render_error :status => 404, :errorcode => 'unknown_package',
133                 :message => "Unknown target package #{action.target.package}"
134               return
135             end
136           end
137         end
138
139         # We only allow submit/change_devel requests from projects where people have write access
140         # to avoid that random people can submit versions without talking to the maintainers 
141         if spkg
142           unless @http_user.can_modify_package? spkg
143             render_error :status => 403, :errorcode => "create_request_no_permission",
144               :message => "No permission to create request for package '#{spkg.name}' in project '#{sprj.name}'"
145             return
146           end
147         else
148           unless @http_user.can_modify_project? sprj
149             render_error :status => 403, :errorcode => "create_request_no_permission",
150               :message => "No permission to create request based on project '#{sprj.name}'"
151             return
152           end
153         end
154       else
155         render_error :status => 403, :errorcode => "create_unknown_request",
156           :message => "Request type is unknown '#{action.data.attributes["type"]}'"
157         return
158       end
159     end
160
161     params[:user] = @http_user.login if @http_user
162     path = request.path
163     path << build_query_from_hash(params, [:cmd, :user, :comment])
164     # forward_path is not working here, because we may modify the request.
165     # can get cleaned up when we moved this to the client
166     response = backend_post( path, req.dump_xml )
167     send_data( response, :disposition => "inline" )
168     return
169   end
170
171   def modify_addreview
172      modify_changestate# :cmd => "addreview",
173                        # :by_user => params[:by_user], :by_group => params[:by_group]
174   end
175   def modify_changereviewstate
176      modify_changestate # :cmd => "changereviewstate", :newstate => params[:newstate], :comment => params[:comment],
177                         #:by_user => params[:by_user], :by_group => params[:by_group]
178   end
179   def modify_changestate
180     req = BsRequest.find params[:id]
181     unless req
182       render_error :status => 404, :message => "No such request", :errorcode => "no_such_request"
183     end
184     if not @http_user or not @http_user.login
185       render_error :status => 403, :errorcode => "post_request_no_permission",
186                :message => "Action requires authentifacted user."
187       return
188     end
189     params[:user] = @http_user.login
190
191     # transform request body into query parameter 'comment'
192     # the query parameter is preferred if both are set
193     if params[:comment].blank? and request.body
194       params[:comment] = request.body.read
195     end
196
197     if req.has_element? 'submit' and req.has_attribute? 'type'
198       # old style, convert to new style on the fly
199       node = req.submit
200       node.data.name = 'action'
201       node.data.attributes['type'] = 'submit'
202       req.delete_attribute('type')
203     end
204     path = request.path + build_query_from_hash(params, [:cmd, :user, :newstate, :by_user, :by_group, :superseded_by, :comment])
205
206     # generic permission check
207     permission_granted = false
208     if @http_user.is_admin?
209       permission_granted = true
210     elsif params[:newstate] == "deleted"
211       render_error :status => 403, :errorcode => "post_request_no_permission",
212                :message => "Deletion of a request is only permitted for administrators. Please revoke the request instead."
213       return
214     elsif params[:newstate] == "superseded" and not params[:superseded_by]
215       render_error :status => 403, :errorcode => "post_request_missing_parameter",
216                :message => "Supersed a request requires a 'superseded_by' parameter with the request id."
217       return
218     elsif (params[:cmd] == "addreview" and req.creator == @http_user.login)
219       # allow request creator to add further reviewers
220       permission_granted = true
221 #    elsif (params[:cmd] == "changereviewstate" and params[:by_group] == # FIXME: support groups
222 #      permission_granted = true
223     elsif (params[:cmd] == "changereviewstate" and params[:by_user] == @http_user.login)
224       permission_granted = true
225     elsif (req.state.name == "new" or req.state.name == "review") and (params[:newstate] == "superseded" or params[:newstate] == "revoked") and req.creator == @http_user.login
226       # allow new -> revoked state change to creators of request
227       permission_granted = true
228     else # check this for changestate (of request) and addreview command
229        # do not allow direct switches from accept to decline or vice versa or double actions
230        if params[:newstate] == "accepted" or params[:newstate] == "declined" or params[:newstate] == "superseded"
231           if req.state.name == "accepted" or req.state.name == "declined" or req.state.name == "superseded"
232              render_error :status => 403, :errorcode => "post_request_no_permission",
233                :message => "set state to #{params[:newstate]} from accepted, superseded or declined is not allowed."
234              return
235           end
236        end
237        # Do not accept to skip the review, except force argument is given
238        if params[:newstate] == "accepted"
239           if req.state.name == "review" and not params[:force]
240              render_error :status => 403, :errorcode => "post_request_no_permission",
241                :message => "Request is in review state."
242              return
243           end
244        end
245
246        # permission check for each request inside
247        req.each_action do |action|
248          if action.data.attributes["type"] == "submit" or action.data.attributes["type"] == "change_devel"
249            source_project = DbProject.find_by_name(action.source.project)
250            target_project = DbProject.find_by_name(action.target.project)
251            if target_project.nil?
252              render_error :status => 403, :errorcode => "post_request_no_permission",
253                :message => "Target project is missing for request #{req.id} (type #{action.data.attributes['type']})"
254              return
255            end
256            if action.target.package.nil? and action.data.attributes["type"] == "change_devel"
257              render_error :status => 403, :errorcode => "post_request_no_permission",
258                :message => "Target package is missing in request #{req.id} (type #{action.data.attributes['type']})"
259              return
260            end
261            if params[:newstate] != "declined" and params[:newstate] != "revoked"
262              if source_project.nil?
263                render_error :status => 403, :errorcode => "post_request_no_permission",
264                  :message => "Source project is missing for request #{req.id} (type #{action.data.attributes['type']})"
265                return
266              else
267                source_package = source_project.db_packages.find_by_name(action.source.package)
268              end
269              if source_package.nil? and params[:newstate] != "revoked"
270                render_error :status => 403, :errorcode => "post_request_no_permission",
271                  :message => "Source package is missing for request #{req.id} (type #{action.data.attributes['type']})"
272                return
273              end
274            end
275            if action.target.has_attribute? :package
276              target_package = target_project.db_packages.find_by_name(action.target.package)
277            else
278              target_package = target_project.db_packages.find_by_name(action.source.package)
279            end
280            if ( target_package and @http_user.can_modify_package? target_package ) or
281               ( not target_package and @http_user.can_modify_project? target_project )
282               permission_granted = true
283            elsif source_project and req.state.name == "new" and params[:newstate] == "revoked" 
284               # source project owners should be able to revoke submit requests as well
285               source_package = source_project.db_packages.find_by_name(action.source.package)
286               if ( source_package and @http_user.can_modify_package? source_package ) or
287                  ( not source_package and @http_user.can_modify_project? source_project )
288                 permission_granted = true
289               else
290                 render_error :status => 403, :errorcode => "post_request_no_permission",
291                   :message => "No permission to revoke request #{req.id} (type #{action.data.attributes['type']})"
292                 return
293               end
294            else
295              render_error :status => 403, :errorcode => "post_request_no_permission",
296                :message => "No permission to change state of request #{req.id} to #{params[:newstate]} (type #{action.data.attributes['type']})"
297              return
298            end
299     
300          elsif action.data.attributes["type"] == "delete"
301            # check permissions for delete
302            project = DbProject.find_by_name(action.target.project)
303            package = nil
304            if action.target.has_attribute? :package
305               package = project.db_packages.find_by_name(action.target.package)
306            end
307            if @http_user.can_modify_project? project or ( package and @http_user.can_modify_package? package )
308              permission_granted = true
309            else
310              render_error :status => 403, :errorcode => "post_request_no_permission",
311                :message => "No permission to change state of delete request #{req.id} (type #{action.data.attributes['type']})"
312              return
313            end
314          else
315            render_error :status => 403, :errorcode => "post_request_no_permission",
316              :message => "Unknown request type #{params[:newstate]} of request #{req.id} (type #{action.data.attributes['type']})"
317            return
318          end
319       end
320     end
321
322     # at this point permissions should be granted, but let's double check
323     if permission_granted != true
324       render_error :status => 403, :errorcode => "post_request_no_permission",
325         :message => "No permission to change state of request #{req.id} (INTERNAL ERROR, PLEASE REPORT ! )"
326       return
327     end
328
329     # We have permission to change all requests inside, now execute
330     req.each_action do |action|
331       if action.data.attributes["type"] == "change_devel"
332         if params[:newstate] == "accepted"
333           target_project = DbProject.find_by_name(action.target.project)
334           target_package = target_project.db_packages.find_by_name(action.target.package)
335           tpac = Package.new(target_package.to_axml, :project => action.target.project)
336           tpac.set_devel :project => action.source.project, :package => action.source.package
337           tpac.save
338           render_ok
339         end
340       elsif action.data.attributes["type"] == "submit"
341         if params[:newstate] == "accepted"
342           sourceupdate = nil
343           if action.has_element? 'options' and action.options.has_element? 'sourceupdate'
344             sourceupdate = action.options.sourceupdate.text
345           end
346           src = action.source
347           comment = "Copy from #{src.project}/#{src.package} via accept of submit request #{params[:id]}"
348           comment += " revision #{src.rev}" if src.has_attribute? :rev
349           comment += ".\n"
350           comment += "Request was accepted with message:\n#{params[:comment]}\n" if params[:comment]
351           cp_params = {
352             :cmd => "copy",
353             :user => @http_user.login,
354             :oproject => src.project,
355             :opackage => src.package,
356             :requestid => params[:id],
357             :comment => comment
358           }
359           cp_params[:orev] = src.rev if src.has_attribute? :rev
360           cp_params[:dontupdatesource] = 1 if sourceupdate == "noupdate"
361
362           #create package unless it exists already
363           target_project = DbProject.find_by_name(action.target.project)
364           if action.target.has_attribute? :package
365             target_package = target_project.db_packages.find_by_name(action.target.package)
366           else
367             target_package = target_project.db_packages.find_by_name(action.source.package)
368           end
369           unless target_package
370             source_project = DbProject.find_by_name(action.source.project)
371             source_package = source_project.db_packages.find_by_name(action.source.package)
372             target_package = Package.new(source_package.to_axml, :project => action.target.project)
373             target_package.name = action.target.package
374             target_package.remove_all_persons
375             target_package.remove_all_flags
376             target_package.remove_devel_project
377             target_package.save
378           end
379
380           cp_path = "/source/#{action.target.project}/#{action.target.package}"
381           cp_path << build_query_from_hash(cp_params, [:cmd, :user, :oproject, :opackage, :orev, :expand, :comment, :requestid, :dontupdatesource])
382           Suse::Backend.post cp_path, nil
383
384           # cleanup source project
385           if sourceupdate == "cleanup"
386             source_project = DbProject.find_by_name(action.source.project)
387             source_package = source_project.db_packages.find_by_name(action.source.package)
388             # check for devel package defines
389             unless source_package.develpackages.empty?
390               msg = "Unable to delete package #{source_package.name}; following packages use this package as devel package: "
391               msg += source_package.develpackages.map {|dp| dp.db_project.name+"/"+dp.name}.join(", ")
392               render_error :status => 400, :errorcode => 'develpackage_dependency',
393                 :message => msg
394               return
395             end
396             if source_project.db_packages.count == 1
397               #find linking repos
398               lreps = Array.new
399               source_project.repositories.each do |repo|
400                 repo.linking_repositories.each do |lrep|
401                   lreps << lrep
402                 end
403               end
404               if lreps.length > 0
405                 #replace links to this projects with links to the "deleted" project
406                 del_repo = DbProject.find_by_name("deleted").repositories[0]
407                 lreps.each do |link_rep|
408                   pe = link_rep.path_elements.find(:first, :include => ["link"], :conditions => ["db_project_id = ?", pro.id])
409                   pe.link = del_repo
410                   pe.save
411                   #update backend
412                   link_prj = link_rep.db_project
413                   logger.info "updating project '#{link_prj.name}'"
414                   Suse::Backend.put_source "/source/#{link_prj.name}/_meta", link_prj.to_axml
415                 end
416               end
417
418               # remove source project, if this is the only package
419               source_project.destroy
420               Suse::Backend.delete "/source/#{action.source.project}"
421             else
422               # just remove package
423               source_package.destroy
424               Suse::Backend.delete "/source/#{action.source.project}/#{action.source.package}"
425             end
426           end
427         end
428       elsif action.data.attributes["type"] == "delete"
429         if params[:newstate] == "accepted" # and req.state.name != "accepted" and req.state.name != "declined"
430           project = DbProject.find_by_name(action.target.project)
431           unless project
432             msg = "Unable to delete project #{action.target.project}; it does not exist."
433             render_error :status => 400, :errorcode => 'not_existing_target',
434               :message => msg
435             return
436           end
437           if not action.target.has_attribute? :package
438             project.destroy
439             Suse::Backend.delete "/source/#{action.target.project}"
440           else
441             DbPackage.transaction do
442               package = project.db_packages.find_by_name(action.target.package)
443               unless package
444                 msg = "Unable to delete package #{action.target.project}/#{action.target.package}; it does not exist."
445                 render_error :status => 400, :errorcode => 'not_existing_target',
446                   :message => msg
447                 return
448               end
449               package.destroy
450               Suse::Backend.delete "/source/#{action.target.project}/#{action.target.package}"
451             end
452           end
453           render_ok
454         end
455       else
456         render_error :status => 403, :errorcode => "post_request_no_permission",
457           :message => "Failed to execute request state change of request #{req.id} (type #{action.data.attributes['type']})"
458         return
459       end
460     end
461     forward_data path, :method => :post
462   end
463 end