3036: Updated migration to set modified_at instead of updated_at, added modified_by.
[arvados.git] / services / api / app / controllers / arvados / v1 / collections_controller.rb
1 class Arvados::V1::CollectionsController < ApplicationController
2   def create
3     if !resource_attrs[:manifest_text]
4       return send_error("'manifest_text' attribute must be specified",
5                         status: :unprocessable_entity)
6     end
7
8     # Check permissions on the collection manifest.
9     # If any signature cannot be verified, return 403 Permission denied.
10     api_token = current_api_client_authorization.andand.api_token
11     signing_opts = {
12       key: Rails.configuration.blob_signing_key,
13       api_token: api_token,
14       ttl: Rails.configuration.blob_signing_ttl,
15     }
16     resource_attrs[:manifest_text].lines.each do |entry|
17       entry.split[1..-1].each do |tok|
18         if /^[[:digit:]]+:[[:digit:]]+:/.match tok
19           # This is a filename token, not a blob locator. Note that we
20           # keep checking tokens after this, even though manifest
21           # format dictates that all subsequent tokens will also be
22           # filenames. Safety first!
23         elsif Blob.verify_signature tok, signing_opts
24           # OK.
25         elsif Locator.parse(tok).andand.signature
26           # Signature provided, but verify_signature did not like it.
27           logger.warn "Invalid signature on locator #{tok}"
28           raise ArvadosModel::PermissionDeniedError
29         elsif Rails.configuration.permit_create_collection_with_unsigned_manifest
30           # No signature provided, but we are running in insecure mode.
31           logger.debug "Missing signature on locator #{tok} ignored"
32         elsif Blob.new(tok).empty?
33           # No signature provided -- but no data to protect, either.
34         else
35           logger.warn "Missing signature on locator #{tok}"
36           raise ArvadosModel::PermissionDeniedError
37         end
38       end
39     end
40
41     # Remove any permission signatures from the manifest.
42     resource_attrs[:manifest_text]
43       .gsub!(/ [[:xdigit:]]{32}(\+[[:digit:]]+)?(\+\S+)/) { |word|
44       word.strip!
45       loc = Locator.parse(word)
46       if loc
47         " " + loc.without_signature.to_s
48       else
49         " " + word
50       end
51     }
52
53     super
54   end
55
56   def find_object_by_uuid
57     if loc = Locator.parse(params[:id])
58       loc.strip_hints!
59       if c = Collection.readable_by(*@read_users).where({ portable_data_hash: loc.to_s }).limit(1).first
60         @object = {
61           portable_data_hash: c.portable_data_hash,
62           manifest_text: c.manifest_text,
63           files: c.files,
64           data_size: c.data_size
65         }
66       end
67     else
68       super
69     end
70     true
71   end
72
73   def show
74     if current_api_client_authorization
75       signing_opts = {
76         key: Rails.configuration.blob_signing_key,
77         api_token: current_api_client_authorization.api_token,
78         ttl: Rails.configuration.blob_signing_ttl,
79       }
80       @object[:manifest_text]
81         .gsub!(/ [[:xdigit:]]{32}(\+[[:digit:]]+)?(\+\S+)/) { |word|
82         word.strip!
83         loc = Locator.parse(word)
84         if loc
85           " " + Blob.sign_locator(word, signing_opts)
86         else
87           " " + word
88         end
89       }
90     end
91     if @object.is_a? Collection
92       render json: @object.as_api_response(:with_data)
93     else
94       render json: @object
95     end
96   end
97
98   def script_param_edges(visited, sp)
99     case sp
100     when Hash
101       sp.each do |k, v|
102         script_param_edges(visited, v)
103       end
104     when Array
105       sp.each do |v|
106         script_param_edges(visited, v)
107       end
108     when String
109       return if sp.empty?
110       if loc = Locator.parse(sp)
111         search_edges(visited, loc.to_s, :search_up)
112       end
113     end
114   end
115
116   def search_edges(visited, uuid, direction)
117     if uuid.nil? or uuid.empty? or visited[uuid]
118       return
119     end
120
121     if loc = Locator.parse(uuid)
122       loc.strip_hints!
123       return if visited[loc.to_s]
124     end
125
126     logger.debug "visiting #{uuid}"
127
128     if loc
129       # uuid is a portable_data_hash
130       if c = Collection.readable_by(*@read_users).where(portable_data_hash: loc.to_s).limit(1).first
131         visited[loc.to_s] = {
132           portable_data_hash: c.portable_data_hash,
133           files: c.files,
134           data_size: c.data_size
135         }
136       end
137
138       if direction == :search_up
139         # Search upstream for jobs where this locator is the output of some job
140         Job.readable_by(*@read_users).where(output: loc.to_s).each do |job|
141           search_edges(visited, job.uuid, :search_up)
142         end
143
144         Job.readable_by(*@read_users).where(log: loc.to_s).each do |job|
145           search_edges(visited, job.uuid, :search_up)
146         end
147       elsif direction == :search_down
148         if loc.to_s == "d41d8cd98f00b204e9800998ecf8427e+0"
149           # Special case, don't follow the empty collection.
150           return
151         end
152
153         # Search downstream for jobs where this locator is in script_parameters
154         Job.readable_by(*@read_users).where(["jobs.script_parameters like ?", "%#{loc.to_s}%"]).each do |job|
155           search_edges(visited, job.uuid, :search_down)
156         end
157       end
158     else
159       # uuid is a regular Arvados UUID
160       rsc = ArvadosModel::resource_class_for_uuid uuid
161       if rsc == Job
162         Job.readable_by(*@read_users).where(uuid: uuid).each do |job|
163           visited[uuid] = job.as_api_response
164           if direction == :search_up
165             # Follow upstream collections referenced in the script parameters
166             script_param_edges(visited, job.script_parameters)
167           elsif direction == :search_down
168             # Follow downstream job output
169             search_edges(visited, job.output, direction)
170           end
171         end
172       elsif rsc == Collection
173         if c = Collection.readable_by(*@read_users).where(uuid: uuid).limit(1).first
174           search_edges(visited, c.portable_data_hash, direction)
175           visited[c.portable_data_hash] = c.as_api_response
176         end
177       elsif rsc != nil
178         rsc.where(uuid: uuid).each do |r|
179           visited[uuid] = r.as_api_response
180         end
181       end
182     end
183
184     if direction == :search_up
185       # Search for provenance links pointing to the current uuid
186       Link.readable_by(*@read_users).
187         where(head_uuid: uuid, link_class: "provenance").
188         each do |link|
189         visited[link.uuid] = link.as_api_response
190         search_edges(visited, link.tail_uuid, direction)
191       end
192     elsif direction == :search_down
193       # Search for provenance links emanating from the current uuid
194       Link.readable_by(current_user).
195         where(tail_uuid: uuid, link_class: "provenance").
196         each do |link|
197         visited[link.uuid] = link.as_api_response
198         search_edges(visited, link.head_uuid, direction)
199       end
200     end
201   end
202
203   def provenance
204     visited = {}
205     search_edges(visited, @object[:uuid] || @object[:portable_data_hash], :search_up)
206     render json: visited
207   end
208
209   def used_by
210     visited = {}
211     search_edges(visited, @object[:uuid] || @object[:portable_data_hash], :search_down)
212     render json: visited
213   end
214
215 end