Merge branch '8784-dir-listings'
[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 end