Merge branch 'master' into 11454-wb-federated-search
[arvados.git] / services / api / test / functional / arvados / v1 / groups_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::GroupsControllerTest < ActionController::TestCase
8
9   test "attempt to delete group without read or write access" do
10     authorize_with :active
11     post :destroy, id: groups(:empty_lonely_group).uuid
12     assert_response 404
13   end
14
15   test "attempt to delete group without write access" do
16     authorize_with :active
17     post :destroy, id: groups(:all_users).uuid
18     assert_response 403
19   end
20
21   test "get list of projects" do
22     authorize_with :active
23     get :index, filters: [['group_class', '=', 'project']], format: :json
24     assert_response :success
25     group_uuids = []
26     json_response['items'].each do |group|
27       assert_equal 'project', group['group_class']
28       group_uuids << group['uuid']
29     end
30     assert_includes group_uuids, groups(:aproject).uuid
31     assert_includes group_uuids, groups(:asubproject).uuid
32     assert_not_includes group_uuids, groups(:system_group).uuid
33     assert_not_includes group_uuids, groups(:private).uuid
34   end
35
36   test "get list of groups that are not projects" do
37     authorize_with :active
38     get :index, filters: [['group_class', '!=', 'project']], format: :json
39     assert_response :success
40     group_uuids = []
41     json_response['items'].each do |group|
42       assert_not_equal 'project', group['group_class']
43       group_uuids << group['uuid']
44     end
45     assert_not_includes group_uuids, groups(:aproject).uuid
46     assert_not_includes group_uuids, groups(:asubproject).uuid
47     assert_includes group_uuids, groups(:private).uuid
48     assert_includes group_uuids, groups(:group_with_no_class).uuid
49   end
50
51   test "get list of groups with bogus group_class" do
52     authorize_with :active
53     get :index, {
54       filters: [['group_class', '=', 'nogrouphasthislittleclass']],
55       format: :json,
56     }
57     assert_response :success
58     assert_equal [], json_response['items']
59     assert_equal 0, json_response['items_available']
60   end
61
62   def check_project_contents_response disabled_kinds=[]
63     assert_response :success
64     assert_operator 2, :<=, json_response['items_available']
65     assert_operator 2, :<=, json_response['items'].count
66     kinds = json_response['items'].collect { |i| i['kind'] }.uniq
67     expect_kinds = %w'arvados#group arvados#specimen arvados#pipelineTemplate arvados#job' - disabled_kinds
68     assert_equal expect_kinds, (expect_kinds & kinds)
69
70     json_response['items'].each do |i|
71       if i['kind'] == 'arvados#group'
72         assert(i['group_class'] == 'project',
73                "group#contents returned a non-project group")
74       end
75     end
76
77     disabled_kinds.each do |d|
78       assert_equal true, !kinds.include?(d)
79     end
80   end
81
82   test 'get group-owned objects' do
83     authorize_with :active
84     get :contents, {
85       id: groups(:aproject).uuid,
86       format: :json,
87     }
88     check_project_contents_response
89   end
90
91   test "user with project read permission can see project objects" do
92     authorize_with :project_viewer
93     get :contents, {
94       id: groups(:aproject).uuid,
95       format: :json,
96     }
97     check_project_contents_response
98   end
99
100   test "list objects across projects" do
101     authorize_with :project_viewer
102     get :contents, {
103       format: :json,
104       filters: [['uuid', 'is_a', 'arvados#specimen']]
105     }
106     assert_response :success
107     found_uuids = json_response['items'].collect { |i| i['uuid'] }
108     [[:in_aproject, true],
109      [:in_asubproject, true],
110      [:owned_by_private_group, false]].each do |specimen_fixture, should_find|
111       if should_find
112         assert_includes found_uuids, specimens(specimen_fixture).uuid, "did not find specimen fixture '#{specimen_fixture}'"
113       else
114         refute_includes found_uuids, specimens(specimen_fixture).uuid, "found specimen fixture '#{specimen_fixture}'"
115       end
116     end
117   end
118
119   test "list objects in home project" do
120     authorize_with :active
121     get :contents, {
122       format: :json,
123       limit: 200,
124       id: users(:active).uuid
125     }
126     assert_response :success
127     found_uuids = json_response['items'].collect { |i| i['uuid'] }
128     assert_includes found_uuids, specimens(:owned_by_active_user).uuid, "specimen did not appear in home project"
129     refute_includes found_uuids, specimens(:in_asubproject).uuid, "specimen appeared unexpectedly in home project"
130   end
131
132   test "user with project read permission can see project collections" do
133     authorize_with :project_viewer
134     get :contents, {
135       id: groups(:asubproject).uuid,
136       format: :json,
137     }
138     ids = json_response['items'].map { |item| item["uuid"] }
139     assert_includes ids, collections(:baz_file_in_asubproject).uuid
140   end
141
142   [['asc', :<=],
143    ['desc', :>=]].each do |order, operator|
144     test "user with project read permission can sort project collections #{order}" do
145       authorize_with :project_viewer
146       get :contents, {
147         id: groups(:asubproject).uuid,
148         format: :json,
149         filters: [['uuid', 'is_a', "arvados#collection"]],
150         order: "collections.name #{order}"
151       }
152       sorted_names = json_response['items'].collect { |item| item["name"] }
153       # Here we avoid assuming too much about the database
154       # collation. Both "alice"<"Bob" and "alice">"Bob" can be
155       # correct. Hopefully it _is_ safe to assume that if "a" comes
156       # before "b" in the ascii alphabet, "aX">"bY" is never true for
157       # any strings X and Y.
158       reliably_sortable_names = sorted_names.select do |name|
159         name[0] >= 'a' and name[0] <= 'z'
160       end.uniq do |name|
161         name[0]
162       end
163       # Preserve order of sorted_names. But do not use &=. If
164       # sorted_names has out-of-order duplicates, we want to preserve
165       # them here, so we can detect them and fail the test below.
166       sorted_names.select! do |name|
167         reliably_sortable_names.include? name
168       end
169       actually_checked_anything = false
170       previous = nil
171       sorted_names.each do |entry|
172         if previous
173           assert_operator(previous, operator, entry,
174                           "Entries sorted incorrectly.")
175           actually_checked_anything = true
176         end
177         previous = entry
178       end
179       assert actually_checked_anything, "Didn't even find two names to compare."
180     end
181   end
182
183   test 'list objects across multiple projects' do
184     authorize_with :project_viewer
185     get :contents, {
186       format: :json,
187       filters: [['uuid', 'is_a', 'arvados#specimen']]
188     }
189     assert_response :success
190     found_uuids = json_response['items'].collect { |i| i['uuid'] }
191     [[:in_aproject, true],
192      [:in_asubproject, true],
193      [:owned_by_private_group, false]].each do |specimen_fixture, should_find|
194       if should_find
195         assert_includes found_uuids, specimens(specimen_fixture).uuid, "did not find specimen fixture '#{specimen_fixture}'"
196       else
197         refute_includes found_uuids, specimens(specimen_fixture).uuid, "found specimen fixture '#{specimen_fixture}'"
198       end
199     end
200   end
201
202   # Even though the project_viewer tests go through other controllers,
203   # I'm putting them here so they're easy to find alongside the other
204   # project tests.
205   def check_new_project_link_fails(link_attrs)
206     @controller = Arvados::V1::LinksController.new
207     post :create, link: {
208       link_class: "permission",
209       name: "can_read",
210       head_uuid: groups(:aproject).uuid,
211     }.merge(link_attrs)
212     assert_includes(403..422, response.status)
213   end
214
215   test "user with project read permission can't add users to it" do
216     authorize_with :project_viewer
217     check_new_project_link_fails(tail_uuid: users(:spectator).uuid)
218   end
219
220   test "user with project read permission can't add items to it" do
221     authorize_with :project_viewer
222     check_new_project_link_fails(tail_uuid: collections(:baz_file).uuid)
223   end
224
225   test "user with project read permission can't rename items in it" do
226     authorize_with :project_viewer
227     @controller = Arvados::V1::LinksController.new
228     post :update, {
229       id: jobs(:running).uuid,
230       name: "Denied test name",
231     }
232     assert_includes(403..404, response.status)
233   end
234
235   test "user with project read permission can't remove items from it" do
236     @controller = Arvados::V1::PipelineTemplatesController.new
237     authorize_with :project_viewer
238     post :update, {
239       id: pipeline_templates(:two_part).uuid,
240       pipeline_template: {
241         owner_uuid: users(:project_viewer).uuid,
242       }
243     }
244     assert_response 403
245   end
246
247   test "user with project read permission can't delete it" do
248     authorize_with :project_viewer
249     post :destroy, {id: groups(:aproject).uuid}
250     assert_response 403
251   end
252
253   test 'get group-owned objects with limit' do
254     authorize_with :active
255     get :contents, {
256       id: groups(:aproject).uuid,
257       limit: 1,
258       format: :json,
259     }
260     assert_response :success
261     assert_operator 1, :<, json_response['items_available']
262     assert_equal 1, json_response['items'].count
263   end
264
265   test 'get group-owned objects with limit and offset' do
266     authorize_with :active
267     get :contents, {
268       id: groups(:aproject).uuid,
269       limit: 1,
270       offset: 12345,
271       format: :json,
272     }
273     assert_response :success
274     assert_operator 1, :<, json_response['items_available']
275     assert_equal 0, json_response['items'].count
276   end
277
278   test 'get group-owned objects with additional filter matching nothing' do
279     authorize_with :active
280     get :contents, {
281       id: groups(:aproject).uuid,
282       filters: [['uuid', 'in', ['foo_not_a_uuid','bar_not_a_uuid']]],
283       format: :json,
284     }
285     assert_response :success
286     assert_equal [], json_response['items']
287     assert_equal 0, json_response['items_available']
288   end
289
290   %w(offset limit).each do |arg|
291     ['foo', '', '1234five', '0x10', '-8'].each do |val|
292       test "Raise error on bogus #{arg} parameter #{val.inspect}" do
293         authorize_with :active
294         get :contents, {
295           :id => groups(:aproject).uuid,
296           :format => :json,
297           arg => val,
298         }
299         assert_response 422
300       end
301     end
302   end
303
304   test "Collection contents don't include manifest_text" do
305     authorize_with :active
306     get :contents, {
307       id: groups(:aproject).uuid,
308       filters: [["uuid", "is_a", "arvados#collection"]],
309       format: :json,
310     }
311     assert_response :success
312     refute(json_response["items"].any? { |c| not c["portable_data_hash"] },
313            "response included an item without a portable data hash")
314     refute(json_response["items"].any? { |c| c.include?("manifest_text") },
315            "response included an item with a manifest text")
316   end
317
318   test 'get writable_by list for owned group' do
319     authorize_with :active
320     get :show, {
321       id: groups(:aproject).uuid,
322       format: :json
323     }
324     assert_response :success
325     assert_not_nil(json_response['writable_by'],
326                    "Should receive uuid list in 'writable_by' field")
327     assert_includes(json_response['writable_by'], users(:active).uuid,
328                     "owner should be included in writable_by list")
329   end
330
331   test 'no writable_by list for group with read-only access' do
332     authorize_with :rominiadmin
333     get :show, {
334       id: groups(:testusergroup_admins).uuid,
335       format: :json
336     }
337     assert_response :success
338     assert_equal([json_response['owner_uuid']],
339                  json_response['writable_by'],
340                  "Should only see owner_uuid in 'writable_by' field")
341   end
342
343   test 'get writable_by list by admin user' do
344     authorize_with :admin
345     get :show, {
346       id: groups(:testusergroup_admins).uuid,
347       format: :json
348     }
349     assert_response :success
350     assert_not_nil(json_response['writable_by'],
351                    "Should receive uuid list in 'writable_by' field")
352     assert_includes(json_response['writable_by'],
353                     users(:admin).uuid,
354                     "Current user should be included in 'writable_by' field")
355   end
356
357   test 'creating subproject with duplicate name fails' do
358     authorize_with :active
359     post :create, {
360       group: {
361         name: 'A Project',
362         owner_uuid: users(:active).uuid,
363         group_class: 'project',
364       },
365     }
366     assert_response 422
367     response_errors = json_response['errors']
368     assert_not_nil response_errors, 'Expected error in response'
369     assert(response_errors.first.include?('duplicate key'),
370            "Expected 'duplicate key' error in #{response_errors.first}")
371   end
372
373   test 'creating duplicate named subproject succeeds with ensure_unique_name' do
374     authorize_with :active
375     post :create, {
376       group: {
377         name: 'A Project',
378         owner_uuid: users(:active).uuid,
379         group_class: 'project',
380       },
381       ensure_unique_name: true
382     }
383     assert_response :success
384     new_project = json_response
385     assert_not_equal(new_project['uuid'],
386                      groups(:aproject).uuid,
387                      "create returned same uuid as existing project")
388     assert_match(/^A Project \(\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d\.\d{3}Z\)$/,
389                  new_project['name'])
390   end
391
392   test "unsharing a project results in hiding it from previously shared user" do
393     # remove sharing link for project
394     @controller = Arvados::V1::LinksController.new
395     authorize_with :admin
396     post :destroy, id: links(:share_starred_project_with_project_viewer).uuid
397     assert_response :success
398
399     # verify that the user can no longer see the project
400     @test_counter = 0  # Reset executed action counter
401     @controller = Arvados::V1::GroupsController.new
402     authorize_with :project_viewer
403     get :index, filters: [['group_class', '=', 'project']], format: :json
404     assert_response :success
405     found_projects = {}
406     json_response['items'].each do |g|
407       found_projects[g['uuid']] = g
408     end
409     assert_equal false, found_projects.include?(groups(:starred_and_shared_active_user_project).uuid)
410
411     # share the project
412     @test_counter = 0
413     @controller = Arvados::V1::LinksController.new
414     authorize_with :system_user
415     post :create, link: {
416       link_class: "permission",
417       name: "can_read",
418       head_uuid: groups(:starred_and_shared_active_user_project).uuid,
419       tail_uuid: users(:project_viewer).uuid,
420     }
421
422     # verify that project_viewer user can now see shared project again
423     @test_counter = 0
424     @controller = Arvados::V1::GroupsController.new
425     authorize_with :project_viewer
426     get :index, filters: [['group_class', '=', 'project']], format: :json
427     assert_response :success
428     found_projects = {}
429     json_response['items'].each do |g|
430       found_projects[g['uuid']] = g
431     end
432     assert_equal true, found_projects.include?(groups(:starred_and_shared_active_user_project).uuid)
433   end
434
435   [
436     [['owner_uuid', '!=', 'zzzzz-tpzed-xurymjxw79nv3jz'], 200,
437         'zzzzz-d1hrv-subprojpipeline', 'zzzzz-d1hrv-1xfj6xkicf2muk2'],
438     [["pipeline_instances.state", "not in", ["Complete", "Failed"]], 200,
439         'zzzzz-d1hrv-1xfj6xkicf2muk2', 'zzzzz-d1hrv-i3e77t9z5y8j9cc'],
440     [['container_requests.requesting_container_uuid', '=', nil], 200,
441         'zzzzz-xvhdp-cr4queuedcontnr', 'zzzzz-xvhdp-cr4requestercn2'],
442     [['container_requests.no_such_column', '=', nil], 422],
443     [['container_requests.', '=', nil], 422],
444     [['.requesting_container_uuid', '=', nil], 422],
445     [['no_such_table.uuid', '!=', 'zzzzz-tpzed-xurymjxw79nv3jz'], 422],
446   ].each do |filter, expect_code, expect_uuid, not_expect_uuid|
447     test "get contents with '#{filter}' filter" do
448       authorize_with :active
449       get :contents, filters: [filter], format: :json
450       assert_response expect_code
451       if expect_code == 200
452         assert_not_empty json_response['items']
453         item_uuids = json_response['items'].collect {|item| item['uuid']}
454         assert_includes(item_uuids, expect_uuid)
455         assert_not_includes(item_uuids, not_expect_uuid)
456       end
457     end
458   end
459
460   test 'get contents with jobs and pipeline instances disabled' do
461     Rails.configuration.disable_api_methods = ['jobs.index', 'pipeline_instances.index']
462
463     authorize_with :active
464     get :contents, {
465       id: groups(:aproject).uuid,
466       format: :json,
467     }
468     check_project_contents_response %w'arvados#pipelineInstance arvados#job'
469   end
470
471   test 'get contents with low max_index_database_read' do
472     # Some result will certainly have at least 12 bytes in a
473     # restricted column
474     Rails.configuration.max_index_database_read = 12
475     authorize_with :active
476     get :contents, {
477           id: groups(:aproject).uuid,
478           format: :json,
479         }
480     assert_response :success
481     assert_not_empty(json_response['items'])
482     assert_operator(json_response['items'].count,
483                     :<, json_response['items_available'])
484   end
485
486   test 'get contents, recursive=true' do
487     authorize_with :active
488     params = {
489       id: groups(:aproject).uuid,
490       recursive: true,
491       format: :json,
492     }
493     get :contents, params
494     owners = json_response['items'].map do |item|
495       item['owner_uuid']
496     end
497     assert_includes(owners, groups(:aproject).uuid)
498     assert_includes(owners, groups(:asubproject).uuid)
499   end
500
501   [false, nil].each do |recursive|
502     test "get contents, recursive=#{recursive.inspect}" do
503       authorize_with :active
504       params = {
505         id: groups(:aproject).uuid,
506         format: :json,
507       }
508       params[:recursive] = false if recursive == false
509       get :contents, params
510       owners = json_response['items'].map do |item|
511         item['owner_uuid']
512       end
513       assert_includes(owners, groups(:aproject).uuid)
514       refute_includes(owners, groups(:asubproject).uuid)
515     end
516   end
517
518   test 'get home project contents, recursive=true' do
519     authorize_with :active
520     get :contents, {
521           id: users(:active).uuid,
522           recursive: true,
523           format: :json,
524         }
525     owners = json_response['items'].map do |item|
526       item['owner_uuid']
527     end
528     assert_includes(owners, users(:active).uuid)
529     assert_includes(owners, groups(:aproject).uuid)
530     assert_includes(owners, groups(:asubproject).uuid)
531   end
532
533   ### trashed project tests ###
534
535   [:active, :admin].each do |auth|
536     # project: to query,    to untrash,    is visible, parent contents listing success
537     [[:trashed_project,     [],                 false, true],
538      [:trashed_project,     [:trashed_project], true,  true],
539      [:trashed_subproject,  [],                 false, false],
540      [:trashed_subproject,  [:trashed_project], true,  true],
541      [:trashed_subproject3, [:trashed_project], false, true],
542      [:trashed_subproject3, [:trashed_subproject3], false, false],
543      [:trashed_subproject3, [:trashed_project, :trashed_subproject3], true, true],
544     ].each do |project, untrash, visible, success|
545
546       test "contents listing #{project} #{untrash} as #{auth}" do
547         authorize_with auth
548         untrash.each do |pr|
549           Group.find_by_uuid(groups(pr).uuid).update! is_trashed: false
550         end
551         get :contents, {
552               id: groups(project).owner_uuid,
553               format: :json
554             }
555         if success
556           assert_response :success
557           item_uuids = json_response['items'].map do |item|
558             item['uuid']
559           end
560           if visible
561             assert_includes(item_uuids, groups(project).uuid)
562           else
563             assert_not_includes(item_uuids, groups(project).uuid)
564           end
565         else
566           assert_response 404
567         end
568       end
569
570       test "contents of #{project} #{untrash} as #{auth}" do
571         authorize_with auth
572         untrash.each do |pr|
573           Group.find_by_uuid(groups(pr).uuid).update! is_trashed: false
574         end
575         get :contents, {
576               id: groups(project).uuid,
577               format: :json
578             }
579         if visible
580           assert_response :success
581         else
582           assert_response 404
583         end
584       end
585
586       test "index #{project} #{untrash} as #{auth}" do
587         authorize_with auth
588         untrash.each do |pr|
589           Group.find_by_uuid(groups(pr).uuid).update! is_trashed: false
590         end
591         get :index, {
592               format: :json,
593             }
594         assert_response :success
595         item_uuids = json_response['items'].map do |item|
596           item['uuid']
597         end
598         if visible
599           assert_includes(item_uuids, groups(project).uuid)
600         else
601           assert_not_includes(item_uuids, groups(project).uuid)
602         end
603       end
604
605       test "show #{project} #{untrash} as #{auth}" do
606         authorize_with auth
607         untrash.each do |pr|
608           Group.find_by_uuid(groups(pr).uuid).update! is_trashed: false
609         end
610         get :show, {
611               id: groups(project).uuid,
612               format: :json
613             }
614         if visible
615           assert_response :success
616         else
617           assert_response 404
618         end
619       end
620
621       test "show include_trash #{project} #{untrash} as #{auth}" do
622         authorize_with auth
623         untrash.each do |pr|
624           Group.find_by_uuid(groups(pr).uuid).update! is_trashed: false
625         end
626         get :show, {
627               id: groups(project).uuid,
628               format: :json,
629               include_trash: true
630             }
631         assert_response :success
632       end
633
634       test "index include_trash #{project} #{untrash} as #{auth}" do
635         authorize_with auth
636         untrash.each do |pr|
637           Group.find_by_uuid(groups(pr).uuid).update! is_trashed: false
638         end
639         get :index, {
640               format: :json,
641               include_trash: true
642             }
643         assert_response :success
644         item_uuids = json_response['items'].map do |item|
645           item['uuid']
646         end
647         assert_includes(item_uuids, groups(project).uuid)
648       end
649     end
650
651     test "delete project #{auth}" do
652       authorize_with auth
653       [:trashed_project].each do |pr|
654         Group.find_by_uuid(groups(pr).uuid).update! is_trashed: false
655       end
656       assert !Group.find_by_uuid(groups(:trashed_project).uuid).is_trashed
657       post :destroy, {
658             id: groups(:trashed_project).uuid,
659             format: :json,
660           }
661       assert_response :success
662       assert Group.find_by_uuid(groups(:trashed_project).uuid).is_trashed
663     end
664
665     test "untrash project #{auth}" do
666       authorize_with auth
667       assert Group.find_by_uuid(groups(:trashed_project).uuid).is_trashed
668       post :untrash, {
669             id: groups(:trashed_project).uuid,
670             format: :json,
671           }
672       assert_response :success
673       assert !Group.find_by_uuid(groups(:trashed_project).uuid).is_trashed
674     end
675
676     test "untrash project with name conflict #{auth}" do
677       authorize_with auth
678       [:trashed_project].each do |pr|
679         Group.find_by_uuid(groups(pr).uuid).update! is_trashed: false
680       end
681       gc = Group.create!({owner_uuid: "zzzzz-j7d0g-trashedproject1",
682                          name: "trashed subproject 3",
683                          group_class: "project"})
684       post :untrash, {
685             id: groups(:trashed_subproject3).uuid,
686             format: :json,
687             ensure_unique_name: true
688            }
689       assert_response :success
690       assert_match /^trashed subproject 3 \(\d{4}-\d\d-\d\d.*?Z\)$/, json_response['name']
691     end
692
693     test "move trashed subproject to new owner #{auth}" do
694       authorize_with auth
695       assert_nil Group.readable_by(users(auth)).where(uuid: groups(:trashed_subproject).uuid).first
696       put :update, {
697             id: groups(:trashed_subproject).uuid,
698             group: {
699               owner_uuid: users(:active).uuid
700             },
701             include_trash: true,
702             format: :json,
703           }
704       assert_response :success
705       assert_not_nil Group.readable_by(users(auth)).where(uuid: groups(:trashed_subproject).uuid).first
706     end
707   end
708 end