2755: Verify permission signatures on create.
[arvados.git] / services / api / app / controllers / arvados / v1 / collections_controller.rb
1 class Arvados::V1::CollectionsController < ApplicationController
2   def create
3     # Collections are owned by system_user. Creating a collection has
4     # two effects: The collection is added if it doesn't already
5     # exist, and a "permission" Link is added (if one doesn't already
6     # exist) giving the current user (or specified owner_uuid)
7     # permission to read it.
8     owner_uuid = resource_attrs.delete(:owner_uuid) || current_user.uuid
9     unless current_user.can? write: owner_uuid
10       logger.warn "User #{current_user.andand.uuid} tried to set collection owner_uuid to #{owner_uuid}"
11       raise ArvadosModel::PermissionDeniedError
12     end
13
14     # Check permissions on the collection manifest.
15     # If any signature cannot be verified, return 403 Permission denied.
16     perms_ok = true
17     api_token = current_api_client_authorization.andand.api_token
18     signing_opts = { key: Rails.configuration.permission_key, api_token: api_token }
19     resource_attrs[:manifest_text].lines.each do |entry|
20       # TODO(twp): fail the request if this match fails.
21       # Add in Phase 4 (see #2755)
22       m = /([[:xdigit:]]{32}(\+[[:digit:]]+)?)(\+A\S*)?/.match(entry)
23       if m and m[3]
24         if !api_token
25           logger.warn "No API token present; cannot verify signature #{m[0]}"
26           perms_ok = false
27         elsif !Blob.verify_signature m[0], signing_opts
28           logger.warn "Invalid signature on locator #{m[0]}"
29           perms_ok = false
30         end
31       end
32     end
33     unless perms_ok
34       raise ArvadosModel::PermissionDeniedError
35     end
36
37     # Remove any permission signatures from the manifest.
38     resource_attrs[:manifest_text]
39       .gsub!(/^(\S+\s+)([[:xdigit:]]{32}(\+[[:digit:]]+)?)(\+A\S*)/, '\1\2')
40
41     # Save the collection with the stripped manifest.
42     act_as_system_user do
43       @object = model_class.new resource_attrs.reject { |k,v| k == :owner_uuid }
44       begin
45         @object.save!
46       rescue ActiveRecord::RecordNotUnique
47         logger.debug resource_attrs.inspect
48         if resource_attrs[:manifest_text] and resource_attrs[:uuid]
49           @existing_object = model_class.
50             where('uuid=? and manifest_text=?',
51                   resource_attrs[:uuid],
52                   resource_attrs[:manifest_text]).
53             first
54           @object = @existing_object || @object
55         end
56       end
57
58       if @object
59         link_attrs = {
60           owner_uuid: owner_uuid,
61           link_class: 'permission',
62           name: 'can_read',
63           head_uuid: @object.uuid,
64           tail_uuid: owner_uuid
65         }
66         ActiveRecord::Base.transaction do
67           if Link.where(link_attrs).empty?
68             Link.create! link_attrs
69           end
70         end
71       end
72     end
73     show
74   end
75
76   def show
77     if current_api_client_authorization
78       signing_opts = {
79         key: Rails.configuration.permission_key,
80         api_token: current_api_client_authorization.api_token,
81       }
82       @object[:manifest_text]
83         .gsub!(/^(\S+\s+)([[:xdigit:]]{32}(\+[[:digit:]]+)?)/) { |m|
84         $1 + Blob.sign_locator($2, signing_opts)
85       }
86     end
87     render json: @object.as_api_response(:with_data)
88   end
89
90   def collection_uuid(uuid)
91     m = /([a-f0-9]{32}(\+[0-9]+)?)(\+.*)?/.match(uuid)
92     if m
93       m[1]
94     else
95       nil
96     end
97   end
98
99   def script_param_edges(visited, sp)
100     case sp
101     when Hash
102       sp.each do |k, v|
103         script_param_edges(visited, v)
104       end
105     when Array
106       sp.each do |v|
107         script_param_edges(visited, v)
108       end
109     when String
110       return if sp.empty?
111       m = collection_uuid(sp)
112       if m
113         generate_provenance_edges(visited, m)
114       end
115     end
116   end
117
118   def generate_provenance_edges(visited, uuid)
119     m = collection_uuid(uuid)
120     uuid = m if m
121
122     if not uuid or uuid.empty? or visited[uuid]
123       return ""
124     end
125
126     logger.debug "visiting #{uuid}"
127
128     if m  
129       # uuid is a collection
130       Collection.readable_by(current_user).where(uuid: uuid).each do |c|
131         visited[uuid] = c.as_api_response
132         visited[uuid][:files] = []
133         c.files.each do |f|
134           visited[uuid][:files] << f
135         end
136       end
137
138       Job.readable_by(current_user).where(output: uuid).each do |job|
139         generate_provenance_edges(visited, job.uuid)
140       end
141
142       Job.readable_by(current_user).where(log: uuid).each do |job|
143         generate_provenance_edges(visited, job.uuid)
144       end
145       
146     else
147       # uuid is something else
148       rsc = ArvadosModel::resource_class_for_uuid uuid
149       if rsc == Job
150         Job.readable_by(current_user).where(uuid: uuid).each do |job|
151           visited[uuid] = job.as_api_response
152           script_param_edges(visited, job.script_parameters)
153         end
154       elsif rsc != nil
155         rsc.where(uuid: uuid).each do |r|
156           visited[uuid] = r.as_api_response
157         end
158       end
159     end
160
161     Link.readable_by(current_user).
162       where(head_uuid: uuid, link_class: "provenance").
163       each do |link|
164       visited[link.uuid] = link.as_api_response
165       generate_provenance_edges(visited, link.tail_uuid)
166     end
167
168     #puts "finished #{uuid}"
169   end
170
171   def provenance
172     visited = {}
173     generate_provenance_edges(visited, @object[:uuid])
174     render json: visited
175   end
176
177   def generate_used_by_edges(visited, uuid)
178     m = collection_uuid(uuid)
179     uuid = m if m
180
181     if not uuid or uuid.empty? or visited[uuid]
182       return ""
183     end
184
185     logger.debug "visiting #{uuid}"
186
187     if m  
188       # uuid is a collection
189       Collection.readable_by(current_user).where(uuid: uuid).each do |c|
190         visited[uuid] = c.as_api_response
191         visited[uuid][:files] = []
192         c.files.each do |f|
193           visited[uuid][:files] << f
194         end
195       end
196
197       if uuid == "d41d8cd98f00b204e9800998ecf8427e+0"
198         # special case for empty collection
199         return
200       end
201
202       Job.readable_by(current_user).where(["jobs.script_parameters like ?", "%#{uuid}%"]).each do |job|
203         generate_used_by_edges(visited, job.uuid)
204       end
205       
206     else
207       # uuid is something else
208       rsc = ArvadosModel::resource_class_for_uuid uuid
209       if rsc == Job
210         Job.readable_by(current_user).where(uuid: uuid).each do |job|
211           visited[uuid] = job.as_api_response
212           generate_used_by_edges(visited, job.output)
213         end
214       elsif rsc != nil
215         rsc.where(uuid: uuid).each do |r|
216           visited[uuid] = r.as_api_response
217         end
218       end
219     end
220
221     Link.readable_by(current_user).
222       where(tail_uuid: uuid, link_class: "provenance").
223       each do |link|
224       visited[link.uuid] = link.as_api_response
225       generate_used_by_edges(visited, link.head_uuid)
226     end
227
228     #puts "finished #{uuid}"
229   end
230
231   def used_by
232     visited = {}
233     generate_used_by_edges(visited, @object[:uuid])
234     render json: visited
235   end
236
237   protected
238   def find_object_by_uuid
239     super
240     if !@object and !params[:uuid].match(/^[0-9a-f]+\+\d+$/)
241       # Normalize the given uuid and search again.
242       hash_part = params[:uuid].match(/^([0-9a-f]*)/)[1]
243       collection = Collection.where('uuid like ?', hash_part + '+%').first
244       if collection
245         # We know the collection exists, and what its real uuid is in
246         # the database. Now, throw out @objects and repeat the usual
247         # lookup procedure. (Returning the collection at this point
248         # would bypass permission checks.)
249         @objects = nil
250         @where = { uuid: collection.uuid }
251         find_objects_for_index
252         @object = @objects.first
253       end
254     end
255   end
256
257 end