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