X-Git-Url: https://git.arvados.org/arvados.git/blobdiff_plain/2e104941dbf1e4bf92e0632cadeb946be0595d67..2b8f39b5067103d60f7bc43d26cb2f59126b2a42:/services/api/test/functional/arvados/v1/collections_controller_test.rb diff --git a/services/api/test/functional/arvados/v1/collections_controller_test.rb b/services/api/test/functional/arvados/v1/collections_controller_test.rb index b96e22ed65..26b8290e69 100644 --- a/services/api/test/functional/arvados/v1/collections_controller_test.rb +++ b/services/api/test/functional/arvados/v1/collections_controller_test.rb @@ -1,18 +1,47 @@ +# Copyright (C) The Arvados Authors. All rights reserved. +# +# SPDX-License-Identifier: AGPL-3.0 + require 'test_helper' class Arvados::V1::CollectionsControllerTest < ActionController::TestCase + include DbCurrentTime + + PERM_TOKEN_RE = /\+A[[:xdigit:]]+@[[:xdigit:]]{8}\b/ def permit_unsigned_manifests isok=true # Set security model for the life of a test. Rails.configuration.permit_create_collection_with_unsigned_manifest = isok end - def assert_signed_manifest manifest_text, label='' + def assert_signed_manifest manifest_text, label='', token: false assert_not_nil manifest_text, "#{label} manifest_text was nil" manifest_text.scan(/ [[:xdigit:]]{32}\S*/) do |tok| - assert_match(/\+A[[:xdigit:]]+@[[:xdigit:]]{8}\b/, tok, + assert_match(PERM_TOKEN_RE, tok, "Locator in #{label} manifest_text was not signed") + if token + bare = tok.gsub(/\+A[^\+]*/, '').sub(/^ /, '') + exp = tok[/\+A[[:xdigit:]]+@([[:xdigit:]]+)/, 1].to_i(16) + sig = Blob.sign_locator( + bare, + key: Rails.configuration.blob_signing_key, + expire: exp, + api_token: token)[/\+A[^\+]*/, 0] + assert_includes tok, sig + end + end + end + + def assert_unsigned_manifest resp, label='' + txt = resp['unsigned_manifest_text'] + assert_not_nil(txt, "#{label} unsigned_manifest_text was nil") + locs = 0 + txt.scan(/ [[:xdigit:]]{32}\S*/) do |tok| + locs += 1 + refute_match(PERM_TOKEN_RE, tok, + "Locator in #{label} unsigned_manifest_text was signed: #{tok}") end + return locs end test "should get index" do @@ -22,14 +51,55 @@ class Arvados::V1::CollectionsControllerTest < ActionController::TestCase assert(assigns(:objects).andand.any?, "no Collections returned in index") refute(json_response["items"].any? { |c| c.has_key?("manifest_text") }, "basic Collections index included manifest_text") + refute(json_response["items"].any? { |c| c["uuid"] == collections(:collection_owned_by_active_past_version_1).uuid }, + "basic Collections index included past version") + end + + test "get index with include_old_versions" do + authorize_with :active + get :index, { + include_old_versions: true + } + assert_response :success + assert(assigns(:objects).andand.any?, "no Collections returned in index") + assert(json_response["items"].any? { |c| c["uuid"] == collections(:collection_owned_by_active_past_version_1).uuid }, + "past version not included on index") end - test "collections.get returns signed locators" do + test "collections.get returns signed locators, and no unsigned_manifest_text" do permit_unsigned_manifests authorize_with :active get :show, {id: collections(:foo_file).uuid} assert_response :success assert_signed_manifest json_response['manifest_text'], 'foo_file' + refute_includes json_response, 'unsigned_manifest_text' + end + + ['v1token', 'v2token'].each do |token_method| + test "correct signatures are given for #{token_method}" do + token = api_client_authorizations(:active).send(token_method) + authorize_with_token token + get :show, {id: collections(:foo_file).uuid} + assert_response :success + assert_signed_manifest json_response['manifest_text'], 'foo_file', token: token + end + + test "signatures with #{token_method} are accepted" do + token = api_client_authorizations(:active).send(token_method) + signed = Blob.sign_locator( + 'acbd18db4cc2f85cedef654fccc4a4d8+3', + key: Rails.configuration.blob_signing_key, + api_token: token) + authorize_with_token token + put :update, { + id: collections(:collection_owned_by_active).uuid, + collection: { + manifest_text: ". #{signed} 0:3:foo.txt\n", + }, + } + assert_response :success + assert_signed_manifest json_response['manifest_text'], 'updated', token: token + end end test "index with manifest_text selected returns signed locators" do @@ -40,12 +110,26 @@ class Arvados::V1::CollectionsControllerTest < ActionController::TestCase assert(assigns(:objects).andand.any?, "no Collections returned for index with columns selected") json_response["items"].each do |coll| - assert_equal(columns, columns & coll.keys, + assert_equal(coll.keys - ['kind'], columns, "Collections index did not respect selected columns") assert_signed_manifest coll['manifest_text'], coll['uuid'] end end + test "index with unsigned_manifest_text selected returns only unsigned locators" do + authorize_with :active + get :index, select: ['unsigned_manifest_text'] + assert_response :success + assert_operator json_response["items"].count, :>, 0 + locs = 0 + json_response["items"].each do |coll| + assert_equal(coll.keys - ['kind'], ['unsigned_manifest_text'], + "Collections index did not respect selected columns") + locs += assert_unsigned_manifest coll, coll['uuid'] + end + assert_operator locs, :>, 0, "no locators found in any manifests" + end + test 'index without select returns everything except manifest' do authorize_with :active get :index @@ -253,12 +337,12 @@ EOS assert_response :success assert_not_nil assigns(:object) resp = assigns(:object) - assert_equal foo_collection[:portable_data_hash], resp['portable_data_hash'] - assert_signed_manifest resp['manifest_text'] + assert_equal foo_collection[:portable_data_hash], resp[:portable_data_hash] + assert_signed_manifest resp[:manifest_text] # The manifest in the response will have had permission hints added. # Remove any permission hints in the response before comparing it to the source. - stripped_manifest = resp['manifest_text'].gsub(/\+A[A-Za-z0-9@_-]+/, '') + stripped_manifest = resp[:manifest_text].gsub(/\+A[A-Za-z0-9@_-]+/, '') assert_equal foo_collection[:manifest_text], stripped_manifest end end @@ -315,7 +399,7 @@ EOS ensure_unique_name: true } assert_response :success - assert_equal 'owned_by_active (2)', json_response['name'] + assert_match /^owned_by_active \(\d{4}-\d\d-\d\d.*?Z\)$/, json_response['name'] end end @@ -930,4 +1014,277 @@ EOS assert_response 200 end end + + test 'get trashed collection with include_trash' do + uuid = 'zzzzz-4zz18-mto52zx1s7sn3ih' # expired_collection + authorize_with :active + get :show, { + id: uuid, + include_trash: true, + } + assert_response 200 + end + + [:admin, :active].each do |user| + test "get trashed collection via filters and #{user} user" do + uuid = 'zzzzz-4zz18-mto52zx1s7sn3ih' # expired_collection + authorize_with user + get :index, { + filters: [["current_version_uuid", "=", uuid]], + include_trash: true, + } + assert_response 200 + # Only the current version is returned + assert_equal 1, json_response["items"].size + end + end + + [:admin, :active].each do |user| + test "get trashed collection via filters and #{user} user, including its past versions" do + uuid = 'zzzzz-4zz18-mto52zx1s7sn3ih' # expired_collection + authorize_with :admin + get :index, { + filters: [["current_version_uuid", "=", uuid]], + include_trash: true, + include_old_versions: true, + } + assert_response 200 + # Both current & past version are returned + assert_equal 2, json_response["items"].size + end + end + + test "trash collection also trash its past versions" do + uuid = collections(:collection_owned_by_active).uuid + authorize_with :active + versions = Collection.where(current_version_uuid: uuid) + assert_equal 2, versions.size + versions.each do |col| + refute col.is_trashed + end + post :trash, { + id: uuid, + } + assert_response 200 + versions = Collection.where(current_version_uuid: uuid) + assert_equal 2, versions.size + versions.each do |col| + assert col.is_trashed + end + end + + test 'get trashed collection without include_trash' do + uuid = 'zzzzz-4zz18-mto52zx1s7sn3ih' # expired_collection + authorize_with :active + get :show, { + id: uuid, + } + assert_response 404 + end + + test 'trash collection using http DELETE verb' do + uuid = collections(:collection_owned_by_active).uuid + authorize_with :active + delete :destroy, { + id: uuid, + } + assert_response 200 + c = Collection.find_by_uuid(uuid) + assert_operator c.trash_at, :<, db_current_time + assert_equal c.delete_at, c.trash_at + Rails.configuration.blob_signature_ttl + end + + test 'delete long-trashed collection immediately using http DELETE verb' do + uuid = 'zzzzz-4zz18-mto52zx1s7sn3ih' # expired_collection + authorize_with :active + delete :destroy, { + id: uuid, + } + assert_response 200 + c = Collection.find_by_uuid(uuid) + assert_operator c.trash_at, :<, db_current_time + assert_operator c.delete_at, :<, db_current_time + end + + ['zzzzz-4zz18-mto52zx1s7sn3ih', # expired_collection + :empty_collection_name_in_active_user_home_project, + ].each do |fixture| + test "trash collection #{fixture} via trash action with grace period" do + if fixture.is_a? String + uuid = fixture + else + uuid = collections(fixture).uuid + end + authorize_with :active + time_before_trashing = db_current_time + post :trash, { + id: uuid, + } + assert_response 200 + c = Collection.find_by_uuid(uuid) + assert_operator c.trash_at, :<, db_current_time + assert_operator c.delete_at, :>=, time_before_trashing + Rails.configuration.default_trash_lifetime + end + end + + test 'untrash a trashed collection' do + authorize_with :active + post :untrash, { + id: collections(:expired_collection).uuid, + } + assert_response 200 + assert_equal false, json_response['is_trashed'] + assert_nil json_response['trash_at'] + end + + test 'untrash error on not trashed collection' do + authorize_with :active + post :untrash, { + id: collections(:collection_owned_by_active).uuid, + } + assert_response 422 + end + + [:active, :admin].each do |user| + test "get trashed collections as #{user}" do + authorize_with user + get :index, { + filters: [["is_trashed", "=", true]], + include_trash: true, + } + assert_response :success + + items = [] + json_response["items"].each do |coll| + items << coll['uuid'] + end + + assert_includes(items, collections('unique_expired_collection')['uuid']) + if user == :admin + assert_includes(items, collections('unique_expired_collection2')['uuid']) + else + assert_not_includes(items, collections('unique_expired_collection2')['uuid']) + end + end + end + + test 'untrash collection with same name as another with no ensure unique name' do + authorize_with :active + post :untrash, { + id: collections(:trashed_collection_to_test_name_conflict_on_untrash).uuid, + } + assert_response 422 + end + + test 'untrash collection with same name as another with ensure unique name' do + authorize_with :active + post :untrash, { + id: collections(:trashed_collection_to_test_name_conflict_on_untrash).uuid, + ensure_unique_name: true + } + assert_response 200 + assert_equal false, json_response['is_trashed'] + assert_nil json_response['trash_at'] + assert_nil json_response['delete_at'] + assert_match /^same name for trashed and persisted collections \(\d{4}-\d\d-\d\d.*?Z\)$/, json_response['name'] + end + + test 'cannot show collection in trashed subproject' do + authorize_with :active + get :show, { + id: collections(:collection_in_trashed_subproject).uuid, + format: :json + } + assert_response 404 + end + + test 'can show collection in untrashed subproject' do + authorize_with :active + Group.find_by_uuid(groups(:trashed_project).uuid).update! is_trashed: false + get :show, { + id: collections(:collection_in_trashed_subproject).uuid, + format: :json, + } + assert_response :success + end + + test 'cannot index collection in trashed subproject' do + authorize_with :active + get :index, { limit: 1000 } + assert_response :success + item_uuids = json_response['items'].map do |item| + item['uuid'] + end + assert_not_includes(item_uuids, collections(:collection_in_trashed_subproject).uuid) + end + + test 'can index collection in untrashed subproject' do + authorize_with :active + Group.find_by_uuid(groups(:trashed_project).uuid).update! is_trashed: false + get :index, { limit: 1000 } + assert_response :success + item_uuids = json_response['items'].map do |item| + item['uuid'] + end + assert_includes(item_uuids, collections(:collection_in_trashed_subproject).uuid) + end + + test 'can index trashed subproject collection with include_trash' do + authorize_with :active + get :index, { + include_trash: true, + limit: 1000 + } + assert_response :success + item_uuids = json_response['items'].map do |item| + item['uuid'] + end + assert_includes(item_uuids, collections(:collection_in_trashed_subproject).uuid) + end + + test 'can get collection with past versions' do + authorize_with :active + get :index, { + filters: [['current_version_uuid','=',collections(:collection_owned_by_active).uuid]], + include_old_versions: true + } + assert_response :success + assert_equal 2, assigns(:objects).length + assert_equal 2, json_response['items_available'] + assert_equal 2, json_response['items'].count + json_response['items'].each do |c| + assert_equal collections(:collection_owned_by_active).uuid, + c['current_version_uuid'], + 'response includes a version from a different collection' + end + end + + test 'can get old version collection by uuid' do + authorize_with :active + get :show, { + id: collections(:collection_owned_by_active_past_version_1).uuid, + } + assert_response :success + assert_equal collections(:collection_owned_by_active_past_version_1).name, + json_response['name'] + end + + test 'version and current_version_uuid are ignored at creation time' do + permit_unsigned_manifests + authorize_with :active + manifest_text = ". d41d8cd98f00b204e9800998ecf8427e 0:0:foo.txt\n" + post :create, { + collection: { + name: 'Test collection', + version: 42, + current_version_uuid: collections(:collection_owned_by_active).uuid, + manifest_text: manifest_text, + # portable_data_hash: "d30fe8ae534397864cb96c544f4cf102+47" + } + } + assert_response :success + resp = JSON.parse(@response.body) + assert_equal 1, resp['version'] + assert_equal resp['uuid'], resp['current_version_uuid'] + end end