Merge branch '14152-s3-v4-signature'
[arvados.git] / services / api / test / functional / arvados / v1 / collections_controller_test.rb
1 # Copyright (C) The Arvados Authors. All rights reserved.
2 #
3 # SPDX-License-Identifier: AGPL-3.0
4
5 require 'test_helper'
6
7 class Arvados::V1::CollectionsControllerTest < ActionController::TestCase
8   include DbCurrentTime
9
10   PERM_TOKEN_RE = /\+A[[:xdigit:]]+@[[:xdigit:]]{8}\b/
11
12   def permit_unsigned_manifests isok=true
13     # Set security model for the life of a test.
14     Rails.configuration.permit_create_collection_with_unsigned_manifest = isok
15   end
16
17   def assert_signed_manifest manifest_text, label=''
18     assert_not_nil manifest_text, "#{label} manifest_text was nil"
19     manifest_text.scan(/ [[:xdigit:]]{32}\S*/) do |tok|
20       assert_match(PERM_TOKEN_RE, tok,
21                    "Locator in #{label} manifest_text was not signed")
22     end
23   end
24
25   def assert_unsigned_manifest resp, label=''
26     txt = resp['unsigned_manifest_text']
27     assert_not_nil(txt, "#{label} unsigned_manifest_text was nil")
28     locs = 0
29     txt.scan(/ [[:xdigit:]]{32}\S*/) do |tok|
30       locs += 1
31       refute_match(PERM_TOKEN_RE, tok,
32                    "Locator in #{label} unsigned_manifest_text was signed: #{tok}")
33     end
34     return locs
35   end
36
37   test "should get index" do
38     authorize_with :active
39     get :index
40     assert_response :success
41     assert(assigns(:objects).andand.any?, "no Collections returned in index")
42     refute(json_response["items"].any? { |c| c.has_key?("manifest_text") },
43            "basic Collections index included manifest_text")
44   end
45
46   test "collections.get returns signed locators, and no unsigned_manifest_text" do
47     permit_unsigned_manifests
48     authorize_with :active
49     get :show, {id: collections(:foo_file).uuid}
50     assert_response :success
51     assert_signed_manifest json_response['manifest_text'], 'foo_file'
52     refute_includes json_response, 'unsigned_manifest_text'
53   end
54
55   test "index with manifest_text selected returns signed locators" do
56     columns = %w(uuid owner_uuid manifest_text)
57     authorize_with :active
58     get :index, select: columns
59     assert_response :success
60     assert(assigns(:objects).andand.any?,
61            "no Collections returned for index with columns selected")
62     json_response["items"].each do |coll|
63       assert_equal(coll.keys - ['kind'], columns,
64                    "Collections index did not respect selected columns")
65       assert_signed_manifest coll['manifest_text'], coll['uuid']
66     end
67   end
68
69   test "index with unsigned_manifest_text selected returns only unsigned locators" do
70     authorize_with :active
71     get :index, select: ['unsigned_manifest_text']
72     assert_response :success
73     assert_operator json_response["items"].count, :>, 0
74     locs = 0
75     json_response["items"].each do |coll|
76       assert_equal(coll.keys - ['kind'], ['unsigned_manifest_text'],
77                    "Collections index did not respect selected columns")
78       locs += assert_unsigned_manifest coll, coll['uuid']
79     end
80     assert_operator locs, :>, 0, "no locators found in any manifests"
81   end
82
83   test 'index without select returns everything except manifest' do
84     authorize_with :active
85     get :index
86     assert_response :success
87     assert json_response['items'].any?
88     json_response['items'].each do |coll|
89       assert_includes(coll.keys, 'uuid')
90       assert_includes(coll.keys, 'name')
91       assert_includes(coll.keys, 'created_at')
92       refute_includes(coll.keys, 'manifest_text')
93     end
94   end
95
96   ['', nil, false, 'null'].each do |select|
97     test "index with select=#{select.inspect} returns everything except manifest" do
98       authorize_with :active
99       get :index, select: select
100       assert_response :success
101       assert json_response['items'].any?
102       json_response['items'].each do |coll|
103         assert_includes(coll.keys, 'uuid')
104         assert_includes(coll.keys, 'name')
105         assert_includes(coll.keys, 'created_at')
106         refute_includes(coll.keys, 'manifest_text')
107       end
108     end
109   end
110
111   [["uuid"],
112    ["uuid", "manifest_text"],
113    '["uuid"]',
114    '["uuid", "manifest_text"]'].each do |select|
115     test "index with select=#{select.inspect} returns no name" do
116       authorize_with :active
117       get :index, select: select
118       assert_response :success
119       assert json_response['items'].any?
120       json_response['items'].each do |coll|
121         refute_includes(coll.keys, 'name')
122       end
123     end
124   end
125
126   [0,1,2].each do |limit|
127     test "get index with limit=#{limit}" do
128       authorize_with :active
129       get :index, limit: limit
130       assert_response :success
131       assert_equal limit, assigns(:objects).count
132       resp = JSON.parse(@response.body)
133       assert_equal limit, resp['limit']
134     end
135   end
136
137   test "items.count == items_available" do
138     authorize_with :active
139     get :index, limit: 100000
140     assert_response :success
141     resp = JSON.parse(@response.body)
142     assert_equal resp['items_available'], assigns(:objects).length
143     assert_equal resp['items_available'], resp['items'].count
144     unique_uuids = resp['items'].collect { |i| i['uuid'] }.compact.uniq
145     assert_equal unique_uuids.count, resp['items'].count
146   end
147
148   test "items.count == items_available with filters" do
149     authorize_with :active
150     get :index, {
151       limit: 100,
152       filters: [['uuid','=',collections(:foo_file).uuid]]
153     }
154     assert_response :success
155     assert_equal 1, assigns(:objects).length
156     assert_equal 1, json_response['items_available']
157     assert_equal 1, json_response['items'].count
158   end
159
160   test "get index with limit=2 offset=99999" do
161     # Assume there are not that many test fixtures.
162     authorize_with :active
163     get :index, limit: 2, offset: 99999
164     assert_response :success
165     assert_equal 0, assigns(:objects).count
166     resp = JSON.parse(@response.body)
167     assert_equal 2, resp['limit']
168     assert_equal 99999, resp['offset']
169   end
170
171   def request_capped_index(params={})
172     authorize_with :user1_with_load
173     coll1 = collections(:collection_1_of_201)
174     Rails.configuration.max_index_database_read =
175       yield(coll1.manifest_text.size)
176     get :index, {
177       select: %w(uuid manifest_text),
178       filters: [["owner_uuid", "=", coll1.owner_uuid]],
179       limit: 300,
180     }.merge(params)
181   end
182
183   test "index with manifest_text limited by max_index_database_read returns non-empty" do
184     request_capped_index() { |_| 1 }
185     assert_response :success
186     assert_equal(1, json_response["items"].size)
187     assert_equal(1, json_response["limit"])
188     assert_equal(201, json_response["items_available"])
189   end
190
191   test "max_index_database_read size check follows same order as real query" do
192     authorize_with :user1_with_load
193     txt = '.' + ' d41d8cd98f00b204e9800998ecf8427e+0'*1000 + " 0:0:empty.txt\n"
194     c = Collection.create! manifest_text: txt, name: '0000000000000000000'
195     request_capped_index(select: %w(uuid manifest_text name),
196                          order: ['name asc'],
197                          filters: [['name','>=',c.name]]) do |_|
198       txt.length - 1
199     end
200     assert_response :success
201     assert_equal(1, json_response["items"].size)
202     assert_equal(1, json_response["limit"])
203     assert_equal(c.uuid, json_response["items"][0]["uuid"])
204     # The effectiveness of the test depends on >1 item matching the filters.
205     assert_operator(1, :<, json_response["items_available"])
206   end
207
208   test "index with manifest_text limited by max_index_database_read" do
209     request_capped_index() { |size| (size * 3) + 1 }
210     assert_response :success
211     assert_equal(3, json_response["items"].size)
212     assert_equal(3, json_response["limit"])
213     assert_equal(201, json_response["items_available"])
214   end
215
216   test "max_index_database_read does not interfere with limit" do
217     request_capped_index(limit: 5) { |size| size * 20 }
218     assert_response :success
219     assert_equal(5, json_response["items"].size)
220     assert_equal(5, json_response["limit"])
221     assert_equal(201, json_response["items_available"])
222   end
223
224   test "max_index_database_read does not interfere with order" do
225     request_capped_index(select: %w(uuid manifest_text name),
226                          order: "name DESC") { |size| (size * 11) + 1 }
227     assert_response :success
228     assert_equal(11, json_response["items"].size)
229     assert_empty(json_response["items"].reject do |coll|
230                    coll["name"] =~ /^Collection_9/
231                  end)
232     assert_equal(11, json_response["limit"])
233     assert_equal(201, json_response["items_available"])
234   end
235
236   test "admin can create collection with unsigned manifest" do
237     authorize_with :admin
238     test_collection = {
239       manifest_text: <<-EOS
240 . d41d8cd98f00b204e9800998ecf8427e+0 0:0:foo.txt
241 . acbd18db4cc2f85cedef654fccc4a4d8+3 0:3:bar.txt
242 . acbd18db4cc2f85cedef654fccc4a4d8+3 0:3:bar.txt
243 ./baz acbd18db4cc2f85cedef654fccc4a4d8+3 0:3:bar.txt
244 EOS
245     }
246     test_collection[:portable_data_hash] =
247       Digest::MD5.hexdigest(test_collection[:manifest_text]) +
248       '+' +
249       test_collection[:manifest_text].length.to_s
250
251     # post :create will modify test_collection in place, so we save a copy first.
252     # Hash.deep_dup is not sufficient as it preserves references of strings (??!?)
253     post_collection = Marshal.load(Marshal.dump(test_collection))
254     post :create, {
255       collection: post_collection
256     }
257
258     assert_response :success
259     assert_nil assigns(:objects)
260
261     response_collection = assigns(:object)
262
263     stored_collection = Collection.select([:uuid, :portable_data_hash, :manifest_text]).
264       where(portable_data_hash: response_collection['portable_data_hash']).first
265
266     assert_equal test_collection[:portable_data_hash], stored_collection['portable_data_hash']
267
268     # The manifest in the response will have had permission hints added.
269     # Remove any permission hints in the response before comparing it to the source.
270     stripped_manifest = stored_collection['manifest_text'].gsub(/\+A[A-Za-z0-9@_-]+/, '')
271     assert_equal test_collection[:manifest_text], stripped_manifest
272
273     # TBD: create action should add permission signatures to manifest_text in the response,
274     # and we need to check those permission signatures here.
275   end
276
277   [:admin, :active].each do |user|
278     test "#{user} can get collection using portable data hash" do
279       authorize_with user
280
281       foo_collection = collections(:foo_file)
282
283       # Get foo_file using its portable data hash
284       get :show, {
285         id: foo_collection[:portable_data_hash]
286       }
287       assert_response :success
288       assert_not_nil assigns(:object)
289       resp = assigns(:object)
290       assert_equal foo_collection[:portable_data_hash], resp[:portable_data_hash]
291       assert_signed_manifest resp[:manifest_text]
292
293       # The manifest in the response will have had permission hints added.
294       # Remove any permission hints in the response before comparing it to the source.
295       stripped_manifest = resp[:manifest_text].gsub(/\+A[A-Za-z0-9@_-]+/, '')
296       assert_equal foo_collection[:manifest_text], stripped_manifest
297     end
298   end
299
300   test "create with owner_uuid set to owned group" do
301     permit_unsigned_manifests
302     authorize_with :active
303     manifest_text = ". d41d8cd98f00b204e9800998ecf8427e 0:0:foo.txt\n"
304     post :create, {
305       collection: {
306         owner_uuid: 'zzzzz-j7d0g-rew6elm53kancon',
307         manifest_text: manifest_text,
308         portable_data_hash: "d30fe8ae534397864cb96c544f4cf102+47"
309       }
310     }
311     assert_response :success
312     resp = JSON.parse(@response.body)
313     assert_equal 'zzzzz-j7d0g-rew6elm53kancon', resp['owner_uuid']
314   end
315
316   test "create fails with duplicate name" do
317     permit_unsigned_manifests
318     authorize_with :admin
319     manifest_text = ". d41d8cd98f00b204e9800998ecf8427e 0:0:foo.txt\n"
320     post :create, {
321       collection: {
322         owner_uuid: 'zzzzz-tpzed-000000000000000',
323         manifest_text: manifest_text,
324         portable_data_hash: "d30fe8ae534397864cb96c544f4cf102+47",
325         name: "foo_file"
326       }
327     }
328     assert_response 422
329     response_errors = json_response['errors']
330     assert_not_nil response_errors, 'Expected error in response'
331     assert(response_errors.first.include?('duplicate key'),
332            "Expected 'duplicate key' error in #{response_errors.first}")
333   end
334
335   [false, true].each do |unsigned|
336     test "create with duplicate name, ensure_unique_name, unsigned=#{unsigned}" do
337       permit_unsigned_manifests unsigned
338       authorize_with :active
339       manifest_text = ". acbd18db4cc2f85cedef654fccc4a4d8+3 0:0:foo.txt\n"
340       if !unsigned
341         manifest_text = Collection.sign_manifest manifest_text, api_token(:active)
342       end
343       post :create, {
344         collection: {
345           owner_uuid: users(:active).uuid,
346           manifest_text: manifest_text,
347           name: "owned_by_active"
348         },
349         ensure_unique_name: true
350       }
351       assert_response :success
352       assert_match /^owned_by_active \(\d{4}-\d\d-\d\d.*?Z\)$/, json_response['name']
353     end
354   end
355
356   test "create with owner_uuid set to group i can_manage" do
357     permit_unsigned_manifests
358     authorize_with :active
359     manifest_text = ". d41d8cd98f00b204e9800998ecf8427e 0:0:foo.txt\n"
360     post :create, {
361       collection: {
362         owner_uuid: groups(:active_user_has_can_manage).uuid,
363         manifest_text: manifest_text,
364         portable_data_hash: "d30fe8ae534397864cb96c544f4cf102+47"
365       }
366     }
367     assert_response :success
368     resp = JSON.parse(@response.body)
369     assert_equal groups(:active_user_has_can_manage).uuid, resp['owner_uuid']
370   end
371
372   test "create with owner_uuid fails on group with only can_read permission" do
373     permit_unsigned_manifests
374     authorize_with :active
375     manifest_text = ". d41d8cd98f00b204e9800998ecf8427e 0:0:foo.txt\n"
376     post :create, {
377       collection: {
378         owner_uuid: groups(:all_users).uuid,
379         manifest_text: manifest_text,
380         portable_data_hash: "d30fe8ae534397864cb96c544f4cf102+47"
381       }
382     }
383     assert_response 403
384   end
385
386   test "create with owner_uuid fails on group with no permission" do
387     permit_unsigned_manifests
388     authorize_with :active
389     manifest_text = ". d41d8cd98f00b204e9800998ecf8427e 0:0:foo.txt\n"
390     post :create, {
391       collection: {
392         owner_uuid: groups(:public).uuid,
393         manifest_text: manifest_text,
394         portable_data_hash: "d30fe8ae534397864cb96c544f4cf102+47"
395       }
396     }
397     assert_response 422
398   end
399
400   test "admin create with owner_uuid set to group with no permission" do
401     permit_unsigned_manifests
402     authorize_with :admin
403     manifest_text = ". d41d8cd98f00b204e9800998ecf8427e 0:0:foo.txt\n"
404     post :create, {
405       collection: {
406         owner_uuid: 'zzzzz-j7d0g-it30l961gq3t0oi',
407         manifest_text: manifest_text,
408         portable_data_hash: "d30fe8ae534397864cb96c544f4cf102+47"
409       }
410     }
411     assert_response :success
412   end
413
414   test "should create with collection passed as json" do
415     permit_unsigned_manifests
416     authorize_with :active
417     post :create, {
418       collection: <<-EOS
419       {
420         "manifest_text":". d41d8cd98f00b204e9800998ecf8427e 0:0:foo.txt\n",\
421         "portable_data_hash":"d30fe8ae534397864cb96c544f4cf102+47"\
422       }
423       EOS
424     }
425     assert_response :success
426   end
427
428   test "should fail to create with checksum mismatch" do
429     permit_unsigned_manifests
430     authorize_with :active
431     post :create, {
432       collection: <<-EOS
433       {
434         "manifest_text":". d41d8cd98f00b204e9800998ecf8427e 0:0:bar.txt\n",\
435         "portable_data_hash":"d30fe8ae534397864cb96c544f4cf102+47"\
436       }
437       EOS
438     }
439     assert_response 422
440   end
441
442   test "collection UUID is normalized when created" do
443     permit_unsigned_manifests
444     authorize_with :active
445     post :create, {
446       collection: {
447         manifest_text: ". d41d8cd98f00b204e9800998ecf8427e 0:0:foo.txt\n",
448         portable_data_hash: "d30fe8ae534397864cb96c544f4cf102+47+Khint+Xhint+Zhint"
449       }
450     }
451     assert_response :success
452     assert_not_nil assigns(:object)
453     resp = JSON.parse(@response.body)
454     assert_equal "d30fe8ae534397864cb96c544f4cf102+47", resp['portable_data_hash']
455   end
456
457   test "get full provenance for baz file" do
458     authorize_with :active
459     get :provenance, id: 'ea10d51bcf88862dbcc36eb292017dfd+45'
460     assert_response :success
461     resp = JSON.parse(@response.body)
462     assert_not_nil resp['ea10d51bcf88862dbcc36eb292017dfd+45'] # baz
463     assert_not_nil resp['fa7aeb5140e2848d39b416daeef4ffc5+45'] # bar
464     assert_not_nil resp['1f4b0bc7583c2a7f9102c395f4ffc5e3+45'] # foo
465     assert_not_nil resp['zzzzz-8i9sb-cjs4pklxxjykyuq'] # bar->baz
466     assert_not_nil resp['zzzzz-8i9sb-aceg2bnq7jt7kon'] # foo->bar
467   end
468
469   test "get no provenance for foo file" do
470     # spectator user cannot even see baz collection
471     authorize_with :spectator
472     get :provenance, id: '1f4b0bc7583c2a7f9102c395f4ffc5e3+45'
473     assert_response 404
474   end
475
476   test "get partial provenance for baz file" do
477     # spectator user can see bar->baz job, but not foo->bar job
478     authorize_with :spectator
479     get :provenance, id: 'ea10d51bcf88862dbcc36eb292017dfd+45'
480     assert_response :success
481     resp = JSON.parse(@response.body)
482     assert_not_nil resp['ea10d51bcf88862dbcc36eb292017dfd+45'] # baz
483     assert_not_nil resp['fa7aeb5140e2848d39b416daeef4ffc5+45'] # bar
484     assert_not_nil resp['zzzzz-8i9sb-cjs4pklxxjykyuq']     # bar->baz
485     assert_nil resp['zzzzz-8i9sb-aceg2bnq7jt7kon']         # foo->bar
486     assert_nil resp['1f4b0bc7583c2a7f9102c395f4ffc5e3+45'] # foo
487   end
488
489   test "search collections with 'any' operator" do
490     expect_pdh = collections(:docker_image).portable_data_hash
491     authorize_with :active
492     get :index, {
493       where: { any: ['contains', expect_pdh[5..25]] }
494     }
495     assert_response :success
496     found = assigns(:objects)
497     assert_equal 1, found.count
498     assert_equal expect_pdh, found.first.portable_data_hash
499   end
500
501   [false, true].each do |permit_unsigned|
502     test "create collection with signed manifest, permit_unsigned=#{permit_unsigned}" do
503       permit_unsigned_manifests permit_unsigned
504       authorize_with :active
505       locators = %w(
506       d41d8cd98f00b204e9800998ecf8427e+0
507       acbd18db4cc2f85cedef654fccc4a4d8+3
508       ea10d51bcf88862dbcc36eb292017dfd+45)
509
510       unsigned_manifest = locators.map { |loc|
511         ". " + loc + " 0:0:foo.txt\n"
512       }.join()
513       manifest_uuid = Digest::MD5.hexdigest(unsigned_manifest) +
514         '+' +
515         unsigned_manifest.length.to_s
516
517       # Build a manifest with both signed and unsigned locators.
518       signing_opts = {
519         key: Rails.configuration.blob_signing_key,
520         api_token: api_token(:active),
521       }
522       signed_locators = locators.collect do |x|
523         Blob.sign_locator x, signing_opts
524       end
525       if permit_unsigned
526         # Leave a non-empty blob unsigned.
527         signed_locators[1] = locators[1]
528       else
529         # Leave the empty blob unsigned. This should still be allowed.
530         signed_locators[0] = locators[0]
531       end
532       signed_manifest =
533         ". " + signed_locators[0] + " 0:0:foo.txt\n" +
534         ". " + signed_locators[1] + " 0:0:foo.txt\n" +
535         ". " + signed_locators[2] + " 0:0:foo.txt\n"
536
537       post :create, {
538         collection: {
539           manifest_text: signed_manifest,
540           portable_data_hash: manifest_uuid,
541         }
542       }
543       assert_response :success
544       assert_not_nil assigns(:object)
545       resp = JSON.parse(@response.body)
546       assert_equal manifest_uuid, resp['portable_data_hash']
547       # All of the locators in the output must be signed.
548       resp['manifest_text'].lines.each do |entry|
549         m = /([[:xdigit:]]{32}\+\S+)/.match(entry)
550         if m
551           assert Blob.verify_signature m[0], signing_opts
552         end
553       end
554     end
555   end
556
557   test "create collection with signed manifest and explicit TTL" do
558     authorize_with :active
559     locators = %w(
560       d41d8cd98f00b204e9800998ecf8427e+0
561       acbd18db4cc2f85cedef654fccc4a4d8+3
562       ea10d51bcf88862dbcc36eb292017dfd+45)
563
564     unsigned_manifest = locators.map { |loc|
565       ". " + loc + " 0:0:foo.txt\n"
566     }.join()
567     manifest_uuid = Digest::MD5.hexdigest(unsigned_manifest) +
568       '+' +
569       unsigned_manifest.length.to_s
570
571     # build a manifest with both signed and unsigned locators.
572     # TODO(twp): in phase 4, all locators will need to be signed, so
573     # this test should break and will need to be rewritten. Issue #2755.
574     signing_opts = {
575       key: Rails.configuration.blob_signing_key,
576       api_token: api_token(:active),
577       ttl: 3600   # 1 hour
578     }
579     signed_manifest =
580       ". " + locators[0] + " 0:0:foo.txt\n" +
581       ". " + Blob.sign_locator(locators[1], signing_opts) + " 0:0:foo.txt\n" +
582       ". " + Blob.sign_locator(locators[2], signing_opts) + " 0:0:foo.txt\n"
583
584     post :create, {
585       collection: {
586         manifest_text: signed_manifest,
587         portable_data_hash: manifest_uuid,
588       }
589     }
590     assert_response :success
591     assert_not_nil assigns(:object)
592     resp = JSON.parse(@response.body)
593     assert_equal manifest_uuid, resp['portable_data_hash']
594     # All of the locators in the output must be signed.
595     resp['manifest_text'].lines.each do |entry|
596       m = /([[:xdigit:]]{32}\+\S+)/.match(entry)
597       if m
598         assert Blob.verify_signature m[0], signing_opts
599       end
600     end
601   end
602
603   test "create fails with invalid signature" do
604     authorize_with :active
605     signing_opts = {
606       key: Rails.configuration.blob_signing_key,
607       api_token: api_token(:active),
608     }
609
610     # Generate a locator with a bad signature.
611     unsigned_locator = "acbd18db4cc2f85cedef654fccc4a4d8+3"
612     bad_locator = unsigned_locator + "+Affffffffffffffffffffffffffffffffffffffff@ffffffff"
613     assert !Blob.verify_signature(bad_locator, signing_opts)
614
615     # Creating a collection with this locator should
616     # produce 403 Permission denied.
617     unsigned_manifest = ". #{unsigned_locator} 0:0:foo.txt\n"
618     manifest_uuid = Digest::MD5.hexdigest(unsigned_manifest) +
619       '+' +
620       unsigned_manifest.length.to_s
621
622     bad_manifest = ". #{bad_locator} 0:0:foo.txt\n"
623     post :create, {
624       collection: {
625         manifest_text: bad_manifest,
626         portable_data_hash: manifest_uuid
627       }
628     }
629
630     assert_response 403
631   end
632
633   test "create fails with uuid of signed manifest" do
634     authorize_with :active
635     signing_opts = {
636       key: Rails.configuration.blob_signing_key,
637       api_token: api_token(:active),
638     }
639
640     unsigned_locator = "d41d8cd98f00b204e9800998ecf8427e+0"
641     signed_locator = Blob.sign_locator(unsigned_locator, signing_opts)
642     signed_manifest = ". #{signed_locator} 0:0:foo.txt\n"
643     manifest_uuid = Digest::MD5.hexdigest(signed_manifest) +
644       '+' +
645       signed_manifest.length.to_s
646
647     post :create, {
648       collection: {
649         manifest_text: signed_manifest,
650         portable_data_hash: manifest_uuid
651       }
652     }
653
654     assert_response 422
655   end
656
657   test "reject manifest with unsigned block as stream name" do
658     authorize_with :active
659     post :create, {
660       collection: {
661         manifest_text: "00000000000000000000000000000000+1234 d41d8cd98f00b204e9800998ecf8427e+0 0:0:foo.txt\n"
662       }
663     }
664     assert_includes [422, 403], response.code.to_i
665   end
666
667   test "multiple locators per line" do
668     permit_unsigned_manifests
669     authorize_with :active
670     locators = %w(
671       d41d8cd98f00b204e9800998ecf8427e+0
672       acbd18db4cc2f85cedef654fccc4a4d8+3
673       ea10d51bcf88862dbcc36eb292017dfd+45)
674
675     manifest_text = [".", *locators, "0:0:foo.txt\n"].join(" ")
676     manifest_uuid = Digest::MD5.hexdigest(manifest_text) +
677       '+' +
678       manifest_text.length.to_s
679
680     test_collection = {
681       manifest_text: manifest_text,
682       portable_data_hash: manifest_uuid,
683     }
684     post_collection = Marshal.load(Marshal.dump(test_collection))
685     post :create, {
686       collection: post_collection
687     }
688     assert_response :success
689     assert_not_nil assigns(:object)
690     resp = JSON.parse(@response.body)
691     assert_equal manifest_uuid, resp['portable_data_hash']
692
693     # The manifest in the response will have had permission hints added.
694     # Remove any permission hints in the response before comparing it to the source.
695     stripped_manifest = resp['manifest_text'].gsub(/\+A[A-Za-z0-9@_-]+/, '')
696     assert_equal manifest_text, stripped_manifest
697   end
698
699   test "multiple signed locators per line" do
700     permit_unsigned_manifests
701     authorize_with :active
702     locators = %w(
703       d41d8cd98f00b204e9800998ecf8427e+0
704       acbd18db4cc2f85cedef654fccc4a4d8+3
705       ea10d51bcf88862dbcc36eb292017dfd+45)
706
707     signing_opts = {
708       key: Rails.configuration.blob_signing_key,
709       api_token: api_token(:active),
710     }
711
712     unsigned_manifest = [".", *locators, "0:0:foo.txt\n"].join(" ")
713     manifest_uuid = Digest::MD5.hexdigest(unsigned_manifest) +
714       '+' +
715       unsigned_manifest.length.to_s
716
717     signed_locators = locators.map { |loc| Blob.sign_locator loc, signing_opts }
718     signed_manifest = [".", *signed_locators, "0:0:foo.txt\n"].join(" ")
719
720     post :create, {
721       collection: {
722         manifest_text: signed_manifest,
723         portable_data_hash: manifest_uuid,
724       }
725     }
726     assert_response :success
727     assert_not_nil assigns(:object)
728     resp = JSON.parse(@response.body)
729     assert_equal manifest_uuid, resp['portable_data_hash']
730     # All of the locators in the output must be signed.
731     # Each line is of the form "path locator locator ... 0:0:file.txt"
732     # entry.split[1..-2] will yield just the tokens in the middle of the line
733     returned_locator_count = 0
734     resp['manifest_text'].lines.each do |entry|
735       entry.split[1..-2].each do |tok|
736         returned_locator_count += 1
737         assert Blob.verify_signature tok, signing_opts
738       end
739     end
740     assert_equal locators.count, returned_locator_count
741   end
742
743   test 'Reject manifest with unsigned blob' do
744     permit_unsigned_manifests false
745     authorize_with :active
746     unsigned_manifest = ". 0cc175b9c0f1b6a831c399e269772661+1 0:1:a.txt\n"
747     manifest_uuid = Digest::MD5.hexdigest(unsigned_manifest)
748     post :create, {
749       collection: {
750         manifest_text: unsigned_manifest,
751         portable_data_hash: manifest_uuid,
752       }
753     }
754     assert_response 403,
755     "Creating a collection with unsigned blobs should respond 403"
756     assert_empty Collection.where('uuid like ?', manifest_uuid+'%'),
757     "Collection should not exist in database after failed create"
758   end
759
760   test 'List expired collection returns empty list' do
761     authorize_with :active
762     get :index, {
763       where: {name: 'expired_collection'},
764     }
765     assert_response :success
766     found = assigns(:objects)
767     assert_equal 0, found.count
768   end
769
770   test 'Show expired collection returns 404' do
771     authorize_with :active
772     get :show, {
773       id: 'zzzzz-4zz18-mto52zx1s7sn3ih',
774     }
775     assert_response 404
776   end
777
778   test 'Update expired collection returns 404' do
779     authorize_with :active
780     post :update, {
781       id: 'zzzzz-4zz18-mto52zx1s7sn3ih',
782       collection: {
783         name: "still expired"
784       }
785     }
786     assert_response 404
787   end
788
789   test 'List collection with future expiration time succeeds' do
790     authorize_with :active
791     get :index, {
792       where: {name: 'collection_expires_in_future'},
793     }
794     found = assigns(:objects)
795     assert_equal 1, found.count
796   end
797
798
799   test 'Show collection with future expiration time succeeds' do
800     authorize_with :active
801     get :show, {
802       id: 'zzzzz-4zz18-padkqo7yb8d9i3j',
803     }
804     assert_response :success
805   end
806
807   test 'Update collection with future expiration time succeeds' do
808     authorize_with :active
809     post :update, {
810       id: 'zzzzz-4zz18-padkqo7yb8d9i3j',
811       collection: {
812         name: "still not expired"
813       }
814     }
815     assert_response :success
816   end
817
818   test "get collection and verify that file_names is not included" do
819     authorize_with :active
820     get :show, {id: collections(:foo_file).uuid}
821     assert_response :success
822     assert_equal collections(:foo_file).uuid, json_response['uuid']
823     assert_nil json_response['file_names']
824     assert json_response['manifest_text']
825   end
826
827   [
828     [2**8, :success],
829     [2**18, 422],
830   ].each do |description_size, expected_response|
831     # Descriptions are not part of search indexes. Skip until
832     # full-text search is implemented, at which point replace with a
833     # search in description.
834     skip "create collection with description size #{description_size}
835           and expect response #{expected_response}" do
836       authorize_with :active
837
838       description = 'here is a collection with a very large description'
839       while description.length < description_size
840         description = description + description
841       end
842
843       post :create, collection: {
844         manifest_text: ". d41d8cd98f00b204e9800998ecf8427e+0 0:0:foo.txt\n",
845         description: description,
846       }
847
848       assert_response expected_response
849     end
850   end
851
852   [1, 5, nil].each do |ask|
853     test "Set replication_desired=#{ask.inspect}" do
854       Rails.configuration.default_collection_replication = 2
855       authorize_with :active
856       put :update, {
857         id: collections(:replication_undesired_unconfirmed).uuid,
858         collection: {
859           replication_desired: ask,
860         },
861       }
862       assert_response :success
863       assert_equal ask, json_response['replication_desired']
864     end
865   end
866
867   test "get collection with properties" do
868     authorize_with :active
869     get :show, {id: collections(:collection_with_one_property).uuid}
870     assert_response :success
871     assert_not_nil json_response['uuid']
872     assert_equal 'value1', json_response['properties']['property1']
873   end
874
875   test "create collection with properties" do
876     authorize_with :active
877     manifest_text = ". d41d8cd98f00b204e9800998ecf8427e 0:0:foo.txt\n"
878     post :create, {
879       collection: {
880         manifest_text: manifest_text,
881         portable_data_hash: "d30fe8ae534397864cb96c544f4cf102+47",
882         properties: {'property_1' => 'value_1'}
883       }
884     }
885     assert_response :success
886     assert_not_nil json_response['uuid']
887     assert_equal 'value_1', json_response['properties']['property_1']
888   end
889
890   [
891     ". 0:0:foo.txt",
892     ". d41d8cd98f00b204e9800998ecf8427e foo.txt",
893     "d41d8cd98f00b204e9800998ecf8427e 0:0:foo.txt",
894     ". d41d8cd98f00b204e9800998ecf8427e 0:0:foo.txt",
895   ].each do |manifest_text|
896     test "create collection with invalid manifest #{manifest_text} and expect error" do
897       authorize_with :active
898       post :create, {
899         collection: {
900           manifest_text: manifest_text,
901           portable_data_hash: "d41d8cd98f00b204e9800998ecf8427e+0"
902         }
903       }
904       assert_response 422
905       response_errors = json_response['errors']
906       assert_not_nil response_errors, 'Expected error in response'
907       assert(response_errors.first.include?('Invalid manifest'),
908              "Expected 'Invalid manifest' error in #{response_errors.first}")
909     end
910   end
911
912   [
913     [nil, "d41d8cd98f00b204e9800998ecf8427e+0"],
914     ["", "d41d8cd98f00b204e9800998ecf8427e+0"],
915     [". d41d8cd98f00b204e9800998ecf8427e 0:0:foo.txt\n", "d30fe8ae534397864cb96c544f4cf102+47"],
916   ].each do |manifest_text, pdh|
917     test "create collection with valid manifest #{manifest_text.inspect} and expect success" do
918       authorize_with :active
919       post :create, {
920         collection: {
921           manifest_text: manifest_text,
922           portable_data_hash: pdh
923         }
924       }
925       assert_response 200
926     end
927   end
928
929   [
930     ". 0:0:foo.txt",
931     ". d41d8cd98f00b204e9800998ecf8427e foo.txt",
932     "d41d8cd98f00b204e9800998ecf8427e 0:0:foo.txt",
933     ". d41d8cd98f00b204e9800998ecf8427e 0:0:foo.txt",
934   ].each do |manifest_text|
935     test "update collection with invalid manifest #{manifest_text} and expect error" do
936       authorize_with :active
937       post :update, {
938         id: 'zzzzz-4zz18-bv31uwvy3neko21',
939         collection: {
940           manifest_text: manifest_text,
941         }
942       }
943       assert_response 422
944       response_errors = json_response['errors']
945       assert_not_nil response_errors, 'Expected error in response'
946       assert(response_errors.first.include?('Invalid manifest'),
947              "Expected 'Invalid manifest' error in #{response_errors.first}")
948     end
949   end
950
951   [
952     nil,
953     "",
954     ". d41d8cd98f00b204e9800998ecf8427e 0:0:foo.txt\n",
955   ].each do |manifest_text|
956     test "update collection with valid manifest #{manifest_text.inspect} and expect success" do
957       authorize_with :active
958       post :update, {
959         id: 'zzzzz-4zz18-bv31uwvy3neko21',
960         collection: {
961           manifest_text: manifest_text,
962         }
963       }
964       assert_response 200
965     end
966   end
967
968   test 'get trashed collection with include_trash' do
969     uuid = 'zzzzz-4zz18-mto52zx1s7sn3ih' # expired_collection
970     authorize_with :active
971     get :show, {
972       id: uuid,
973       include_trash: true,
974     }
975     assert_response 200
976   end
977
978   test 'get trashed collection without include_trash' do
979     uuid = 'zzzzz-4zz18-mto52zx1s7sn3ih' # expired_collection
980     authorize_with :active
981     get :show, {
982       id: uuid,
983     }
984     assert_response 404
985   end
986
987   test 'trash collection using http DELETE verb' do
988     uuid = collections(:collection_owned_by_active).uuid
989     authorize_with :active
990     delete :destroy, {
991       id: uuid,
992     }
993     assert_response 200
994     c = Collection.find_by_uuid(uuid)
995     assert_operator c.trash_at, :<, db_current_time
996     assert_equal c.delete_at, c.trash_at + Rails.configuration.blob_signature_ttl
997   end
998
999   test 'delete long-trashed collection immediately using http DELETE verb' do
1000     uuid = 'zzzzz-4zz18-mto52zx1s7sn3ih' # expired_collection
1001     authorize_with :active
1002     delete :destroy, {
1003       id: uuid,
1004     }
1005     assert_response 200
1006     c = Collection.find_by_uuid(uuid)
1007     assert_operator c.trash_at, :<, db_current_time
1008     assert_operator c.delete_at, :<, db_current_time
1009   end
1010
1011   ['zzzzz-4zz18-mto52zx1s7sn3ih', # expired_collection
1012    :empty_collection_name_in_active_user_home_project,
1013   ].each do |fixture|
1014     test "trash collection #{fixture} via trash action with grace period" do
1015       if fixture.is_a? String
1016         uuid = fixture
1017       else
1018         uuid = collections(fixture).uuid
1019       end
1020       authorize_with :active
1021       time_before_trashing = db_current_time
1022       post :trash, {
1023         id: uuid,
1024       }
1025       assert_response 200
1026       c = Collection.find_by_uuid(uuid)
1027       assert_operator c.trash_at, :<, db_current_time
1028       assert_operator c.delete_at, :>=, time_before_trashing + Rails.configuration.default_trash_lifetime
1029     end
1030   end
1031
1032   test 'untrash a trashed collection' do
1033     authorize_with :active
1034     post :untrash, {
1035       id: collections(:expired_collection).uuid,
1036     }
1037     assert_response 200
1038     assert_equal false, json_response['is_trashed']
1039     assert_nil json_response['trash_at']
1040   end
1041
1042   test 'untrash error on not trashed collection' do
1043     authorize_with :active
1044     post :untrash, {
1045       id: collections(:collection_owned_by_active).uuid,
1046     }
1047     assert_response 422
1048   end
1049
1050   [:active, :admin].each do |user|
1051     test "get trashed collections as #{user}" do
1052       authorize_with user
1053       get :index, {
1054         filters: [["is_trashed", "=", true]],
1055         include_trash: true,
1056       }
1057       assert_response :success
1058
1059       items = []
1060       json_response["items"].each do |coll|
1061         items << coll['uuid']
1062       end
1063
1064       assert_includes(items, collections('unique_expired_collection')['uuid'])
1065       if user == :admin
1066         assert_includes(items, collections('unique_expired_collection2')['uuid'])
1067       else
1068         assert_not_includes(items, collections('unique_expired_collection2')['uuid'])
1069       end
1070     end
1071   end
1072
1073   test 'untrash collection with same name as another with no ensure unique name' do
1074     authorize_with :active
1075     post :untrash, {
1076       id: collections(:trashed_collection_to_test_name_conflict_on_untrash).uuid,
1077     }
1078     assert_response 422
1079   end
1080
1081   test 'untrash collection with same name as another with ensure unique name' do
1082     authorize_with :active
1083     post :untrash, {
1084       id: collections(:trashed_collection_to_test_name_conflict_on_untrash).uuid,
1085       ensure_unique_name: true
1086     }
1087     assert_response 200
1088     assert_equal false, json_response['is_trashed']
1089     assert_nil json_response['trash_at']
1090     assert_nil json_response['delete_at']
1091     assert_match /^same name for trashed and persisted collections \(\d{4}-\d\d-\d\d.*?Z\)$/, json_response['name']
1092   end
1093
1094   test 'cannot show collection in trashed subproject' do
1095     authorize_with :active
1096     get :show, {
1097       id: collections(:collection_in_trashed_subproject).uuid,
1098       format: :json
1099     }
1100     assert_response 404
1101   end
1102
1103   test 'can show collection in untrashed subproject' do
1104     authorize_with :active
1105     Group.find_by_uuid(groups(:trashed_project).uuid).update! is_trashed: false
1106     get :show, {
1107       id: collections(:collection_in_trashed_subproject).uuid,
1108       format: :json,
1109     }
1110     assert_response :success
1111   end
1112
1113   test 'cannot index collection in trashed subproject' do
1114     authorize_with :active
1115     get :index, { limit: 1000 }
1116     assert_response :success
1117     item_uuids = json_response['items'].map do |item|
1118       item['uuid']
1119     end
1120     assert_not_includes(item_uuids, collections(:collection_in_trashed_subproject).uuid)
1121   end
1122
1123   test 'can index collection in untrashed subproject' do
1124     authorize_with :active
1125     Group.find_by_uuid(groups(:trashed_project).uuid).update! is_trashed: false
1126     get :index, { limit: 1000 }
1127     assert_response :success
1128     item_uuids = json_response['items'].map do |item|
1129       item['uuid']
1130     end
1131     assert_includes(item_uuids, collections(:collection_in_trashed_subproject).uuid)
1132   end
1133
1134   test 'can index trashed subproject collection with include_trash' do
1135     authorize_with :active
1136     get :index, {
1137           include_trash: true,
1138           limit: 1000
1139         }
1140     assert_response :success
1141     item_uuids = json_response['items'].map do |item|
1142       item['uuid']
1143     end
1144     assert_includes(item_uuids, collections(:collection_in_trashed_subproject).uuid)
1145   end
1146 end