Merge branch 'master' into 2681-new-inactive-user-notification
[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 = {
19       key: Rails.configuration.blob_signing_key,
20       api_token: api_token,
21       ttl: Rails.configuration.blob_signing_ttl,
22     }
23     resource_attrs[:manifest_text].lines.each do |entry|
24       entry.split[1..-1].each do |tok|
25         # TODO(twp): in Phase 4, fail the request if the locator
26         # lacks a permission signature. (see #2755)
27         loc = Locator.parse(tok)
28         if loc and loc.signature
29           if !api_token
30             logger.warn "No API token present; cannot verify signature on #{loc}"
31             perms_ok = false
32           elsif !Blob.verify_signature tok, signing_opts
33             logger.warn "Invalid signature on locator #{loc}"
34             perms_ok = false
35           end
36         end
37       end
38     end
39     unless perms_ok
40       raise ArvadosModel::PermissionDeniedError
41     end
42
43     # Remove any permission signatures from the manifest.
44     resource_attrs[:manifest_text]
45       .gsub!(/ [[:xdigit:]]{32}(\+[[:digit:]]+)?(\+\S+)/) { |word|
46       word.strip!
47       loc = Locator.parse(word)
48       if loc
49         " " + loc.without_signature.to_s
50       else
51         " " + word
52       end
53     }
54
55     # Save the collection with the stripped manifest.
56     act_as_system_user do
57       @object = model_class.new resource_attrs.reject { |k,v| k == :owner_uuid }
58       begin
59         @object.save!
60       rescue ActiveRecord::RecordNotUnique
61         logger.debug resource_attrs.inspect
62         if resource_attrs[:manifest_text] and resource_attrs[:uuid]
63           @existing_object = model_class.
64             where('uuid=? and manifest_text=?',
65                   resource_attrs[:uuid],
66                   resource_attrs[:manifest_text]).
67             first
68           @object = @existing_object || @object
69         end
70       end
71       if @object
72         link_attrs = {
73           owner_uuid: owner_uuid,
74           link_class: 'permission',
75           name: 'can_read',
76           head_uuid: @object.uuid,
77           tail_uuid: owner_uuid
78         }
79         ActiveRecord::Base.transaction do
80           if Link.where(link_attrs).empty?
81             Link.create! link_attrs
82           end
83         end
84       end
85     end
86     show
87   end
88
89   def show
90     if current_api_client_authorization
91       signing_opts = {
92         key: Rails.configuration.blob_signing_key,
93         api_token: current_api_client_authorization.api_token,
94         ttl: Rails.configuration.blob_signing_ttl,
95       }
96       @object[:manifest_text]
97         .gsub!(/ [[:xdigit:]]{32}(\+[[:digit:]]+)?(\+\S+)/) { |word|
98         word.strip!
99         loc = Locator.parse(word)
100         if loc
101           " " + Blob.sign_locator(word, signing_opts)
102         else
103           " " + word
104         end
105       }
106     end
107     render json: @object.as_api_response(:with_data)
108   end
109
110   def collection_uuid(uuid)
111     m = /([a-f0-9]{32}(\+[0-9]+)?)(\+.*)?/.match(uuid)
112     if m
113       m[1]
114     else
115       nil
116     end
117   end
118
119   def script_param_edges(visited, sp)
120     case sp
121     when Hash
122       sp.each do |k, v|
123         script_param_edges(visited, v)
124       end
125     when Array
126       sp.each do |v|
127         script_param_edges(visited, v)
128       end
129     when String
130       return if sp.empty?
131       m = collection_uuid(sp)
132       if m
133         generate_provenance_edges(visited, m)
134       end
135     end
136   end
137
138   def generate_provenance_edges(visited, uuid)
139     m = collection_uuid(uuid)
140     uuid = m if m
141
142     if not uuid or uuid.empty? or visited[uuid]
143       return ""
144     end
145
146     logger.debug "visiting #{uuid}"
147
148     if m  
149       # uuid is a collection
150       Collection.readable_by(current_user).where(uuid: uuid).each do |c|
151         visited[uuid] = c.as_api_response
152         visited[uuid][:files] = []
153         c.files.each do |f|
154           visited[uuid][:files] << f
155         end
156       end
157
158       Job.readable_by(current_user).where(output: uuid).each do |job|
159         generate_provenance_edges(visited, job.uuid)
160       end
161
162       Job.readable_by(current_user).where(log: uuid).each do |job|
163         generate_provenance_edges(visited, job.uuid)
164       end
165       
166     else
167       # uuid is something else
168       rsc = ArvadosModel::resource_class_for_uuid uuid
169       if rsc == Job
170         Job.readable_by(current_user).where(uuid: uuid).each do |job|
171           visited[uuid] = job.as_api_response
172           script_param_edges(visited, job.script_parameters)
173         end
174       elsif rsc != nil
175         rsc.where(uuid: uuid).each do |r|
176           visited[uuid] = r.as_api_response
177         end
178       end
179     end
180
181     Link.readable_by(current_user).
182       where(head_uuid: uuid, link_class: "provenance").
183       each do |link|
184       visited[link.uuid] = link.as_api_response
185       generate_provenance_edges(visited, link.tail_uuid)
186     end
187
188     #puts "finished #{uuid}"
189   end
190
191   def provenance
192     visited = {}
193     generate_provenance_edges(visited, @object[:uuid])
194     render json: visited
195   end
196
197   def generate_used_by_edges(visited, uuid)
198     m = collection_uuid(uuid)
199     uuid = m if m
200
201     if not uuid or uuid.empty? or visited[uuid]
202       return ""
203     end
204
205     logger.debug "visiting #{uuid}"
206
207     if m  
208       # uuid is a collection
209       Collection.readable_by(current_user).where(uuid: uuid).each do |c|
210         visited[uuid] = c.as_api_response
211         visited[uuid][:files] = []
212         c.files.each do |f|
213           visited[uuid][:files] << f
214         end
215       end
216
217       if uuid == "d41d8cd98f00b204e9800998ecf8427e+0"
218         # special case for empty collection
219         return
220       end
221
222       Job.readable_by(current_user).where(["jobs.script_parameters like ?", "%#{uuid}%"]).each do |job|
223         generate_used_by_edges(visited, job.uuid)
224       end
225       
226     else
227       # uuid is something else
228       rsc = ArvadosModel::resource_class_for_uuid uuid
229       if rsc == Job
230         Job.readable_by(current_user).where(uuid: uuid).each do |job|
231           visited[uuid] = job.as_api_response
232           generate_used_by_edges(visited, job.output)
233         end
234       elsif rsc != nil
235         rsc.where(uuid: uuid).each do |r|
236           visited[uuid] = r.as_api_response
237         end
238       end
239     end
240
241     Link.readable_by(current_user).
242       where(tail_uuid: uuid, link_class: "provenance").
243       each do |link|
244       visited[link.uuid] = link.as_api_response
245       generate_used_by_edges(visited, link.head_uuid)
246     end
247
248     #puts "finished #{uuid}"
249   end
250
251   def used_by
252     visited = {}
253     generate_used_by_edges(visited, @object[:uuid])
254     render json: visited
255   end
256
257   protected
258   def find_object_by_uuid
259     super
260     if !@object and !params[:uuid].match(/^[0-9a-f]+\+\d+$/)
261       # Normalize the given uuid and search again.
262       hash_part = params[:uuid].match(/^([0-9a-f]*)/)[1]
263       collection = Collection.where('uuid like ?', hash_part + '+%').first
264       if collection
265         # We know the collection exists, and what its real uuid is in
266         # the database. Now, throw out @objects and repeat the usual
267         # lookup procedure. (Returning the collection at this point
268         # would bypass permission checks.)
269         @objects = nil
270         @where = { uuid: collection.uuid }
271         find_objects_for_index
272         @object = @objects.first
273       end
274     end
275   end
276 end