Merge branch '2800-python-global-state' into 2800-pgs
[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     munge_manifest_locators(resource_attrs[:manifest_text]) do |loc|
49       loc.without_signature.to_s
50     end
51
52     # Save the collection with the stripped manifest.
53     act_as_system_user do
54       @object = model_class.new resource_attrs.reject { |k,v| k == :owner_uuid }
55       begin
56         @object.save!
57       rescue ActiveRecord::RecordNotUnique
58         logger.debug resource_attrs.inspect
59         if @object.manifest_text and @object.uuid
60           @existing_object = model_class.
61             where('uuid=? and manifest_text=?',
62                   @object.uuid,
63                   @object.manifest_text).
64             first
65           @object = @existing_object || @object
66         end
67       end
68       if @object
69         link_attrs = {
70           owner_uuid: owner_uuid,
71           link_class: 'permission',
72           name: 'can_read',
73           head_uuid: @object.uuid,
74           tail_uuid: owner_uuid
75         }
76         ActiveRecord::Base.transaction do
77           if Link.where(link_attrs).empty?
78             Link.create! link_attrs
79           end
80         end
81       end
82     end
83     show
84   end
85
86   def show
87     sign_manifests(@object[:manifest_text])
88     super
89   end
90
91   def index
92     sign_manifests(*@objects.map { |c| c[:manifest_text] })
93     super
94   end
95
96   def collection_uuid(uuid)
97     m = /([a-f0-9]{32}(\+[0-9]+)?)(\+.*)?/.match(uuid)
98     if m
99       m[1]
100     else
101       nil
102     end
103   end
104
105   def script_param_edges(visited, sp)
106     case sp
107     when Hash
108       sp.each do |k, v|
109         script_param_edges(visited, v)
110       end
111     when Array
112       sp.each do |v|
113         script_param_edges(visited, v)
114       end
115     when String
116       return if sp.empty?
117       m = collection_uuid(sp)
118       if m
119         generate_provenance_edges(visited, m)
120       end
121     end
122   end
123
124   def generate_provenance_edges(visited, uuid)
125     m = collection_uuid(uuid)
126     uuid = m if m
127
128     if not uuid or uuid.empty? or visited[uuid]
129       return ""
130     end
131
132     logger.debug "visiting #{uuid}"
133
134     if m
135       # uuid is a collection
136       Collection.readable_by(current_user).where(uuid: uuid).each do |c|
137         visited[uuid] = c.as_api_response
138         visited[uuid][:files] = []
139         c.files.each do |f|
140           visited[uuid][:files] << f
141         end
142       end
143
144       Job.readable_by(current_user).where(output: uuid).each do |job|
145         generate_provenance_edges(visited, job.uuid)
146       end
147
148       Job.readable_by(current_user).where(log: uuid).each do |job|
149         generate_provenance_edges(visited, job.uuid)
150       end
151
152     else
153       # uuid is something else
154       rsc = ArvadosModel::resource_class_for_uuid uuid
155       if rsc == Job
156         Job.readable_by(current_user).where(uuid: uuid).each do |job|
157           visited[uuid] = job.as_api_response
158           script_param_edges(visited, job.script_parameters)
159         end
160       elsif rsc != nil
161         rsc.where(uuid: uuid).each do |r|
162           visited[uuid] = r.as_api_response
163         end
164       end
165     end
166
167     Link.readable_by(current_user).
168       where(head_uuid: uuid, link_class: "provenance").
169       each do |link|
170       visited[link.uuid] = link.as_api_response
171       generate_provenance_edges(visited, link.tail_uuid)
172     end
173
174     #puts "finished #{uuid}"
175   end
176
177   def provenance
178     visited = {}
179     generate_provenance_edges(visited, @object[:uuid])
180     render json: visited
181   end
182
183   def generate_used_by_edges(visited, uuid)
184     m = collection_uuid(uuid)
185     uuid = m if m
186
187     if not uuid or uuid.empty? or visited[uuid]
188       return ""
189     end
190
191     logger.debug "visiting #{uuid}"
192
193     if m
194       # uuid is a collection
195       Collection.readable_by(current_user).where(uuid: uuid).each do |c|
196         visited[uuid] = c.as_api_response
197         visited[uuid][:files] = []
198         c.files.each do |f|
199           visited[uuid][:files] << f
200         end
201       end
202
203       if uuid == "d41d8cd98f00b204e9800998ecf8427e+0"
204         # special case for empty collection
205         return
206       end
207
208       Job.readable_by(current_user).where(["jobs.script_parameters like ?", "%#{uuid}%"]).each do |job|
209         generate_used_by_edges(visited, job.uuid)
210       end
211
212     else
213       # uuid is something else
214       rsc = ArvadosModel::resource_class_for_uuid uuid
215       if rsc == Job
216         Job.readable_by(current_user).where(uuid: uuid).each do |job|
217           visited[uuid] = job.as_api_response
218           generate_used_by_edges(visited, job.output)
219         end
220       elsif rsc != nil
221         rsc.where(uuid: uuid).each do |r|
222           visited[uuid] = r.as_api_response
223         end
224       end
225     end
226
227     Link.readable_by(current_user).
228       where(tail_uuid: uuid, link_class: "provenance").
229       each do |link|
230       visited[link.uuid] = link.as_api_response
231       generate_used_by_edges(visited, link.head_uuid)
232     end
233
234     #puts "finished #{uuid}"
235   end
236
237   def used_by
238     visited = {}
239     generate_used_by_edges(visited, @object[:uuid])
240     render json: visited
241   end
242
243   def self.munge_manifest_locators(manifest)
244     # Given a manifest text and a block, yield each locator,
245     # and replace it with whatever the block returns.
246     manifest.andand.gsub!(/ [[:xdigit:]]{32}(\+[[:digit:]]+)?(\+\S+)/) do |word|
247       if loc = Locator.parse(word.strip)
248         " " + yield(loc)
249       else
250         " " + word
251       end
252     end
253   end
254
255   protected
256
257   def find_objects_for_index
258     # Omit manifest_text from index results unless expressly selected.
259     @select ||= model_class.api_accessible_attributes(:user).
260       map { |attr_spec| attr_spec.first.to_s } - ["manifest_text"]
261     super
262   end
263
264   def find_object_by_uuid
265     super
266     if !@object and !params[:uuid].match(/^[0-9a-f]+\+\d+$/)
267       # Normalize the given uuid and search again.
268       hash_part = params[:uuid].match(/^([0-9a-f]*)/)[1]
269       collection = Collection.where('uuid like ?', hash_part + '+%').first
270       if collection
271         # We know the collection exists, and what its real uuid is in
272         # the database. Now, throw out @objects and repeat the usual
273         # lookup procedure. (Returning the collection at this point
274         # would bypass permission checks.)
275         @objects = nil
276         @where = { uuid: collection.uuid }
277         find_objects_for_index
278         @object = @objects.first
279       end
280     end
281   end
282
283   def munge_manifest_locators(manifest, &block)
284     self.class.munge_manifest_locators(manifest, &block)
285   end
286
287   def sign_manifests(*manifests)
288     if current_api_client_authorization
289       signing_opts = {
290         key: Rails.configuration.blob_signing_key,
291         api_token: current_api_client_authorization.api_token,
292         ttl: Rails.configuration.blob_signing_ttl,
293       }
294       manifests.each do |text|
295         munge_manifest_locators(text) do |loc|
296           Blob.sign_locator(loc.to_s, signing_opts)
297         end
298       end
299     end
300   end
301 end