From 6d040c68340fa3615412673c38ece4b677601dbc Mon Sep 17 00:00:00 2001 From: Vladimir Avdeev Date: Thu, 25 Aug 2022 13:57:55 +0300 Subject: [PATCH] Add ACL and eACL PyTest tests Signed-off-by: Vladimir Avdeev --- pytest_tests/testsuites/acl/conftest.py | 80 +++++++ pytest_tests/testsuites/acl/test_acl.py | 177 +++++++--------- pytest_tests/testsuites/acl/test_bearer.py | 108 ++++++++++ pytest_tests/testsuites/acl/test_eacl.py | 105 ++++++++++ .../testsuites/acl/test_eacl_filters.py | 197 ++++++++++++++++++ .../testsuites/container/test_container.py | 3 +- .../testsuites/object/test_object_api.py | 4 +- .../eacl_tables/gen_eacl_allow_all_OTHERS | 74 ------- .../eacl_tables/gen_eacl_allow_all_SYSTEM | 74 ------- .../files/eacl_tables/gen_eacl_allow_all_USER | 74 ------- .../eacl_tables/gen_eacl_compound_del_OTHERS | 34 --- .../eacl_tables/gen_eacl_compound_del_SYSTEM | 34 --- .../eacl_tables/gen_eacl_compound_del_USER | 34 --- .../eacl_tables/gen_eacl_compound_get_OTHERS | 44 ---- .../eacl_tables/gen_eacl_compound_get_SYSTEM | 44 ---- .../eacl_tables/gen_eacl_compound_get_USER | 44 ---- .../gen_eacl_compound_get_hash_OTHERS | 34 --- .../gen_eacl_compound_get_hash_SYSTEM | 34 --- .../gen_eacl_compound_get_hash_USER | 34 --- .../eacl_tables/gen_eacl_deny_all_OTHERS | 74 ------- .../eacl_tables/gen_eacl_deny_all_SYSTEM | 74 ------- .../files/eacl_tables/gen_eacl_deny_all_USER | 74 ------- .../eacl_tables/gen_eacl_xheader_allow_all | 193 ----------------- .../eacl_tables/gen_eacl_xheader_deny_all | 123 ----------- robot/resources/lib/python_keywords/acl.py | 169 +++++++++------ .../resources/lib/python_keywords/cli/acl.py | 47 +++++ .../resources/lib/python_keywords/cli/cli.py | 3 + .../lib/python_keywords/cli/cli_command.py | 14 +- .../lib/python_keywords/cli/container.py | 6 +- .../lib/python_keywords/cli/object.py | 33 ++- .../lib/python_keywords/container.py | 5 +- .../lib/python_keywords/container_access.py | 70 +++++++ .../lib/python_keywords/neofs_verbs.py | 71 ++++--- .../lib/python_keywords/object_access.py | 100 +++++++++ .../lib/python_keywords/tombstone.py | 7 +- robot/variables/common.py | 2 +- 36 files changed, 979 insertions(+), 1318 deletions(-) create mode 100644 pytest_tests/testsuites/acl/conftest.py create mode 100644 pytest_tests/testsuites/acl/test_bearer.py create mode 100644 pytest_tests/testsuites/acl/test_eacl.py create mode 100644 pytest_tests/testsuites/acl/test_eacl_filters.py delete mode 100644 robot/resources/files/eacl_tables/gen_eacl_allow_all_OTHERS delete mode 100644 robot/resources/files/eacl_tables/gen_eacl_allow_all_SYSTEM delete mode 100644 robot/resources/files/eacl_tables/gen_eacl_allow_all_USER delete mode 100644 robot/resources/files/eacl_tables/gen_eacl_compound_del_OTHERS delete mode 100644 robot/resources/files/eacl_tables/gen_eacl_compound_del_SYSTEM delete mode 100644 robot/resources/files/eacl_tables/gen_eacl_compound_del_USER delete mode 100644 robot/resources/files/eacl_tables/gen_eacl_compound_get_OTHERS delete mode 100644 robot/resources/files/eacl_tables/gen_eacl_compound_get_SYSTEM delete mode 100644 robot/resources/files/eacl_tables/gen_eacl_compound_get_USER delete mode 100644 robot/resources/files/eacl_tables/gen_eacl_compound_get_hash_OTHERS delete mode 100644 robot/resources/files/eacl_tables/gen_eacl_compound_get_hash_SYSTEM delete mode 100644 robot/resources/files/eacl_tables/gen_eacl_compound_get_hash_USER delete mode 100644 robot/resources/files/eacl_tables/gen_eacl_deny_all_OTHERS delete mode 100644 robot/resources/files/eacl_tables/gen_eacl_deny_all_SYSTEM delete mode 100644 robot/resources/files/eacl_tables/gen_eacl_deny_all_USER delete mode 100644 robot/resources/files/eacl_tables/gen_eacl_xheader_allow_all delete mode 100644 robot/resources/files/eacl_tables/gen_eacl_xheader_deny_all create mode 100644 robot/resources/lib/python_keywords/cli/acl.py create mode 100644 robot/resources/lib/python_keywords/container_access.py create mode 100644 robot/resources/lib/python_keywords/object_access.py diff --git a/pytest_tests/testsuites/acl/conftest.py b/pytest_tests/testsuites/acl/conftest.py new file mode 100644 index 0000000..bdd79df --- /dev/null +++ b/pytest_tests/testsuites/acl/conftest.py @@ -0,0 +1,80 @@ +from dataclasses import dataclass +from typing import Dict, List, Optional + +import allure +import pytest + +from common import ASSETS_DIR, IR_WALLET_CONFIG, IR_WALLET_PATH, WALLET_CONFIG +from python_keywords.acl import EACLRole +from python_keywords.container import create_container +from python_keywords.neofs_verbs import put_object +from python_keywords.utility_keywords import generate_file +from wallet import init_wallet +from wellknown_acl import PUBLIC_ACL + +OBJECT_COUNT = 5 + + +@dataclass +class Wallet: + wallet_path: Optional[str] = None + config_path: Optional[str] = None + + +@dataclass +class Wallets: + wallets: Dict[EACLRole, List[Wallet]] + + def get_wallet(self, role: EACLRole = EACLRole.USER) -> Wallet: + return self.wallets[role][0] + + def get_wallets_list(self, role: EACLRole = EACLRole.USER) -> List[Wallet]: + return self.wallets[role] + + +@pytest.fixture(scope="module") +def wallets(prepare_wallet_and_deposit): + yield Wallets(wallets={ + EACLRole.USER: [ + Wallet( + wallet_path=prepare_wallet_and_deposit, + config_path=WALLET_CONFIG + )], + EACLRole.OTHERS: [ + Wallet( + wallet_path=init_wallet(ASSETS_DIR)[0], + config_path=WALLET_CONFIG + ), + Wallet( + wallet_path=init_wallet(ASSETS_DIR)[0], + config_path=WALLET_CONFIG + )], + EACLRole.SYSTEM: [ + Wallet( + wallet_path=IR_WALLET_PATH, + config_path=IR_WALLET_CONFIG + )], + }) + + +@pytest.fixture(scope="module") +def file_path(): + yield generate_file() + + +@pytest.fixture(scope='function') +def eacl_container_with_objects(wallets, file_path): + user_wallet = wallets.get_wallet() + with allure.step('Create eACL public container'): + cid = create_container(user_wallet.wallet_path, basic_acl=PUBLIC_ACL) + + with allure.step('Add test objects to container'): + objects_oids = [ + put_object( + user_wallet.wallet_path, file_path, cid, + attributes={'key1': 'val1', 'key': val, 'key2': 'abc'}) for val in range(OBJECT_COUNT)] + + yield cid, objects_oids, file_path + + # with allure.step('Delete eACL public container'): + # delete_container(user_wallet, cid) diff --git a/pytest_tests/testsuites/acl/test_acl.py b/pytest_tests/testsuites/acl/test_acl.py index e659c5b..8b5cade 100644 --- a/pytest_tests/testsuites/acl/test_acl.py +++ b/pytest_tests/testsuites/acl/test_acl.py @@ -1,117 +1,102 @@ -import os -from typing import Tuple - import allure import pytest -import wallet -from common import ASSETS_DIR -from grpc_responses import OBJECT_ACCESS_DENIED -from python_keywords.acl import set_eacl +from python_keywords.acl import EACLRole from python_keywords.container import create_container -from python_keywords.neofs_verbs import (delete_object, get_object, get_range, - get_range_hash, head_object, - put_object, search_object) -from python_keywords.utility_keywords import generate_file, get_file_hash - -RESOURCE_DIR = os.path.join( - os.path.dirname(os.path.realpath(__file__)), - '../../../robot/resources/files/', -) +from python_keywords.container_access import (check_full_access_to_container, check_no_access_to_container, + check_read_only_container) +from python_keywords.neofs_verbs import put_object +from wellknown_acl import PRIVATE_ACL_F, PUBLIC_ACL_F, READONLY_ACL_F @pytest.mark.sanity @pytest.mark.acl -class TestACL: - @pytest.fixture(autouse=True) - def create_two_wallets(self, prepare_wallet_and_deposit): - self.main_wallet = prepare_wallet_and_deposit - self.other_wallet = wallet.init_wallet(ASSETS_DIR)[0] # We need wallet file path only +@pytest.mark.acl_container +class TestACLBasic: - @allure.title('Test basic ACL') - def test_basic_acl(self): + @pytest.fixture(scope='function') + def public_container(self, wallets): + user_wallet = wallets.get_wallet() + with allure.step('Create public container'): + cid_public = create_container(user_wallet.wallet_path, basic_acl=PUBLIC_ACL_F) + + yield cid_public + + # with allure.step('Delete public container'): + # delete_container(user_wallet.wallet_path, cid_public) + + @pytest.fixture(scope='function') + def private_container(self, wallets): + user_wallet = wallets.get_wallet() + with allure.step('Create private container'): + cid_private = create_container(user_wallet.wallet_path, basic_acl=PRIVATE_ACL_F) + + yield cid_private + + # with allure.step('Delete private container'): + # delete_container(user_wallet.wallet_path, cid_private) + + @pytest.fixture(scope='function') + def read_only_container(self, wallets): + user_wallet = wallets.get_wallet() + with allure.step('Create public readonly container'): + cid_read_only = create_container(user_wallet.wallet_path, basic_acl=READONLY_ACL_F) + + yield cid_read_only + + # with allure.step('Delete public readonly container'): + # delete_container(user_wallet.wallet_path, cid_read_only) + + @allure.title('Test basic ACL on public container') + def test_basic_acl_public(self, wallets, public_container, file_path): """ - Test basic ACL set during container creation. + Test basic ACL set during public container creation. """ - file_name = generate_file() + user_wallet = wallets.get_wallet() + other_wallet = wallets.get_wallet(role=EACLRole.OTHERS) + cid = public_container + for wallet, desc in ((user_wallet, 'owner'), (other_wallet, 'other users')): + with allure.step('Add test objects to container'): + # We create new objects for each wallet because check_full_access_to_container deletes the object + owner_object_oid = put_object(user_wallet.wallet_path, file_path, cid, attributes={'created': 'owner'}) + other_object_oid = put_object(other_wallet.wallet_path, file_path, cid, attributes={'created': 'other'}) + with allure.step(f'Check {desc} has full access to public container'): + check_full_access_to_container(wallet.wallet_path, cid, owner_object_oid, file_path) + check_full_access_to_container(wallet.wallet_path, cid, other_object_oid, file_path) - with allure.step('Create public container and check access'): - cid_public = create_container(self.main_wallet, basic_acl='public-read-write') - self.check_full_access(cid_public, file_name) - - with allure.step('Create private container and check only owner has access'): - cid_private = create_container(self.main_wallet, basic_acl='private') - - with allure.step('Check owner can put/get object into private container'): - oid = put_object(wallet=self.main_wallet, path=file_name, cid=cid_private) - - got_file = get_object(self.main_wallet, cid_private, oid) - assert get_file_hash(got_file) == get_file_hash(file_name) + @allure.title('Test basic ACL on private container') + def test_basic_acl_private(self, wallets, private_container, file_path): + """ + Test basic ACL set during private container creation. + """ + user_wallet = wallets.get_wallet() + other_wallet = wallets.get_wallet(role=EACLRole.OTHERS) + cid = private_container + with allure.step('Add test objects to container'): + owner_object_oid = put_object(user_wallet.wallet_path, file_path, cid) + with allure.step('Check only owner has full access to private container'): with allure.step('Check no one except owner has access to operations with container'): - self.check_no_access_to_container(self.other_wallet, cid_private, oid, file_name) + check_no_access_to_container(other_wallet.wallet_path, cid, owner_object_oid, file_path) - delete_object(self.main_wallet, cid_private, oid) + with allure.step('Check owner has full access to private container'): + check_full_access_to_container( + user_wallet.wallet_path, cid, owner_object_oid, file_path) - @allure.title('Test extended ACL') - def test_extended_acl(self): + @allure.title('Test basic ACL on readonly container') + def test_basic_acl_readonly(self, wallets, read_only_container, file_path): """ - Test basic extended ACL applied after container creation. + Test basic ACL Operations for Read-Only Container. """ - file_name = generate_file() - deny_all_eacl = os.path.join(RESOURCE_DIR, 'eacl_tables/gen_eacl_deny_all_OTHERS') + user_wallet = wallets.get_wallet() + other_wallet = wallets.get_wallet(role=EACLRole.OTHERS) + cid = read_only_container - with allure.step('Create public container and check access'): - cid_public = create_container(self.main_wallet, basic_acl='eacl-public-read-write') - oid = self.check_full_access(cid_public, file_name) + with allure.step('Add test objects to container'): + object_oid = put_object(user_wallet.wallet_path, file_path, cid) - with allure.step('Set "deny all operations for other" for created container'): - set_eacl(self.main_wallet, cid_public, deny_all_eacl) + with allure.step('Check other has read-only access to operations with container'): + check_read_only_container(other_wallet.wallet_path, cid, object_oid, file_path) - with allure.step('Check no one except owner has access to operations with container'): - self.check_no_access_to_container(self.other_wallet, cid_public, oid, file_name) - - with allure.step('Check owner has access to operations with container'): - self.check_full_access(cid_public, file_name, wallet_to_check=((self.main_wallet, 'owner'),)) - - delete_object(self.main_wallet, cid_public, oid) - - @staticmethod - def check_no_access_to_container(wallet: str, cid: str, oid: str, file_name: str): - with pytest.raises(Exception, match=OBJECT_ACCESS_DENIED): - get_object(wallet, cid, oid) - - with pytest.raises(Exception, match=OBJECT_ACCESS_DENIED): - put_object(wallet, file_name, cid) - - with pytest.raises(Exception, match=OBJECT_ACCESS_DENIED): - delete_object(wallet, cid, oid) - - with pytest.raises(Exception, match=OBJECT_ACCESS_DENIED): - head_object(wallet, cid, oid) - - with pytest.raises(Exception, match=OBJECT_ACCESS_DENIED): - get_range(wallet, cid, oid, bearer='', range_cut='0:10') - - with pytest.raises(Exception, match=OBJECT_ACCESS_DENIED): - get_range_hash(wallet, cid, oid, bearer_token='', range_cut='0:10') - - with pytest.raises(Exception, match=OBJECT_ACCESS_DENIED): - search_object(wallet, cid) - - def check_full_access(self, cid: str, file_name: str, wallet_to_check: Tuple = None) -> str: - wallets = wallet_to_check or ((self.main_wallet, 'owner'), (self.other_wallet, 'not owner')) - for current_wallet, desc in wallets: - with allure.step(f'Check {desc} can put object into public container'): - oid = put_object(current_wallet, file_name, cid) - - with allure.step(f'Check {desc} can execute operations on object from public container'): - got_file = get_object(current_wallet, cid, oid) - assert get_file_hash(got_file) == get_file_hash(file_name), 'Expected hashes are the same' - - head_object(current_wallet, cid, oid) - get_range(current_wallet, cid, oid, bearer='', range_cut='0:10') - get_range_hash(current_wallet, cid, oid, bearer_token='', range_cut='0:10') - search_object(current_wallet, cid) - - return oid + with allure.step('Check owner has full access to public container'): + check_full_access_to_container(user_wallet.wallet_path, cid, object_oid, file_path) diff --git a/pytest_tests/testsuites/acl/test_bearer.py b/pytest_tests/testsuites/acl/test_bearer.py new file mode 100644 index 0000000..1e83288 --- /dev/null +++ b/pytest_tests/testsuites/acl/test_bearer.py @@ -0,0 +1,108 @@ +import allure +import pytest + +from python_keywords.acl import (EACLAccess, EACLOperation, EACLRole, EACLRule, create_eacl, form_bearertoken_file, + set_eacl, wait_for_cache_expired) +from python_keywords.container_access import (check_custom_access_to_container, check_full_access_to_container, + check_no_access_to_container) + + +@pytest.mark.sanity +@pytest.mark.acl +@pytest.mark.acl_bearer +class TestACLBearer: + @pytest.mark.parametrize('role', [EACLRole.USER, EACLRole.OTHERS]) + def test_bearer_token_operations(self, wallets, eacl_container_with_objects, role): + allure.dynamic.title(f"Testcase to validate NeoFS operations with {role.value} BearerToken") + cid, objects_oids, file_path = eacl_container_with_objects + user_wallet = wallets.get_wallet() + deny_wallet = wallets.get_wallet(role) + + with allure.step(f'Check {role.value} has full access to container without bearer token'): + check_full_access_to_container(deny_wallet.wallet_path, cid, objects_oids.pop(), file_path, + wallet_config=deny_wallet.config_path) + + with allure.step(f'Set deny all operations for {role.value} via eACL'): + eacl = [EACLRule(access=EACLAccess.DENY, role=role, operation=op) for op in EACLOperation] + eacl_file = create_eacl(cid, eacl) + set_eacl(user_wallet.wallet_path, cid, eacl_file) + wait_for_cache_expired() + + with allure.step(f'Create bearer token for {role.value} with all operations allowed'): + bearer_token = form_bearertoken_file(user_wallet.wallet_path, cid, [ + EACLRule(operation=op, access=EACLAccess.ALLOW, role=role) + for op in EACLOperation]) + + with allure.step(f'Check {role.value} without token has no access to all operations with container'): + check_no_access_to_container( + deny_wallet.wallet_path, cid, objects_oids.pop(), file_path, + wallet_config=deny_wallet.config_path) + + with allure.step(f'Check {role.value} with token has access to all operations with container'): + check_full_access_to_container(deny_wallet.wallet_path, cid, objects_oids.pop(), file_path, + bearer=bearer_token, wallet_config=deny_wallet.config_path) + + with allure.step(f'Set allow all operations for {role.value} via eACL'): + eacl = [EACLRule(access=EACLAccess.ALLOW, role=role, operation=op) for op in EACLOperation] + eacl_file = create_eacl(cid, eacl) + set_eacl(user_wallet.wallet_path, cid, eacl_file) + wait_for_cache_expired() + + with allure.step(f'Check {role.value} without token has access to all operations with container'): + check_full_access_to_container(deny_wallet.wallet_path, cid, objects_oids.pop(), file_path, + wallet_config=deny_wallet.config_path) + + @allure.title('BearerToken Operations for compound Operations') + def test_bearer_token_compound_operations(self, wallets, eacl_container_with_objects): + cid, objects_oids, file_path = eacl_container_with_objects + user_wallet = wallets.get_wallet() + other_wallet = wallets.get_wallet(role=EACLRole.OTHERS) + + # Operations that we will deny for each role via eACL + deny_map = { + EACLRole.USER: [EACLOperation.DELETE], + EACLRole.OTHERS: [EACLOperation.GET, EACLOperation.PUT, EACLOperation.GET_RANGE] + } + + # Operations that we will allow for each role with bearer token + bearer_map = { + EACLRole.USER: [EACLOperation.DELETE, EACLOperation.PUT, EACLOperation.GET_RANGE], + EACLRole.OTHERS: [EACLOperation.GET, EACLOperation.GET_RANGE], + } + + deny_map_with_bearer = { + EACLRole.USER: [op for op in deny_map[EACLRole.USER] if op not in bearer_map[EACLRole.USER]], + EACLRole.OTHERS: [op for op in deny_map[EACLRole.OTHERS] if op not in bearer_map[EACLRole.OTHERS]], + } + + eacl_deny = [] + for role, operations in deny_map.items(): + eacl_deny += [EACLRule(access=EACLAccess.DENY, role=role, operation=op) for op in operations] + set_eacl(user_wallet.wallet_path, cid, eacl_table_path=create_eacl(cid, eacl_deny)) + wait_for_cache_expired() + + with allure.step('Check rule consistency without bearer'): + check_custom_access_to_container(user_wallet.wallet_path, cid, objects_oids.pop(), file_path, + deny_operations=deny_map[EACLRole.USER], + wallet_config=user_wallet.config_path) + check_custom_access_to_container(other_wallet.wallet_path, cid, objects_oids.pop(), file_path, + deny_operations=deny_map[EACLRole.OTHERS], + wallet_config=other_wallet.config_path) + + with allure.step('Check rule consistency with bearer'): + bearer_token_user = form_bearertoken_file(user_wallet.wallet_path, cid, [ + EACLRule(operation=op, access=EACLAccess.ALLOW, role=EACLRole.USER) + for op in bearer_map[EACLRole.USER]]) + + bearer_token_other = form_bearertoken_file(user_wallet.wallet_path, cid, [ + EACLRule(operation=op, access=EACLAccess.ALLOW, role=EACLRole.OTHERS) + for op in bearer_map[EACLRole.OTHERS]]) + + check_custom_access_to_container(user_wallet.wallet_path, cid, objects_oids.pop(), file_path, + deny_operations=deny_map_with_bearer[EACLRole.USER], + bearer=bearer_token_user, + wallet_config=user_wallet.config_path) + check_custom_access_to_container(other_wallet.wallet_path, cid, objects_oids.pop(), file_path, + deny_operations=deny_map_with_bearer[EACLRole.OTHERS], + bearer=bearer_token_other, + wallet_config=other_wallet.config_path) diff --git a/pytest_tests/testsuites/acl/test_eacl.py b/pytest_tests/testsuites/acl/test_eacl.py new file mode 100644 index 0000000..88bf167 --- /dev/null +++ b/pytest_tests/testsuites/acl/test_eacl.py @@ -0,0 +1,105 @@ +import allure +import pytest + +from common import NEOFS_NETMAP_DICT +from failover_utils import wait_object_replication_on_nodes +from python_keywords.acl import (EACLAccess, EACLOperation, EACLRole, EACLRule, create_eacl, set_eacl, + wait_for_cache_expired) +from python_keywords.container import create_container +from python_keywords.container_access import check_full_access_to_container, check_no_access_to_container +from python_keywords.neofs_verbs import put_object +from python_keywords.node_management import drop_object +from wellknown_acl import PUBLIC_ACL + + +@pytest.mark.sanity +@pytest.mark.acl +@pytest.mark.acl_container +class TestEACLContainer: + NODE_COUNT = len(NEOFS_NETMAP_DICT.keys()) + + @pytest.fixture(scope='function') + def eacl_full_placement_container_with_object(self, wallets, file_path): + user_wallet = wallets.get_wallet() + with allure.step('Create eACL public container with full placement rule'): + full_placement_rule = f'REP {self.NODE_COUNT} IN X CBF 1 SELECT {self.NODE_COUNT} FROM * AS X' + cid = create_container(user_wallet.wallet_path, full_placement_rule, basic_acl=PUBLIC_ACL) + + with allure.step('Add test object to container'): + oid = put_object(user_wallet.wallet_path, file_path, cid) + wait_object_replication_on_nodes(user_wallet.wallet_path, cid, oid, self.NODE_COUNT) + + yield cid, oid, file_path + + @pytest.mark.parametrize('deny_role', [EACLRole.USER, EACLRole.OTHERS]) + def test_extended_acl_deny_all_operations(self, wallets, eacl_container_with_objects, deny_role): + user_wallet = wallets.get_wallet() + other_wallet = wallets.get_wallet(EACLRole.OTHERS) + deny_role_wallet = other_wallet if deny_role == EACLRole.OTHERS else user_wallet + not_deny_role_wallet = user_wallet if deny_role == EACLRole.OTHERS else other_wallet + deny_role_str = 'all others' if deny_role == EACLRole.OTHERS else 'user' + not_deny_role_str = 'user' if deny_role == EACLRole.OTHERS else 'all others' + allure.dynamic.title(f'Testcase to deny NeoFS operations for {deny_role_str}.') + cid, object_oids, file_path = eacl_container_with_objects + + with allure.step(f'Deny all operations for {deny_role_str} via eACL'): + eacl_deny = [EACLRule(access=EACLAccess.DENY, role=deny_role, operation=op) for op in EACLOperation] + set_eacl(user_wallet.wallet_path, cid, create_eacl(cid, eacl_deny)) + wait_for_cache_expired() + + with allure.step(f'Check only {not_deny_role_str} has full access to container'): + with allure.step(f'Check {deny_role_str} has not access to any operations with container'): + check_no_access_to_container(deny_role_wallet.wallet_path, cid, object_oids[0], file_path) + + with allure.step(f'Check {not_deny_role_wallet} has full access to eACL public container'): + check_full_access_to_container(not_deny_role_wallet.wallet_path, cid, object_oids.pop(), file_path) + + with allure.step(f'Allow all operations for {deny_role_str} via eACL'): + eacl_deny = [EACLRule(access=EACLAccess.ALLOW, role=deny_role, operation=op) for op in EACLOperation] + set_eacl(user_wallet.wallet_path, cid, create_eacl(cid, eacl_deny)) + wait_for_cache_expired() + + with allure.step(f'Check all have full access to eACL public container'): + check_full_access_to_container(user_wallet.wallet_path, cid, object_oids.pop(), file_path) + check_full_access_to_container(other_wallet.wallet_path, cid, object_oids.pop(), file_path) + + @allure.title('Testcase to allow NeoFS operations for only one other pubkey.') + def test_extended_acl_deny_all_operations_exclude_pubkey(self, wallets, eacl_container_with_objects): + user_wallet = wallets.get_wallet() + other_wallet, other_wallet_allow = wallets.get_wallets_list(EACLRole.OTHERS)[0:2] + cid, object_oids, file_path = eacl_container_with_objects + + with allure.step('Deny all operations for others except single wallet via eACL'): + eacl = [EACLRule(access=EACLAccess.ALLOW, role=other_wallet_allow.wallet_path, operation=op) + for op in EACLOperation] + eacl += [EACLRule(access=EACLAccess.DENY, role=EACLRole.OTHERS, operation=op) for op in EACLOperation] + set_eacl(user_wallet.wallet_path, cid, create_eacl(cid, eacl)) + wait_for_cache_expired() + + with allure.step('Check only owner and allowed other have full access to public container'): + with allure.step('Check other has not access to operations with container'): + check_no_access_to_container(other_wallet.wallet_path, cid, object_oids[0], file_path) + + with allure.step('Check owner has full access to public container'): + check_full_access_to_container(user_wallet.wallet_path, cid, object_oids.pop(), file_path) + + with allure.step('Check allowed other has full access to public container'): + check_full_access_to_container(other_wallet_allow.wallet_path, cid, object_oids.pop(), file_path) + + @allure.title('Testcase to validate NeoFS replication with eACL deny rules.') + def test_extended_acl_deny_replication(self, wallets, eacl_full_placement_container_with_object, file_path): + user_wallet = wallets.get_wallet() + cid, oid, file_path = eacl_full_placement_container_with_object + + with allure.step('Deny all operations for user via eACL'): + eacl_deny = [EACLRule(access=EACLAccess.DENY, role=EACLRole.USER, operation=op) for op in EACLOperation] + eacl_deny += [EACLRule(access=EACLAccess.DENY, role=EACLRole.OTHERS, operation=op) for op in EACLOperation] + set_eacl(user_wallet.wallet_path, cid, create_eacl(cid, eacl_deny)) + wait_for_cache_expired() + + with allure.step('Drop object to check replication'): + drop_object(node_name=[*NEOFS_NETMAP_DICT][0], cid=cid, oid=oid) + + storage_wallet_path = NEOFS_NETMAP_DICT[[*NEOFS_NETMAP_DICT][0]]["wallet_path"] + with allure.step('Wait for dropped object replicated'): + wait_object_replication_on_nodes(storage_wallet_path, cid, oid, self.NODE_COUNT) diff --git a/pytest_tests/testsuites/acl/test_eacl_filters.py b/pytest_tests/testsuites/acl/test_eacl_filters.py new file mode 100644 index 0000000..a07945a --- /dev/null +++ b/pytest_tests/testsuites/acl/test_eacl_filters.py @@ -0,0 +1,197 @@ +import allure +import pytest + +from python_keywords.acl import (EACLAccess, EACLFilter, EACLFilters, EACLHeaderType, EACLMatchType, EACLOperation, + EACLRole, EACLRule, create_eacl, set_eacl, wait_for_cache_expired) +from python_keywords.container import create_container, delete_container +from python_keywords.container_access import check_full_access_to_container, check_no_access_to_container +from python_keywords.neofs_verbs import put_object +from python_keywords.object_access import can_get_object, can_get_head_object, can_put_object +from wellknown_acl import PUBLIC_ACL + + +@pytest.mark.sanity +@pytest.mark.acl +@pytest.mark.acl_container +class TestEACLFilters: + # SPEC: https://github.com/nspcc-dev/neofs-spec/blob/master/01-arch/07-acl.md + ATTRIBUTE = {'check_key': 'check_value'} + OTHER_ATTRIBUTE = {'check_key': 'other_value'} + SET_HEADERS = {'key_one': 'check_value', 'x_key': 'xvalue', 'check_key': 'check_value'} + OTHER_HEADERS = {'key_one': 'check_value', 'x_key': 'other_value', 'check_key': 'other_value'} + REQ_EQUAL_FILTER = EACLFilter(key='check_key', value='check_value', header_type=EACLHeaderType.REQUEST) + NOT_REQ_EQUAL_FILTER = EACLFilter(key='check_key', value='other_value', match_type=EACLMatchType.STRING_NOT_EQUAL, + header_type=EACLHeaderType.REQUEST) + OBJ_EQUAL_FILTER = EACLFilter(key='check_key', value='check_value', header_type=EACLHeaderType.OBJECT) + NOT_OBJ_EQUAL_FILTER = EACLFilter(key='check_key', value='other_value', match_type=EACLMatchType.STRING_NOT_EQUAL, + header_type=EACLHeaderType.OBJECT) + OBJECT_COUNT = 3 + OBJECT_ATTRIBUTES_FILTER_SUPPORTED_OPERATIONS = [EACLOperation.GET, EACLOperation.HEAD, EACLOperation.PUT] + + @pytest.fixture(scope='function') + def eacl_container_with_objects(self, wallets, file_path): + user_wallet = wallets.get_wallet() + with allure.step('Create eACL public container'): + cid = create_container(user_wallet.wallet_path, basic_acl=PUBLIC_ACL) + + with allure.step('Add test objects to container'): + objects_with_header = [ + put_object(user_wallet.wallet_path, file_path, cid, + attributes={**self.SET_HEADERS, 'key': val}) for val in range(self.OBJECT_COUNT)] + + objects_with_other_header = [ + put_object(user_wallet.wallet_path, file_path, cid, + attributes={**self.OTHER_HEADERS, 'key': val}) for val in range(self.OBJECT_COUNT)] + + objects_without_header = [ + put_object(user_wallet.wallet_path, file_path, cid) for _ in range(self.OBJECT_COUNT)] + + yield cid, objects_with_header, objects_with_other_header, objects_without_header, file_path + + with allure.step('Delete eACL public container'): + delete_container(user_wallet.wallet_path, cid) + + @pytest.mark.parametrize('match_type', [EACLMatchType.STRING_EQUAL, EACLMatchType.STRING_NOT_EQUAL]) + def test_extended_acl_filters_request(self, wallets, eacl_container_with_objects, match_type): + allure.dynamic.title(f"Validate NeoFS operations with request filter: {match_type.name}") + user_wallet = wallets.get_wallet() + other_wallet = wallets.get_wallet(EACLRole.OTHERS) + cid, objects_with_header, objects_with_other_header, objects_without_header, file_path =\ + eacl_container_with_objects + + with allure.step('Deny all operations for other with eACL request filter'): + equal_filter = EACLFilter(**self.REQ_EQUAL_FILTER.__dict__) + equal_filter.match_type = match_type + eacl_deny = [EACLRule(access=EACLAccess.DENY, + role=EACLRole.OTHERS, + filters=EACLFilters([equal_filter]), + operation=op) for op in EACLOperation] + set_eacl(user_wallet.wallet_path, cid, create_eacl(cid, eacl_deny)) + wait_for_cache_expired() + + # Filter denies requests where "check_key {match_type} ATTRIBUTE", so when match_type + # is STRING_EQUAL, then requests with "check_key=OTHER_ATTRIBUTE" will be allowed while + # requests with "check_key=ATTRIBUTE" will be denied, and vice versa + allow_headers = self.OTHER_ATTRIBUTE if match_type == EACLMatchType.STRING_EQUAL else self.ATTRIBUTE + deny_headers = self.ATTRIBUTE if match_type == EACLMatchType.STRING_EQUAL else self.OTHER_ATTRIBUTE + # We test on 3 groups of objects with various headers, but eACL rule should ignore object headers and + # work only based on request headers + for oid in (objects_with_header, objects_with_other_header, objects_without_header): + with allure.step('Check other has full access when sending request without headers'): + check_full_access_to_container(other_wallet.wallet_path, cid, oid.pop(), file_path) + + with allure.step('Check other has full access when sending request with allowed headers'): + check_full_access_to_container(other_wallet.wallet_path, cid, oid.pop(), file_path, xhdr=allow_headers) + + with allure.step('Check other has no access when sending request with denied headers'): + check_no_access_to_container(other_wallet.wallet_path, cid, oid.pop(), file_path, xhdr=deny_headers) + + @pytest.mark.parametrize('match_type', [EACLMatchType.STRING_EQUAL, EACLMatchType.STRING_NOT_EQUAL]) + def test_extended_acl_deny_filters_object(self, wallets, eacl_container_with_objects, match_type): + allure.dynamic.title(f"Validate NeoFS operations with deny user headers filter: {match_type.name}") + user_wallet = wallets.get_wallet() + other_wallet = wallets.get_wallet(EACLRole.OTHERS) + cid, objects_with_header, objects_with_other_header, objs_without_header, file_path = \ + eacl_container_with_objects + + with allure.step('Deny all operations for other with object filter'): + equal_filter = EACLFilter(**self.OBJ_EQUAL_FILTER.__dict__) + equal_filter.match_type = match_type + eacl_deny = [EACLRule(access=EACLAccess.DENY, + role=EACLRole.OTHERS, + filters=EACLFilters([equal_filter]), + operation=op) for op in self.OBJECT_ATTRIBUTES_FILTER_SUPPORTED_OPERATIONS] + set_eacl(user_wallet.wallet_path, cid, create_eacl(cid, eacl_deny)) + wait_for_cache_expired() + + allow_objects = objects_with_other_header if match_type == EACLMatchType.STRING_EQUAL else objects_with_header + deny_objects = objects_with_header if match_type == EACLMatchType.STRING_EQUAL else objects_with_other_header + + # We will attempt requests with various headers, but eACL rule should ignore request headers and validate + # only object headers + for xhdr in (self.ATTRIBUTE, self.OTHER_ATTRIBUTE, None): + with allure.step(f'Check other have full access to objects without attributes'): + check_full_access_to_container( + other_wallet.wallet_path, cid, objs_without_header.pop(), file_path, xhdr=xhdr) + + with allure.step(f'Check other have full access to objects without deny attribute'): + check_full_access_to_container( + other_wallet.wallet_path, cid, allow_objects.pop(), file_path, xhdr=xhdr) + + with allure.step(f'Check other have no access to objects with deny attribute'): + oid = deny_objects.pop() + with pytest.raises(AssertionError): + assert can_get_head_object(other_wallet.wallet_path, cid, oid, xhdr=xhdr) + with pytest.raises(AssertionError): + assert can_get_object(other_wallet.wallet_path, cid, oid, file_path, xhdr=xhdr) + + allow_attribute = self.OTHER_ATTRIBUTE if match_type == EACLMatchType.STRING_EQUAL else self.ATTRIBUTE + with allure.step('Check other can PUT objects without denied attribute'): + assert can_put_object(other_wallet.wallet_path, cid, file_path, attributes=allow_attribute) + assert can_put_object(other_wallet.wallet_path, cid, file_path) + + deny_attribute = self.ATTRIBUTE if match_type == EACLMatchType.STRING_EQUAL else self.OTHER_ATTRIBUTE + with allure.step('Check other can not PUT objects with denied attribute'): + with pytest.raises(AssertionError): + assert can_put_object(other_wallet.wallet_path, cid, file_path, attributes=deny_attribute) + + @pytest.mark.parametrize('match_type', [EACLMatchType.STRING_EQUAL, EACLMatchType.STRING_NOT_EQUAL]) + def test_extended_acl_allow_filters_object(self, wallets, eacl_container_with_objects, match_type): + allure.dynamic.title( + f"Testcase to validate NeoFS operation with allow eACL user headers filters: {match_type.name}") + user_wallet = wallets.get_wallet() + other_wallet = wallets.get_wallet(EACLRole.OTHERS) + cid, objects_with_header, objects_with_other_header, objects_without_header, file_path = \ + eacl_container_with_objects + + with allure.step('Deny all operations for others except few operations allowed by object filter'): + equal_filter = EACLFilter(**self.OBJ_EQUAL_FILTER.__dict__) + equal_filter.match_type = match_type + eacl = [ + EACLRule(access=EACLAccess.ALLOW, + role=EACLRole.OTHERS, + filters=EACLFilters([equal_filter]), + operation=op) for op in self.OBJECT_ATTRIBUTES_FILTER_SUPPORTED_OPERATIONS + ] + [ + EACLRule(access=EACLAccess.DENY, + role=EACLRole.OTHERS, + operation=op) for op in self.OBJECT_ATTRIBUTES_FILTER_SUPPORTED_OPERATIONS + + ] + set_eacl(user_wallet.wallet_path, cid, create_eacl(cid, eacl)) + wait_for_cache_expired() + + if match_type == EACLMatchType.STRING_EQUAL: + allow_objects = objects_with_header + deny_objects = objects_with_other_header + allow_attribute = self.ATTRIBUTE + deny_attribute = self.OTHER_ATTRIBUTE + else: + allow_objects = objects_with_other_header + deny_objects = objects_with_header + allow_attribute = self.OTHER_ATTRIBUTE + deny_attribute = self.ATTRIBUTE + + with allure.step(f'Check other cannot get and put objects without attributes'): + oid = objects_without_header.pop() + with pytest.raises(AssertionError): + assert can_get_head_object(other_wallet.wallet_path, cid, oid) + with pytest.raises(AssertionError): + assert can_get_object(other_wallet.wallet_path, cid, oid, file_path) + with pytest.raises(AssertionError): + assert can_put_object(other_wallet.wallet_path, cid, file_path) + + with allure.step(f'Check other can get objects with attributes matching the filter'): + oid = allow_objects.pop() + assert can_get_head_object(other_wallet.wallet_path, cid, oid) + assert can_get_object(other_wallet.wallet_path, cid, oid, file_path) + assert can_put_object(other_wallet.wallet_path, cid, file_path, attributes=allow_attribute) + + with allure.step(f'Check other cannot get objects without attributes matching the filter'): + oid = deny_objects.pop() + with pytest.raises(AssertionError): + assert can_get_head_object(other_wallet.wallet_path, cid, oid) + with pytest.raises(AssertionError): + assert can_get_object(other_wallet.wallet_path, cid, oid, file_path) + with pytest.raises(AssertionError): + assert can_put_object(other_wallet.wallet_path, cid, file_path, attributes=deny_attribute) diff --git a/pytest_tests/testsuites/container/test_container.py b/pytest_tests/testsuites/container/test_container.py index 484b363..73d5796 100644 --- a/pytest_tests/testsuites/container/test_container.py +++ b/pytest_tests/testsuites/container/test_container.py @@ -3,7 +3,6 @@ import json import allure import pytest from epoch import tick_epoch -from grpc_responses import CONTAINER_NOT_FOUND, error_matches_status from python_keywords.container import (create_container, delete_container, get_container, list_containers, wait_for_container_creation, wait_for_container_deletion) from utility import placement_policy_from_container @@ -28,7 +27,7 @@ def test_container_creation(prepare_wallet_and_deposit, name): assert cid in containers, f'Expected container {cid} in containers: {containers}' container_info: str = get_container(wallet, cid, json_mode=False) - container_info = container_info.casefold() # To ignore case when comparing with expected values + container_info = container_info.casefold() # To ignore case when comparing with expected values info_to_check = { f'basic ACL: {PRIVATE_ACL_F} (private)', diff --git a/pytest_tests/testsuites/object/test_object_api.py b/pytest_tests/testsuites/object/test_object_api.py index 407b83d..bbd512b 100644 --- a/pytest_tests/testsuites/object/test_object_api.py +++ b/pytest_tests/testsuites/object/test_object_api.py @@ -45,8 +45,8 @@ def test_object_api(prepare_wallet_and_deposit, request, object_size): with allure.step('Put objects'): oids.append(put_object(wallet=wallet, path=file_path, cid=cid)) - oids.append(put_object(wallet=wallet, path=file_path, cid=cid, user_headers=file_usr_header)) - oids.append(put_object(wallet=wallet, path=file_path, cid=cid, user_headers=file_usr_header_oth)) + oids.append(put_object(wallet=wallet, path=file_path, cid=cid, attributes=file_usr_header)) + oids.append(put_object(wallet=wallet, path=file_path, cid=cid, attributes=file_usr_header_oth)) with allure.step('Validate storage policy for objects'): for oid_to_check in oids: diff --git a/robot/resources/files/eacl_tables/gen_eacl_allow_all_OTHERS b/robot/resources/files/eacl_tables/gen_eacl_allow_all_OTHERS deleted file mode 100644 index 871fd98..0000000 --- a/robot/resources/files/eacl_tables/gen_eacl_allow_all_OTHERS +++ /dev/null @@ -1,74 +0,0 @@ -{ - "records": [ - { - "operation": "GET", - "action": "ALLOW", - "filters": [], - "targets": [ - { - "role": "OTHERS" - } - ] - }, - { - "operation": "HEAD", - "action": "ALLOW", - "filters": [], - "targets": [ - { - "role": "OTHERS" - } - ] - }, - { - "operation": "PUT", - "action": "ALLOW", - "filters": [], - "targets": [ - { - "role": "OTHERS" - } - ] - }, - { - "operation": "DELETE", - "action": "ALLOW", - "filters": [], - "targets": [ - { - "role": "OTHERS" - } - ] - }, - { - "operation": "SEARCH", - "action": "ALLOW", - "filters": [], - "targets": [ - { - "role": "OTHERS" - } - ] - }, - { - "operation": "GETRANGE", - "action": "ALLOW", - "filters": [], - "targets": [ - { - "role": "OTHERS" - } - ] - }, - { - "operation": "GETRANGEHASH", - "action": "ALLOW", - "filters": [], - "targets": [ - { - "role": "OTHERS" - } - ] - } - ] -} \ No newline at end of file diff --git a/robot/resources/files/eacl_tables/gen_eacl_allow_all_SYSTEM b/robot/resources/files/eacl_tables/gen_eacl_allow_all_SYSTEM deleted file mode 100644 index 1a34150..0000000 --- a/robot/resources/files/eacl_tables/gen_eacl_allow_all_SYSTEM +++ /dev/null @@ -1,74 +0,0 @@ -{ - "records": [ - { - "operation": "GET", - "action": "ALLOW", - "filters": [], - "targets": [ - { - "role": "SYSTEM" - } - ] - }, - { - "operation": "HEAD", - "action": "ALLOW", - "filters": [], - "targets": [ - { - "role": "SYSTEM" - } - ] - }, - { - "operation": "PUT", - "action": "ALLOW", - "filters": [], - "targets": [ - { - "role": "SYSTEM" - } - ] - }, - { - "operation": "DELETE", - "action": "ALLOW", - "filters": [], - "targets": [ - { - "role": "SYSTEM" - } - ] - }, - { - "operation": "SEARCH", - "action": "ALLOW", - "filters": [], - "targets": [ - { - "role": "SYSTEM" - } - ] - }, - { - "operation": "GETRANGE", - "action": "ALLOW", - "filters": [], - "targets": [ - { - "role": "SYSTEM" - } - ] - }, - { - "operation": "GETRANGEHASH", - "action": "ALLOW", - "filters": [], - "targets": [ - { - "role": "SYSTEM" - } - ] - } - ] -} \ No newline at end of file diff --git a/robot/resources/files/eacl_tables/gen_eacl_allow_all_USER b/robot/resources/files/eacl_tables/gen_eacl_allow_all_USER deleted file mode 100644 index eff32d3..0000000 --- a/robot/resources/files/eacl_tables/gen_eacl_allow_all_USER +++ /dev/null @@ -1,74 +0,0 @@ -{ - "records": [ - { - "operation": "GET", - "action": "ALLOW", - "filters": [], - "targets": [ - { - "role": "USER" - } - ] - }, - { - "operation": "HEAD", - "action": "ALLOW", - "filters": [], - "targets": [ - { - "role": "USER" - } - ] - }, - { - "operation": "PUT", - "action": "ALLOW", - "filters": [], - "targets": [ - { - "role": "USER" - } - ] - }, - { - "operation": "DELETE", - "action": "ALLOW", - "filters": [], - "targets": [ - { - "role": "USER" - } - ] - }, - { - "operation": "SEARCH", - "action": "ALLOW", - "filters": [], - "targets": [ - { - "role": "USER" - } - ] - }, - { - "operation": "GETRANGE", - "action": "ALLOW", - "filters": [], - "targets": [ - { - "role": "USER" - } - ] - }, - { - "operation": "GETRANGEHASH", - "action": "ALLOW", - "filters": [], - "targets": [ - { - "role": "USER" - } - ] - } - ] -} \ No newline at end of file diff --git a/robot/resources/files/eacl_tables/gen_eacl_compound_del_OTHERS b/robot/resources/files/eacl_tables/gen_eacl_compound_del_OTHERS deleted file mode 100644 index ef797b2..0000000 --- a/robot/resources/files/eacl_tables/gen_eacl_compound_del_OTHERS +++ /dev/null @@ -1,34 +0,0 @@ -{ - "records": [ - { - "operation": "DELETE", - "action": "ALLOW", - "filters": [], - "targets": [ - { - "role": "OTHERS" - } - ] - }, - { - "operation": "PUT", - "action": "DENY", - "filters": [], - "targets": [ - { - "role": "OTHERS" - } - ] - }, - { - "operation": "HEAD", - "action": "DENY", - "filters": [], - "targets": [ - { - "role": "OTHERS" - } - ] - } - ] -} \ No newline at end of file diff --git a/robot/resources/files/eacl_tables/gen_eacl_compound_del_SYSTEM b/robot/resources/files/eacl_tables/gen_eacl_compound_del_SYSTEM deleted file mode 100644 index a26fe34..0000000 --- a/robot/resources/files/eacl_tables/gen_eacl_compound_del_SYSTEM +++ /dev/null @@ -1,34 +0,0 @@ -{ - "records": [ - { - "operation": "DELETE", - "action": "ALLOW", - "filters": [], - "targets": [ - { - "role": "SYSTEM" - } - ] - }, - { - "operation": "PUT", - "action": "DENY", - "filters": [], - "targets": [ - { - "role": "SYSTEM" - } - ] - }, - { - "operation": "HEAD", - "action": "DENY", - "filters": [], - "targets": [ - { - "role": "SYSTEM" - } - ] - } - ] -} \ No newline at end of file diff --git a/robot/resources/files/eacl_tables/gen_eacl_compound_del_USER b/robot/resources/files/eacl_tables/gen_eacl_compound_del_USER deleted file mode 100644 index abf2881..0000000 --- a/robot/resources/files/eacl_tables/gen_eacl_compound_del_USER +++ /dev/null @@ -1,34 +0,0 @@ -{ - "records": [ - { - "operation": "DELETE", - "action": "ALLOW", - "filters": [], - "targets": [ - { - "role": "USER" - } - ] - }, - { - "operation": "PUT", - "action": "DENY", - "filters": [], - "targets": [ - { - "role": "USER" - } - ] - }, - { - "operation": "HEAD", - "action": "DENY", - "filters": [], - "targets": [ - { - "role": "USER" - } - ] - } - ] -} \ No newline at end of file diff --git a/robot/resources/files/eacl_tables/gen_eacl_compound_get_OTHERS b/robot/resources/files/eacl_tables/gen_eacl_compound_get_OTHERS deleted file mode 100644 index 64f292a..0000000 --- a/robot/resources/files/eacl_tables/gen_eacl_compound_get_OTHERS +++ /dev/null @@ -1,44 +0,0 @@ -{ - "records": [ - { - "operation": "GET", - "action": "ALLOW", - "filters": [], - "targets": [ - { - "role": "OTHERS" - } - ] - }, - { - "operation": "GETRANGE", - "action": "ALLOW", - "filters": [], - "targets": [ - { - "role": "OTHERS" - } - ] - }, - { - "operation": "GETRANGEHASH", - "action": "ALLOW", - "filters": [], - "targets": [ - { - "role": "OTHERS" - } - ] - }, - { - "operation": "HEAD", - "action": "DENY", - "filters": [], - "targets": [ - { - "role": "OTHERS" - } - ] - } - ] -} \ No newline at end of file diff --git a/robot/resources/files/eacl_tables/gen_eacl_compound_get_SYSTEM b/robot/resources/files/eacl_tables/gen_eacl_compound_get_SYSTEM deleted file mode 100644 index 23cdbdc..0000000 --- a/robot/resources/files/eacl_tables/gen_eacl_compound_get_SYSTEM +++ /dev/null @@ -1,44 +0,0 @@ -{ - "records": [ - { - "operation": "GET", - "action": "ALLOW", - "filters": [], - "targets": [ - { - "role": "SYSTEM" - } - ] - }, - { - "operation": "GETRANGE", - "action": "ALLOW", - "filters": [], - "targets": [ - { - "role": "SYSTEM" - } - ] - }, - { - "operation": "GETRANGEHASH", - "action": "ALLOW", - "filters": [], - "targets": [ - { - "role": "SYSTEM" - } - ] - }, - { - "operation": "HEAD", - "action": "DENY", - "filters": [], - "targets": [ - { - "role": "SYSTEM" - } - ] - } - ] -} \ No newline at end of file diff --git a/robot/resources/files/eacl_tables/gen_eacl_compound_get_USER b/robot/resources/files/eacl_tables/gen_eacl_compound_get_USER deleted file mode 100644 index a8015e2..0000000 --- a/robot/resources/files/eacl_tables/gen_eacl_compound_get_USER +++ /dev/null @@ -1,44 +0,0 @@ -{ - "records": [ - { - "operation": "GET", - "action": "ALLOW", - "filters": [], - "targets": [ - { - "role": "USER" - } - ] - }, - { - "operation": "GETRANGE", - "action": "ALLOW", - "filters": [], - "targets": [ - { - "role": "USER" - } - ] - }, - { - "operation": "GETRANGEHASH", - "action": "ALLOW", - "filters": [], - "targets": [ - { - "role": "USER" - } - ] - }, - { - "operation": "HEAD", - "action": "DENY", - "filters": [], - "targets": [ - { - "role": "USER" - } - ] - } - ] -} \ No newline at end of file diff --git a/robot/resources/files/eacl_tables/gen_eacl_compound_get_hash_OTHERS b/robot/resources/files/eacl_tables/gen_eacl_compound_get_hash_OTHERS deleted file mode 100644 index fc37206..0000000 --- a/robot/resources/files/eacl_tables/gen_eacl_compound_get_hash_OTHERS +++ /dev/null @@ -1,34 +0,0 @@ -{ - "records": [ - { - "operation": "GETRANGEHASH", - "action": "ALLOW", - "filters": [], - "targets": [ - { - "role": "OTHERS" - } - ] - }, - { - "operation": "GETRANGE", - "action": "DENY", - "filters": [], - "targets": [ - { - "role": "OTHERS" - } - ] - }, - { - "operation": "GET", - "action": "DENY", - "filters": [], - "targets": [ - { - "role": "OTHERS" - } - ] - } - ] -} \ No newline at end of file diff --git a/robot/resources/files/eacl_tables/gen_eacl_compound_get_hash_SYSTEM b/robot/resources/files/eacl_tables/gen_eacl_compound_get_hash_SYSTEM deleted file mode 100644 index 2f73c67..0000000 --- a/robot/resources/files/eacl_tables/gen_eacl_compound_get_hash_SYSTEM +++ /dev/null @@ -1,34 +0,0 @@ -{ - "records": [ - { - "operation": "GETRANGEHASH", - "action": "ALLOW", - "filters": [], - "targets": [ - { - "role": "SYSTEM" - } - ] - }, - { - "operation": "GETRANGE", - "action": "DENY", - "filters": [], - "targets": [ - { - "role": "SYSTEM" - } - ] - }, - { - "operation": "GET", - "action": "DENY", - "filters": [], - "targets": [ - { - "role": "SYSTEM" - } - ] - } - ] -} \ No newline at end of file diff --git a/robot/resources/files/eacl_tables/gen_eacl_compound_get_hash_USER b/robot/resources/files/eacl_tables/gen_eacl_compound_get_hash_USER deleted file mode 100644 index 8a47e9d..0000000 --- a/robot/resources/files/eacl_tables/gen_eacl_compound_get_hash_USER +++ /dev/null @@ -1,34 +0,0 @@ -{ - "records": [ - { - "operation": "GETRANGEHASH", - "action": "ALLOW", - "filters": [], - "targets": [ - { - "role": "USER" - } - ] - }, - { - "operation": "GETRANGE", - "action": "DENY", - "filters": [], - "targets": [ - { - "role": "USER" - } - ] - }, - { - "operation": "GET", - "action": "DENY", - "filters": [], - "targets": [ - { - "role": "USER" - } - ] - } - ] -} \ No newline at end of file diff --git a/robot/resources/files/eacl_tables/gen_eacl_deny_all_OTHERS b/robot/resources/files/eacl_tables/gen_eacl_deny_all_OTHERS deleted file mode 100644 index 8c1efd3..0000000 --- a/robot/resources/files/eacl_tables/gen_eacl_deny_all_OTHERS +++ /dev/null @@ -1,74 +0,0 @@ -{ - "records": [ - { - "operation": "GET", - "action": "DENY", - "filters": [], - "targets": [ - { - "role": "OTHERS" - } - ] - }, - { - "operation": "HEAD", - "action": "DENY", - "filters": [], - "targets": [ - { - "role": "OTHERS" - } - ] - }, - { - "operation": "PUT", - "action": "DENY", - "filters": [], - "targets": [ - { - "role": "OTHERS" - } - ] - }, - { - "operation": "DELETE", - "action": "DENY", - "filters": [], - "targets": [ - { - "role": "OTHERS" - } - ] - }, - { - "operation": "SEARCH", - "action": "DENY", - "filters": [], - "targets": [ - { - "role": "OTHERS" - } - ] - }, - { - "operation": "GETRANGE", - "action": "DENY", - "filters": [], - "targets": [ - { - "role": "OTHERS" - } - ] - }, - { - "operation": "GETRANGEHASH", - "action": "DENY", - "filters": [], - "targets": [ - { - "role": "OTHERS" - } - ] - } - ] -} \ No newline at end of file diff --git a/robot/resources/files/eacl_tables/gen_eacl_deny_all_SYSTEM b/robot/resources/files/eacl_tables/gen_eacl_deny_all_SYSTEM deleted file mode 100644 index b5e34a2..0000000 --- a/robot/resources/files/eacl_tables/gen_eacl_deny_all_SYSTEM +++ /dev/null @@ -1,74 +0,0 @@ -{ - "records": [ - { - "operation": "GET", - "action": "DENY", - "filters": [], - "targets": [ - { - "role": "SYSTEM" - } - ] - }, - { - "operation": "HEAD", - "action": "DENY", - "filters": [], - "targets": [ - { - "role": "SYSTEM" - } - ] - }, - { - "operation": "PUT", - "action": "DENY", - "filters": [], - "targets": [ - { - "role": "SYSTEM" - } - ] - }, - { - "operation": "DELETE", - "action": "DENY", - "filters": [], - "targets": [ - { - "role": "SYSTEM" - } - ] - }, - { - "operation": "SEARCH", - "action": "DENY", - "filters": [], - "targets": [ - { - "role": "SYSTEM" - } - ] - }, - { - "operation": "GETRANGE", - "action": "DENY", - "filters": [], - "targets": [ - { - "role": "SYSTEM" - } - ] - }, - { - "operation": "GETRANGEHASH", - "action": "DENY", - "filters": [], - "targets": [ - { - "role": "SYSTEM" - } - ] - } - ] -} \ No newline at end of file diff --git a/robot/resources/files/eacl_tables/gen_eacl_deny_all_USER b/robot/resources/files/eacl_tables/gen_eacl_deny_all_USER deleted file mode 100644 index 8688ea5..0000000 --- a/robot/resources/files/eacl_tables/gen_eacl_deny_all_USER +++ /dev/null @@ -1,74 +0,0 @@ -{ - "records": [ - { - "operation": "GET", - "action": "DENY", - "filters": [], - "targets": [ - { - "role": "USER" - } - ] - }, - { - "operation": "HEAD", - "action": "DENY", - "filters": [], - "targets": [ - { - "role": "USER" - } - ] - }, - { - "operation": "PUT", - "action": "DENY", - "filters": [], - "targets": [ - { - "role": "USER" - } - ] - }, - { - "operation": "DELETE", - "action": "DENY", - "filters": [], - "targets": [ - { - "role": "USER" - } - ] - }, - { - "operation": "SEARCH", - "action": "DENY", - "filters": [], - "targets": [ - { - "role": "USER" - } - ] - }, - { - "operation": "GETRANGE", - "action": "DENY", - "filters": [], - "targets": [ - { - "role": "USER" - } - ] - }, - { - "operation": "GETRANGEHASH", - "action": "DENY", - "filters": [], - "targets": [ - { - "role": "USER" - } - ] - } - ] -} \ No newline at end of file diff --git a/robot/resources/files/eacl_tables/gen_eacl_xheader_allow_all b/robot/resources/files/eacl_tables/gen_eacl_xheader_allow_all deleted file mode 100644 index 26d147b..0000000 --- a/robot/resources/files/eacl_tables/gen_eacl_xheader_allow_all +++ /dev/null @@ -1,193 +0,0 @@ -{ - "records": [ - { - "operation": "GET", - "action": "ALLOW", - "filters": [ - { - "headerType": "REQUEST", - "matchType": "STRING_EQUAL", - "key": "a", - "value": "2" - } - ], - "targets": [ - { - "role": "OTHERS" - } - ] - }, - { - "operation": "GET", - "action": "DENY", - "filters": [], - "targets": [ - { - "role": "OTHERS" - } - ] - }, - { - "operation": "HEAD", - "action": "ALLOW", - "filters": [ - { - "headerType": "REQUEST", - "matchType": "STRING_EQUAL", - "key": "a", - "value": "2" - } - ], - "targets": [ - { - "role": "OTHERS" - } - ] - }, - { - "operation": "HEAD", - "action": "DENY", - "filters": [], - "targets": [ - { - "role": "OTHERS" - } - ] - }, - { - "operation": "PUT", - "action": "ALLOW", - "filters": [ - { - "headerType": "REQUEST", - "matchType": "STRING_EQUAL", - "key": "a", - "value": "2" - } - ], - "targets": [ - { - "role": "OTHERS" - } - ] - }, - { - "operation": "PUT", - "action": "DENY", - "filters": [], - "targets": [ - { - "role": "OTHERS" - } - ] - }, - { - "operation": "DELETE", - "action": "ALLOW", - "filters": [ - { - "headerType": "REQUEST", - "matchType": "STRING_EQUAL", - "key": "a", - "value": "2" - } - ], - "targets": [ - { - "role": "OTHERS" - } - ] - }, - { - "operation": "DELETE", - "action": "DENY", - "filters": [], - "targets": [ - { - "role": "OTHERS" - } - ] - }, - { - "operation": "SEARCH", - "action": "ALLOW", - "filters": [ - { - "headerType": "REQUEST", - "matchType": "STRING_EQUAL", - "key": "a", - "value": "2" - } - ], - "targets": [ - { - "role": "OTHERS" - } - ] - }, - { - "operation": "SEARCH", - "action": "DENY", - "filters": [], - "targets": [ - { - "role": "OTHERS" - } - ] - }, - { - "operation": "GETRANGE", - "action": "ALLOW", - "filters": [ - { - "headerType": "REQUEST", - "matchType": "STRING_EQUAL", - "key": "a", - "value": "2" - } - ], - "targets": [ - { - "role": "OTHERS" - } - ] - }, - { - "operation": "GETRANGE", - "action": "DENY", - "filters": [], - "targets": [ - { - "role": "OTHERS" - } - ] - }, - { - "operation": "GETRANGEHASH", - "action": "ALLOW", - "filters": [ - { - "headerType": "REQUEST", - "matchType": "STRING_EQUAL", - "key": "a", - "value": "2" - } - ], - "targets": [ - { - "role": "OTHERS" - } - ] - }, - { - "operation": "GETRANGEHASH", - "action": "DENY", - "filters": [], - "targets": [ - { - "role": "OTHERS" - } - ] - } - ] -} \ No newline at end of file diff --git a/robot/resources/files/eacl_tables/gen_eacl_xheader_deny_all b/robot/resources/files/eacl_tables/gen_eacl_xheader_deny_all deleted file mode 100644 index 4b3a009..0000000 --- a/robot/resources/files/eacl_tables/gen_eacl_xheader_deny_all +++ /dev/null @@ -1,123 +0,0 @@ -{ - "records": [ - { - "operation": "GET", - "action": "DENY", - "filters": [ - { - "headerType": "REQUEST", - "matchType": "STRING_EQUAL", - "key": "a", - "value": "2" - } - ], - "targets": [ - { - "role": "OTHERS" - } - ] - }, - { - "operation": "HEAD", - "action": "DENY", - "filters": [ - { - "headerType": "REQUEST", - "matchType": "STRING_EQUAL", - "key": "a", - "value": "2" - } - ], - "targets": [ - { - "role": "OTHERS" - } - ] - }, - { - "operation": "PUT", - "action": "DENY", - "filters": [ - { - "headerType": "REQUEST", - "matchType": "STRING_EQUAL", - "key": "a", - "value": "2" - } - ], - "targets": [ - { - "role": "OTHERS" - } - ] - }, - { - "operation": "DELETE", - "action": "DENY", - "filters": [ - { - "headerType": "REQUEST", - "matchType": "STRING_EQUAL", - "key": "a", - "value": "2" - } - ], - "targets": [ - { - "role": "OTHERS" - } - ] - }, - { - "operation": "SEARCH", - "action": "DENY", - "filters": [ - { - "headerType": "REQUEST", - "matchType": "STRING_EQUAL", - "key": "a", - "value": "2" - } - ], - "targets": [ - { - "role": "OTHERS" - } - ] - }, - { - "operation": "GETRANGE", - "action": "DENY", - "filters": [ - { - "headerType": "REQUEST", - "matchType": "STRING_EQUAL", - "key": "a", - "value": "2" - } - ], - "targets": [ - { - "role": "OTHERS" - } - ] - }, - { - "operation": "GETRANGEHASH", - "action": "DENY", - "filters": [ - { - "headerType": "REQUEST", - "matchType": "STRING_EQUAL", - "key": "a", - "value": "2" - } - ], - "targets": [ - { - "role": "OTHERS" - } - ] - } - ] -} \ No newline at end of file diff --git a/robot/resources/lib/python_keywords/acl.py b/robot/resources/lib/python_keywords/acl.py index 2750b0e..3c6012e 100644 --- a/robot/resources/lib/python_keywords/acl.py +++ b/robot/resources/lib/python_keywords/acl.py @@ -1,63 +1,119 @@ -#!/usr/bin/python3.9 - import base64 import json +import logging import os -import re import uuid -from enum import Enum, auto -from typing import Optional +from dataclasses import dataclass +from enum import Enum +from time import sleep +from typing import Any, Dict, List, Optional, Union +import allure import base58 +from cli import NeofsCli from cli_helpers import _cmd_run from common import ASSETS_DIR, NEOFS_CLI_EXEC, NEOFS_ENDPOINT, WALLET_CONFIG from data_formatters import get_wallet_public_key -from robot.api import logger -from robot.api.deco import keyword -""" -Robot Keywords and helper functions for work with NeoFS ACL. -""" - -ROBOT_AUTO_KEYWORDS = False +logger = logging.getLogger('NeoLogger') EACL_LIFETIME = 100500 +NEOFS_CONTRACT_CACHE_TIMEOUT = 30 -class AutoName(Enum): - def _generate_next_value_(name, start, count, last_values): - return name +class EACLOperation(Enum): + PUT = 'put' + GET = 'get' + HEAD = 'head' + GET_RANGE = 'getrange' + GET_RANGE_HASH = 'getrangehash' + SEARCH = 'search' + DELETE = 'delete' -class Role(AutoName): - USER = auto() - SYSTEM = auto() - OTHERS = auto() +class EACLAccess(Enum): + ALLOW = 'allow' + DENY = 'deny' -@keyword('Get eACL') +class EACLRole(Enum): + OTHERS = 'others' + USER = 'user' + SYSTEM = 'system' + + +class EACLHeaderType(Enum): + REQUEST = 'req' # Filter request headers + OBJECT = 'obj' # Filter object headers + SERVICE = 'SERVICE' # Filter service headers. These are not processed by NeoFS nodes and exist for service use only + + +class EACLMatchType(Enum): + STRING_EQUAL = '=' # Return true if strings are equal + STRING_NOT_EQUAL = '!=' # Return true if strings are different + + +@dataclass +class EACLFilter: + header_type: EACLHeaderType = EACLHeaderType.REQUEST + match_type: EACLMatchType = EACLMatchType.STRING_EQUAL + key: Optional[str] = None + value: Optional[str] = None + + def to_dict(self) -> Dict[str, Any]: + return {'headerType': self.header_type, 'matchType': self.match_type, 'key': self.key, 'value': self.value} + + +@dataclass +class EACLFilters: + filters: Optional[List[EACLFilter]] = None + + def __str__(self): + return ','.join( + [f'{filter.header_type.value}:{filter.key}{filter.match_type.value}{filter.value}' + for filter in self.filters] + ) if self.filters else [] + + +@dataclass +class EACLPubKey: + keys: Optional[List[str]] = None + + +@dataclass +class EACLRule: + operation: Optional[EACLOperation] = None + access: Optional[EACLAccess] = None + role: Optional[Union[EACLRole, str]] = None + filters: Optional[EACLFilters] = None + + def to_dict(self) -> Dict[str, Any]: + return {'Operation': self.operation, 'Access': self.access, 'Role': self.role, + 'Filters': self.filters or []} + + def __str__(self): + role = self.role.value if isinstance(self.role, EACLRole) else f'pubkey:{get_wallet_public_key(self.role, "")}' + return f'{self.access.value} {self.operation.value} {self.filters or ""} {role}' + + +@allure.title('Get extended ACL') def get_eacl(wallet_path: str, cid: str) -> Optional[str]: - cmd = ( - f'{NEOFS_CLI_EXEC} --rpc-endpoint {NEOFS_ENDPOINT} --wallet {wallet_path} ' - f'container get-eacl --cid {cid} --config {WALLET_CONFIG}' - ) + cli = NeofsCli(config=WALLET_CONFIG) try: - output = _cmd_run(cmd) - if re.search(r'extended ACL table is not set for this container', output): - return None - return output + output = cli.container.get_eacl(wallet=wallet_path, rpc_endpoint=NEOFS_ENDPOINT, cid=cid) except RuntimeError as exc: logger.info("Extended ACL table is not set for this container") logger.info(f"Got exception while getting eacl: {exc}") return None + if 'extended ACL table is not set for this container' in output: + return None + return output -@keyword('Set eACL') +@allure.title('Set extended ACL') def set_eacl(wallet_path: str, cid: str, eacl_table_path: str) -> None: - cmd = ( - f'{NEOFS_CLI_EXEC} --rpc-endpoint {NEOFS_ENDPOINT} --wallet {wallet_path} ' - f'container set-eacl --cid {cid} --table {eacl_table_path} --config {WALLET_CONFIG} --await' - ) - _cmd_run(cmd) + cli = NeofsCli(config=WALLET_CONFIG, timeout=60) + cli.container.set_eacl(wallet=wallet_path, rpc_endpoint=NEOFS_ENDPOINT, cid=cid, table=eacl_table_path, + await_mode=True) def _encode_cid_for_eacl(cid: str) -> str: @@ -65,13 +121,9 @@ def _encode_cid_for_eacl(cid: str) -> str: return base64.b64encode(cid_base58).decode("utf-8") -@keyword('Create eACL') -def create_eacl(cid: str, rules_list: list) -> str: +def create_eacl(cid: str, rules_list: List[EACLRule]) -> str: table_file_path = f"{os.getcwd()}/{ASSETS_DIR}/eacl_table_{str(uuid.uuid4())}.json" - rules = " ".join(f"--rule '{rule}'" for rule in rules_list) - - cmd = f"{NEOFS_CLI_EXEC} acl extended create --cid {cid} {rules} --out {table_file_path}" - _cmd_run(cmd) + NeofsCli().acl.extended_create(cid=cid, out=table_file_path, rule=rules_list) with open(table_file_path, 'r') as file: table_data = file.read() @@ -80,11 +132,10 @@ def create_eacl(cid: str, rules_list: list) -> str: return table_file_path -@keyword('Form BearerToken File') -def form_bearertoken_file(wif: str, cid: str, eacl_records: list) -> str: +def form_bearertoken_file(wif: str, cid: str, eacl_rule_list: List[Union[EACLRule, EACLPubKey]]) -> str: """ This function fetches eACL for given on behalf of , - then extends it with filters taken from , signs + then extends it with filters taken from , signs with bearer token and writes to file """ enc_cid = _encode_cid_for_eacl(cid) @@ -93,8 +144,7 @@ def form_bearertoken_file(wif: str, cid: str, eacl_records: list) -> str: eacl = get_eacl(wif, cid) json_eacl = dict() if eacl: - eacl = eacl.replace('eACL: ', '') - eacl = eacl.split('Signature')[0] + eacl = eacl.replace('eACL: ', '').split('Signature')[0] json_eacl = json.loads(eacl) logger.info(json_eacl) eacl_result = { @@ -117,32 +167,28 @@ def form_bearertoken_file(wif: str, cid: str, eacl_records: list) -> str: } } - if not eacl_records: - raise (f"Got empty eacl_records list: {eacl_records}") - for record in eacl_records: + assert eacl_rules, 'Got empty eacl_records list' + for rule in eacl_rule_list: op_data = { - "operation": record['Operation'], - "action": record['Access'], - "filters": [], + "operation": rule.operation.value.upper(), + "action": rule.access.value.upper(), + "filters": rule.filters or [], "targets": [] } - if Role(record['Role']): + if isinstance(rule.role, EACLRole): op_data['targets'] = [ { - "role": record['Role'] + "role": rule.role.value.upper() } ] - else: + elif isinstance(rule.role, EACLPubKey): op_data['targets'] = [ { - "keys": [record['Role']] + 'keys': rule.role.keys } ] - if 'Filters' in record.keys(): - op_data["filters"].append(record['Filters']) - eacl_result["body"]["eaclTable"]["records"].append(op_data) # Add records from current eACL @@ -158,7 +204,6 @@ def form_bearertoken_file(wif: str, cid: str, eacl_records: list) -> str: return file_path -@keyword('EACL Rules') def eacl_rules(access: str, verbs: list, user: str) -> list[str]: """ This function creates a list of eACL rules. @@ -188,3 +233,9 @@ def sign_bearer_token(wallet_path: str, eacl_rules_file: str) -> None: f'--to {eacl_rules_file} --wallet {wallet_path} --config {WALLET_CONFIG} --json' ) _cmd_run(cmd) + + +@allure.title('Wait for eACL cache expired') +def wait_for_cache_expired(): + sleep(NEOFS_CONTRACT_CACHE_TIMEOUT) + return diff --git a/robot/resources/lib/python_keywords/cli/acl.py b/robot/resources/lib/python_keywords/cli/acl.py new file mode 100644 index 0000000..887921c --- /dev/null +++ b/robot/resources/lib/python_keywords/cli/acl.py @@ -0,0 +1,47 @@ +from typing import Optional + +from .cli_command import NeofsCliCommandBase + + +class NeofsCliACL(NeofsCliCommandBase): + def extended_create(self, cid: str, out: str, file: Optional[str] = None, rule: Optional[list] = None) -> str: + + """Create extended ACL from the text representation. + + Rule consist of these blocks: [ ...] [ ...] + Action is 'allow' or 'deny'. + Operation is an object service verb: 'get', 'head', 'put', 'search', 'delete', 'getrange', or 'getrangehash'. + + Filter consists of : + Typ is 'obj' for object applied filter or 'req' for request applied filter. + Key is a valid unicode string corresponding to object or request header key. + Well-known system object headers start with '$Object:' prefix. + User defined headers start without prefix. + Read more about filter keys at: + http://github.com/nspcc-dev/neofs-api/blob/master/proto-docs/acl.md#message-eaclrecordfilter + Match is '=' for matching and '!=' for non-matching filter. + Value is a valid unicode string corresponding to object or request header value. + + Target is + 'user' for container owner, + 'system' for Storage nodes in container and Inner Ring nodes, + 'others' for all other request senders, + 'pubkey:,,...' for exact request sender, where is a hex-encoded 33-byte public key. + + When both '--rule' and '--file' arguments are used, '--rule' records will be placed higher in resulting + extended ACL table. + + Args: + cid: Container ID + file: Read list of extended ACL table records from from text file + out: Save JSON formatted extended ACL table in file + rule: Extended ACL table record to apply + + Returns: + str: Command string + + """ + return self._execute( + 'acl extended create', + **{param: param_value for param, param_value in locals().items() if param not in ['self']} + ) diff --git a/robot/resources/lib/python_keywords/cli/cli.py b/robot/resources/lib/python_keywords/cli/cli.py index 2cb4292..2074f74 100644 --- a/robot/resources/lib/python_keywords/cli/cli.py +++ b/robot/resources/lib/python_keywords/cli/cli.py @@ -3,6 +3,7 @@ from typing import Optional from common import NEOFS_CLI_EXEC from .accounting import NeofsCliAccounting +from .acl import NeofsCliACL from .cli_command import NeofsCliCommandBase from .container import NeofsCliContainer from .object import NeofsCliObject @@ -12,6 +13,7 @@ class NeofsCli: neofs_cli_exec_path: Optional[str] = None config: Optional[str] = None accounting: Optional[NeofsCliAccounting] = None + acl: Optional[NeofsCliACL] = None container: Optional[NeofsCliContainer] = None object: Optional[NeofsCliObject] = None @@ -19,6 +21,7 @@ class NeofsCli: self.config = config # config(str): config file (default is $HOME/.config/neofs-cli/config.yaml) self.neofs_cli_exec_path = neofs_cli_exec_path or NEOFS_CLI_EXEC self.accounting = NeofsCliAccounting(self.neofs_cli_exec_path, timeout=timeout, config=config) + self.acl = NeofsCliACL(self.neofs_cli_exec_path, timeout=timeout, config=config) self.container = NeofsCliContainer(self.neofs_cli_exec_path, timeout=timeout, config=config) self.object = NeofsCliObject(self.neofs_cli_exec_path, timeout=timeout, config=config) diff --git a/robot/resources/lib/python_keywords/cli/cli_command.py b/robot/resources/lib/python_keywords/cli/cli_command.py index 6066ee7..1d2afa1 100644 --- a/robot/resources/lib/python_keywords/cli/cli_command.py +++ b/robot/resources/lib/python_keywords/cli/cli_command.py @@ -24,13 +24,21 @@ class NeofsCliCommandBase: continue if isinstance(value, bool): param_str.append(f'--{param}') + elif isinstance(value, int): + param_str.append(f'--{param} {value}') elif isinstance(value, list): - param_str.append(f'--{param} \'{",".join(value)}\'') + for value_item in value: + val_str = str(value_item).replace("'", "\\'") + param_str.append(f"--{param} '{val_str}'") elif isinstance(value, dict): param_str.append(f'--{param} \'{",".join(f"{key}={val}" for key, val in value.items())}\'') else: - value_str = str(value).replace("'", "\\'") - param_str.append(f"--{param} '{value_str}'") + if "'" in str(value): + value_str = str(value).replace('"', '\\"') + param_str.append(f'--{param} "{value_str}"') + else: + param_str.append(f"--{param} '{value}'") + param_str = ' '.join(param_str) return f'{self.neofs_cli_exec} {self.__base_params} {command or ""} {param_str}' diff --git a/robot/resources/lib/python_keywords/cli/container.py b/robot/resources/lib/python_keywords/cli/container.py index 61b358f..7b7e924 100644 --- a/robot/resources/lib/python_keywords/cli/container.py +++ b/robot/resources/lib/python_keywords/cli/container.py @@ -8,7 +8,7 @@ class NeofsCliContainer(NeofsCliCommandBase): basic_acl: Optional[str] = None, await_mode: bool = False, disable_timestamp: bool = False, name: Optional[str] = None, nonce: Optional[str] = None, policy: Optional[str] = None, session: Optional[str] = None, subnet: Optional[str] = None, ttl: Optional[int] = None, - xhdr: Optional[list] = None) -> str: + xhdr: Optional[dict] = None) -> str: """Create a new container and register it in the NeoFS. It will be stored in the sidechain when the Inner Ring accepts it. @@ -39,7 +39,8 @@ class NeofsCliContainer(NeofsCliCommandBase): ) def delete(self, rpc_endpoint: str, wallet: str, cid: str, address: Optional[str] = None, await_mode: bool = False, - session: Optional[str] = None, ttl: Optional[int] = None, xhdr: Optional[list] = None) -> str: + session: Optional[str] = None, ttl: Optional[int] = None, xhdr: Optional[dict] = None, + force: bool = False) -> str: """Delete an existing container. Only the owner of the container has permission to remove the container. @@ -47,6 +48,7 @@ class NeofsCliContainer(NeofsCliCommandBase): address: address of wallet account await_mode: block execution until container is removed cid: container ID + force: do not check whether container contains locks and remove immediately rpc_endpoint: remote node address (as 'multiaddr' or ':') session: path to a JSON-encoded container session token ttl: TTL value in request meta header (default 2) diff --git a/robot/resources/lib/python_keywords/cli/object.py b/robot/resources/lib/python_keywords/cli/object.py index 59eefca..ce9b65e 100644 --- a/robot/resources/lib/python_keywords/cli/object.py +++ b/robot/resources/lib/python_keywords/cli/object.py @@ -6,7 +6,7 @@ from .cli_command import NeofsCliCommandBase class NeofsCliObject(NeofsCliCommandBase): def delete(self, rpc_endpoint: str, wallet: str, cid: str, oid: str, address: Optional[str] = None, bearer: Optional[str] = None, session: Optional[str] = None, ttl: Optional[int] = None, - xhdr: Optional[list] = None, **params) -> str: + xhdr: Optional[dict] = None) -> str: """Delete object from NeoFS Args: @@ -26,13 +26,13 @@ class NeofsCliObject(NeofsCliCommandBase): """ return self._execute( 'object delete', - **{param: param_value for param, param_value in locals().items() if param not in ['self', 'params']} + **{param: param_value for param, param_value in locals().items() if param not in ['self']} ) def get(self, rpc_endpoint: str, wallet: str, cid: str, oid: str, address: Optional[str] = None, bearer: Optional[str] = None, file: Optional[str] = None, header: Optional[str] = None, no_progress: bool = False, raw: bool = False, - session: Optional[str] = None, ttl: Optional[int] = None, xhdr: Optional[list] = None, **params) -> str: + session: Optional[str] = None, ttl: Optional[int] = None, xhdr: Optional[dict] = None) -> str: """Get object from NeoFS Args: @@ -56,13 +56,12 @@ class NeofsCliObject(NeofsCliCommandBase): """ return self._execute( 'object get', - **{param: param_value for param, param_value in locals().items() if param not in ['self', 'params']} + **{param: param_value for param, param_value in locals().items() if param not in ['self']} ) def hash(self, rpc_endpoint: str, wallet: str, cid: str, oid: str, address: Optional[str] = None, bearer: Optional[str] = None, range: Optional[str] = None, salt: Optional[str] = None, - ttl: Optional[int] = None, hash_type: Optional[str] = None, xhdr: Optional[list] = None, - **params) -> str: + ttl: Optional[int] = None, hash_type: Optional[str] = None, xhdr: Optional[dict] = None) -> str: """Get object hash Args: @@ -90,7 +89,7 @@ class NeofsCliObject(NeofsCliCommandBase): def head(self, rpc_endpoint: str, wallet: str, cid: str, oid: str, address: Optional[str] = None, bearer: Optional[str] = None, file: Optional[str] = None, json_mode: bool = False, main_only: bool = False, proto: bool = False, raw: bool = False, - session: Optional[str] = None, ttl: Optional[int] = None, xhdr: Optional[list] = None, **params) -> str: + session: Optional[str] = None, ttl: Optional[int] = None, xhdr: Optional[dict] = None) -> str: """Get object header Args: @@ -116,12 +115,12 @@ class NeofsCliObject(NeofsCliCommandBase): """ return self._execute( 'object head', - **{param: param_value for param, param_value in locals().items() if param not in ['self', 'params']} + **{param: param_value for param, param_value in locals().items() if param not in ['self']} ) def lock(self, rpc_endpoint: str, wallet: str, cid: str, oid: str, lifetime: int, address: Optional[str] = None, - bearer: Optional[str] = None, session: Optional[str] = None, - ttl: Optional[int] = None, xhdr: Optional[list] = None, **params) -> str: + bearer: Optional[str] = None, session: Optional[str] = None, ttl: Optional[int] = None, + xhdr: Optional[dict] = None) -> str: """Lock object in container Args: @@ -143,14 +142,14 @@ class NeofsCliObject(NeofsCliCommandBase): """ return self._execute( 'object lock', - **{param: param_value for param, param_value in locals().items() if param not in ['self', 'params']} + **{param: param_value for param, param_value in locals().items() if param not in ['self']} ) def put(self, rpc_endpoint: str, wallet: str, cid: str, file: str, address: Optional[str] = None, attributes: Optional[dict] = None, bearer: Optional[str] = None, disable_filename: bool = False, disable_timestamp: bool = False, expire_at: Optional[int] = None, no_progress: bool = False, notify: Optional[str] = None, session: Optional[str] = None, ttl: Optional[int] = None, - xhdr: Optional[list] = None, **params) -> str: + xhdr: Optional[dict] = None) -> str: """Put object to NeoFS Args: @@ -176,12 +175,12 @@ class NeofsCliObject(NeofsCliCommandBase): """ return self._execute( 'object put', - **{param: param_value for param, param_value in locals().items() if param not in ['self', 'params']} + **{param: param_value for param, param_value in locals().items() if param not in ['self']} ) def range(self, rpc_endpoint: str, wallet: str, cid: str, oid: str, range: str, address: Optional[str] = None, bearer: Optional[str] = None, file: Optional[str] = None, json_mode: bool = False, raw: bool = False, - session: Optional[str] = None, ttl: Optional[int] = None, xhdr: Optional[list] = None, **params) -> str: + session: Optional[str] = None, ttl: Optional[int] = None, xhdr: Optional[dict] = None) -> str: """Get payload range data of an object Args: @@ -206,13 +205,13 @@ class NeofsCliObject(NeofsCliCommandBase): """ return self._execute( 'object range', - **{param: param_value for param, param_value in locals().items() if param not in ['self', 'params']} + **{param: param_value for param, param_value in locals().items() if param not in ['self']} ) def search(self, rpc_endpoint: str, wallet: str, cid: str, address: Optional[str] = None, bearer: Optional[str] = None, filters: Optional[list] = None, oid: Optional[str] = None, phy: bool = False, root: bool = False, session: Optional[str] = None, ttl: Optional[int] = None, - xhdr: Optional[list] = None, **params) -> str: + xhdr: Optional[dict] = None) -> str: """Search object Args: @@ -236,5 +235,5 @@ class NeofsCliObject(NeofsCliCommandBase): """ return self._execute( 'object search', - **{param: param_value for param, param_value in locals().items() if param not in ['self', 'params']} + **{param: param_value for param, param_value in locals().items() if param not in ['self']} ) diff --git a/robot/resources/lib/python_keywords/container.py b/robot/resources/lib/python_keywords/container.py index 16d527f..54b6e93 100644 --- a/robot/resources/lib/python_keywords/container.py +++ b/robot/resources/lib/python_keywords/container.py @@ -130,17 +130,18 @@ def get_container(wallet: str, cid: str, json_mode: bool = True) -> Union[dict, @keyword('Delete Container') # TODO: make the error message about a non-found container more user-friendly # https://github.com/nspcc-dev/neofs-contract/issues/121 -def delete_container(wallet: str, cid: str) -> None: +def delete_container(wallet: str, cid: str, force: bool = False) -> None: """ A wrapper for `neofs-cli container delete` call. Args: wallet (str): path to a wallet on whose behalf we delete the container cid (str): ID of the container to delete + force (bool): do not check whether container contains locks and remove immediately This function doesn't return anything. """ cli = NeofsCli(config=WALLET_CONFIG) - cli.container.delete(wallet=wallet, cid=cid, rpc_endpoint=NEOFS_ENDPOINT) + cli.container.delete(wallet=wallet, cid=cid, rpc_endpoint=NEOFS_ENDPOINT, force=force) def _parse_cid(output: str) -> str: diff --git a/robot/resources/lib/python_keywords/container_access.py b/robot/resources/lib/python_keywords/container_access.py new file mode 100644 index 0000000..3f77736 --- /dev/null +++ b/robot/resources/lib/python_keywords/container_access.py @@ -0,0 +1,70 @@ +from typing import List, Optional + +from acl import EACLOperation +from python_keywords.object_access import (can_get_object, can_put_object, can_delete_object, can_get_head_object, + can_get_range_hash_of_object, can_get_range_of_object, can_search_object) + + +def check_full_access_to_container(wallet: str, cid: str, oid: str, file_name: str, + bearer: Optional[str] = None, wallet_config: Optional[str] = None, + xhdr: Optional[dict] = None): + assert can_put_object(wallet, cid, file_name, bearer, wallet_config, xhdr) + assert can_get_head_object(wallet, cid, oid, bearer, wallet_config, xhdr) + assert can_get_range_of_object(wallet, cid, oid, bearer, wallet_config, xhdr) + assert can_get_range_hash_of_object(wallet, cid, oid, bearer, wallet_config, xhdr) + assert can_search_object(wallet, cid, oid, bearer, wallet_config, xhdr) + assert can_get_object(wallet, cid, oid, file_name, bearer, wallet_config, xhdr) + assert can_delete_object(wallet, cid, oid, bearer, wallet_config, xhdr) + + +def check_no_access_to_container(wallet: str, cid: str, oid: str, file_name: str, + bearer: Optional[str] = None, wallet_config: Optional[str] = None, + xhdr: Optional[dict] = None): + assert not can_put_object(wallet, cid, file_name, bearer, wallet_config, xhdr) + assert not can_get_head_object(wallet, cid, oid, bearer, wallet_config, xhdr) + assert not can_get_range_of_object(wallet, cid, oid, bearer, wallet_config, xhdr) + assert not can_get_range_hash_of_object(wallet, cid, oid, bearer, wallet_config, xhdr) + assert not can_search_object(wallet, cid, oid, bearer, wallet_config, xhdr) + assert not can_get_object(wallet, cid, oid, file_name, bearer, wallet_config, xhdr) + assert not can_delete_object(wallet, cid, oid, bearer, wallet_config, xhdr) + + +def check_custom_access_to_container(wallet: str, cid: str, oid: str, file_name: str, + deny_operations: Optional[List[EACLOperation]] = None, + ignore_operations: Optional[List[EACLOperation]] = None, + bearer: Optional[str] = None, wallet_config: Optional[str] = None, + xhdr: Optional[dict] = None): + deny_operations = [op.value for op in deny_operations or []] + ignore_operations = [op.value for op in ignore_operations or []] + checks: dict = {} + if EACLOperation.PUT.value not in ignore_operations: + checks[EACLOperation.PUT.value] = can_put_object(wallet, cid, file_name, bearer, wallet_config, xhdr) + if EACLOperation.HEAD.value not in ignore_operations: + checks[EACLOperation.HEAD.value] = can_get_head_object(wallet, cid, oid, bearer, wallet_config, xhdr) + if EACLOperation.GET_RANGE.value not in ignore_operations: + checks[EACLOperation.GET_RANGE.value] = can_get_range_of_object(wallet, cid, oid, bearer, wallet_config, xhdr) + if EACLOperation.GET_RANGE_HASH.value not in ignore_operations: + checks[EACLOperation.GET_RANGE_HASH.value] = can_get_range_hash_of_object(wallet, cid, oid, bearer, + wallet_config, xhdr) + if EACLOperation.SEARCH.value not in ignore_operations: + checks[EACLOperation.SEARCH.value] = can_search_object(wallet, cid, oid, bearer, wallet_config, xhdr) + if EACLOperation.GET.value not in ignore_operations: + checks[EACLOperation.GET.value] = can_get_object(wallet, cid, oid, file_name, bearer, wallet_config, xhdr) + if EACLOperation.DELETE.value not in ignore_operations: + checks[EACLOperation.DELETE.value] = can_delete_object(wallet, cid, oid, bearer, wallet_config, xhdr) + + failed_checks = ( + [f'allowed {action} failed' for action, success in checks.items() if + not success and action not in deny_operations] + + [f'denied {action} succeeded' for action, success in checks.items() if + success and action in deny_operations]) + + assert not failed_checks, ", ".join(failed_checks) + + +def check_read_only_container(wallet: str, cid: str, oid: str, file_name: str, + bearer: Optional[str] = None, wallet_config: Optional[str] = None, + xhdr: Optional[dict] = None): + return check_custom_access_to_container(wallet, cid, oid, file_name, + deny_operations=[EACLOperation.PUT, EACLOperation.DELETE], + bearer=bearer, wallet_config=wallet_config, xhdr=xhdr) diff --git a/robot/resources/lib/python_keywords/neofs_verbs.py b/robot/resources/lib/python_keywords/neofs_verbs.py index 20b2d9e..315c55f 100644 --- a/robot/resources/lib/python_keywords/neofs_verbs.py +++ b/robot/resources/lib/python_keywords/neofs_verbs.py @@ -21,8 +21,8 @@ ROBOT_AUTO_KEYWORDS = False @keyword('Get object') def get_object(wallet: str, cid: str, oid: str, bearer_token: Optional[str] = None, write_object: str = "", - endpoint: str = "", options: Optional[dict] = None, wallet_config: str = WALLET_CONFIG, - no_progress: bool = True): + endpoint: str = "", xhdr: Optional[dict] = None, wallet_config: Optional[str] = None, + no_progress: bool = True) -> str: """ GET from NeoFS. @@ -35,11 +35,12 @@ def get_object(wallet: str, cid: str, oid: str, bearer_token: Optional[str] = No endpoint (optional, str): NeoFS endpoint to send request to, appends to `--rpc-endpoint` key wallet_config(optional, str): path to the wallet config no_progress(optional, bool): do not show progress bar - options (optional, str): any options which `neofs-cli object get` accepts + xhdr (optional, dict): Request X-Headers in form of Key=Value Returns: (str): path to downloaded file """ + wallet_config = wallet_config or WALLET_CONFIG if not write_object: write_object = str(uuid.uuid4()) file_path = f"{ASSETS_DIR}/{write_object}" @@ -49,7 +50,7 @@ def get_object(wallet: str, cid: str, oid: str, bearer_token: Optional[str] = No cli = NeofsCli(config=wallet_config) cli.object.get(rpc_endpoint=endpoint, wallet=wallet, cid=cid, oid=oid, file=file_path, - bearer=bearer_token, no_progress=no_progress, **options or {}) + bearer=bearer_token, no_progress=no_progress, xhdr=xhdr) return file_path @@ -57,7 +58,7 @@ def get_object(wallet: str, cid: str, oid: str, bearer_token: Optional[str] = No # TODO: make `bearer_token` optional @keyword('Get Range Hash') def get_range_hash(wallet: str, cid: str, oid: str, bearer_token: str, range_cut: str, - wallet_config: str = WALLET_CONFIG, options: Optional[dict] = None): + wallet_config: Optional[str] = None, xhdr: Optional[dict] = None): """ GETRANGEHASH of given Object. @@ -69,23 +70,24 @@ def get_range_hash(wallet: str, cid: str, oid: str, bearer_token: str, range_cut value to pass to the `--range` parameter bearer_token (optional, str): path to Bearer Token file, appends to `--bearer` key wallet_config(optional, str): path to the wallet config - options (optional, str): any options which `neofs-cli object hash` accepts + xhdr (optional, dict): Request X-Headers in form of Key=Value Returns: None """ + wallet_config = wallet_config or WALLET_CONFIG cli = NeofsCli(config=wallet_config) output = cli.object.hash(rpc_endpoint=NEOFS_ENDPOINT, wallet=wallet, cid=cid, oid=oid, range=range_cut, - bearer=bearer_token, **options or {}) + bearer=bearer_token, xhdr=xhdr) # cutting off output about range offset and length return output.split(':')[1].strip() @keyword('Put object') -def put_object(wallet: str, path: str, cid: str, bearer: str = "", user_headers: Optional[dict] = None, - endpoint: str = "", wallet_config: str = WALLET_CONFIG, expire_at: Optional[int] = None, - no_progress: bool = True, options: Optional[dict] = None): +def put_object(wallet: str, path: str, cid: str, bearer: str = "", attributes: Optional[dict] = None, + xhdr: Optional[dict] = None, endpoint: str = "", wallet_config: Optional[str] = None, + expire_at: Optional[int] = None, no_progress: bool = True): """ PUT of given file. @@ -94,24 +96,24 @@ def put_object(wallet: str, path: str, cid: str, bearer: str = "", user_headers: path (str): path to file to be PUT cid (str): ID of Container where we get the Object from bearer (optional, str): path to Bearer Token file, appends to `--bearer` key - user_headers (optional, dict): Object attributes, append to `--attributes` key + attributes (optional, str): User attributes in form of Key1=Value1,Key2=Value2 endpoint(optional, str): NeoFS endpoint to send request to wallet_config(optional, str): path to the wallet config no_progress(optional, bool): do not show progress bar - options (optional, str): any options which `neofs-cli object put` accepts expire_at (optional, int): Last epoch in the life of the object + xhdr (optional, dict): Request X-Headers in form of Key=Value Returns: (str): ID of uploaded Object """ + wallet_config = wallet_config or WALLET_CONFIG if not endpoint: endpoint = random.sample(NEOFS_NETMAP, 1)[0] if not endpoint: logger.info(f'---DEB:\n{NEOFS_NETMAP}') cli = NeofsCli(config=wallet_config) - output = cli.object.put(rpc_endpoint=endpoint, wallet=wallet, file=path, cid=cid, bearer=bearer, - expire_at=expire_at, no_progress=no_progress, - attributes=user_headers or {}, **options or {}) + output = cli.object.put(rpc_endpoint=endpoint, wallet=wallet, file=path, cid=cid, attributes=attributes, + bearer=bearer, expire_at=expire_at, no_progress=no_progress, xhdr=xhdr) # splitting CLI output to lines and taking the penultimate line id_str = output.strip().split('\n')[-2] @@ -120,8 +122,8 @@ def put_object(wallet: str, path: str, cid: str, bearer: str = "", user_headers: @keyword('Delete object') -def delete_object(wallet: str, cid: str, oid: str, bearer: str = "", wallet_config: str = WALLET_CONFIG, - options: Optional[dict] = None): +def delete_object(wallet: str, cid: str, oid: str, bearer: str = "", wallet_config: Optional[str] = None, + xhdr: Optional[dict] = None): """ DELETE an Object. @@ -131,14 +133,15 @@ def delete_object(wallet: str, cid: str, oid: str, bearer: str = "", wallet_conf oid (str): ID of Object we are going to delete bearer (optional, str): path to Bearer Token file, appends to `--bearer` key wallet_config(optional, str): path to the wallet config - options (optional, dict): any options which `neofs-cli object delete` accepts + xhdr (optional, dict): Request X-Headers in form of Key=Value Returns: (str): Tombstone ID """ + wallet_config = wallet_config or WALLET_CONFIG cli = NeofsCli(config=wallet_config) output = cli.object.delete(rpc_endpoint=NEOFS_ENDPOINT, wallet=wallet, cid=cid, oid=oid, bearer=bearer, - **options or {}) + xhdr=xhdr) id_str = output.split('\n')[1] tombstone = id_str.split(':')[1] @@ -146,8 +149,8 @@ def delete_object(wallet: str, cid: str, oid: str, bearer: str = "", wallet_conf @keyword('Get Range') -def get_range(wallet: str, cid: str, oid: str, range_cut: str, wallet_config: str = WALLET_CONFIG, - bearer: str = "", options: Optional[dict] = None): +def get_range(wallet: str, cid: str, oid: str, range_cut: str, wallet_config: Optional[str] = None, + bearer: str = "", xhdr: Optional[dict] = None): """ GETRANGE an Object. @@ -158,15 +161,16 @@ def get_range(wallet: str, cid: str, oid: str, range_cut: str, wallet_config: st range_cut (str): range to take data from in the form offset:length bearer (optional, str): path to Bearer Token file, appends to `--bearer` key wallet_config(optional, str): path to the wallet config - options (optional, dict): any options which `neofs-cli object range` accepts + xhdr (optional, dict): Request X-Headers in form of Key=Value Returns: (str, bytes) - path to the file with range content and content of this file as bytes """ + wallet_config = wallet_config or WALLET_CONFIG range_file = f"{ASSETS_DIR}/{uuid.uuid4()}" cli = NeofsCli(config=wallet_config) cli.object.range(rpc_endpoint=NEOFS_ENDPOINT, wallet=wallet, cid=cid, oid=oid, range=range_cut, file=range_file, - bearer=bearer, **options or {}) + bearer=bearer, xhdr=xhdr) with open(range_file, 'rb') as fout: content = fout.read() @@ -175,8 +179,8 @@ def get_range(wallet: str, cid: str, oid: str, range_cut: str, wallet_config: st @keyword('Search object') def search_object(wallet: str, cid: str, bearer: str = "", filters: Optional[dict] = None, - expected_objects_list: Optional[list] = None, wallet_config: str = WALLET_CONFIG, - options: Optional[dict] = None): + expected_objects_list: Optional[list] = None, wallet_config: Optional[str] = None, + xhdr: Optional[dict] = None) -> list: """ SEARCH an Object. @@ -187,16 +191,16 @@ def search_object(wallet: str, cid: str, bearer: str = "", filters: Optional[dic filters (optional, dict): key=value pairs to filter Objects expected_objects_list (optional, list): a list of ObjectIDs to compare found Objects with wallet_config(optional, str): path to the wallet config - options(optional, str): any other options which `neofs-cli object search` might accept + xhdr (optional, dict): Request X-Headers in form of Key=Value Returns: (list): list of found ObjectIDs """ + wallet_config = wallet_config or WALLET_CONFIG cli = NeofsCli(config=wallet_config) output = cli.object.search( - rpc_endpoint=NEOFS_ENDPOINT, wallet=wallet, cid=cid, bearer=bearer, - filters=[f'{filter_key} EQ {filter_val}' for filter_key, filter_val in filters.items()] if filters else None, - **options or {}) + rpc_endpoint=NEOFS_ENDPOINT, wallet=wallet, cid=cid, bearer=bearer, xhdr=xhdr, + filters=[f'{filter_key} EQ {filter_val}' for filter_key, filter_val in filters.items()] if filters else None) found_objects = re.findall(r'(\w{43,44})', output) @@ -213,8 +217,8 @@ def search_object(wallet: str, cid: str, bearer: str = "", filters: Optional[dic @keyword('Head object') def head_object(wallet: str, cid: str, oid: str, bearer_token: str = "", - options: Optional[dict] = None, endpoint: str = None, json_output: bool = True, - is_raw: bool = False, is_direct: bool = False, wallet_config: str = WALLET_CONFIG): + xhdr: Optional[dict] = None, endpoint: str = None, json_output: bool = True, + is_raw: bool = False, is_direct: bool = False, wallet_config: Optional[str] = None): """ HEAD an Object. @@ -223,7 +227,6 @@ def head_object(wallet: str, cid: str, oid: str, bearer_token: str = "", cid (str): ID of Container where we get the Object from oid (str): ObjectID to HEAD bearer_token (optional, str): path to Bearer Token file, appends to `--bearer` key - options (optional, str): any options which `neofs-cli object head` accepts endpoint(optional, str): NeoFS endpoint to send request to json_output(optional, bool): return reponse in JSON format or not; this flag turns into `--json` key @@ -232,6 +235,7 @@ def head_object(wallet: str, cid: str, oid: str, bearer_token: str = "", is_direct(optional, bool): send request directly to the node or not; this flag turns into `--ttl 1` key wallet_config(optional, str): path to the wallet config + xhdr (optional, dict): Request X-Headers in form of Key=Value Returns: depending on the `json_output` parameter value, the function returns (dict): HEAD response in JSON format @@ -239,10 +243,11 @@ def head_object(wallet: str, cid: str, oid: str, bearer_token: str = "", (str): HEAD response as a plain text """ + wallet_config = wallet_config or WALLET_CONFIG cli = NeofsCli(config=wallet_config) output = cli.object.head(rpc_endpoint=endpoint or NEOFS_ENDPOINT, wallet=wallet, cid=cid, oid=oid, bearer=bearer_token, json_mode=json_output, raw=is_raw, - ttl=1 if is_direct else None, **options or {}) + ttl=1 if is_direct else None, xhdr=xhdr) if not json_output: return output diff --git a/robot/resources/lib/python_keywords/object_access.py b/robot/resources/lib/python_keywords/object_access.py new file mode 100644 index 0000000..754f8f3 --- /dev/null +++ b/robot/resources/lib/python_keywords/object_access.py @@ -0,0 +1,100 @@ +from typing import Optional + +import allure + +from grpc_responses import OBJECT_ACCESS_DENIED, error_matches_status +from python_keywords.neofs_verbs import (delete_object, get_object, get_range, get_range_hash, head_object, put_object, + search_object) +from python_keywords.utility_keywords import get_file_hash + +OPERATION_ERROR_TYPE = RuntimeError + + +def can_get_object(wallet: str, cid: str, oid: str, file_name: str, bearer: Optional[str] = None, + wallet_config: Optional[str] = None, xhdr: Optional[dict] = None + ) -> bool: + with allure.step('Try get object from container'): + try: + got_file_path = get_object(wallet, cid, oid, bearer_token=bearer, wallet_config=wallet_config, xhdr=xhdr) + except OPERATION_ERROR_TYPE as err: + assert error_matches_status(err, OBJECT_ACCESS_DENIED), f'Expected {err} to match {OBJECT_ACCESS_DENIED}' + return False + assert get_file_hash(file_name) == get_file_hash(got_file_path) + return True + + +def can_put_object(wallet: str, cid: str, file_name: str, bearer: Optional[str] = None, + wallet_config: Optional[str] = None, xhdr: Optional[dict] = None, attributes: Optional[dict] = None, + ) -> bool: + with allure.step('Try put object to container'): + try: + put_object(wallet, file_name, cid, bearer=bearer, wallet_config=wallet_config, xhdr=xhdr, + attributes=attributes) + except OPERATION_ERROR_TYPE as err: + assert error_matches_status(err, OBJECT_ACCESS_DENIED), f'Expected {err} to match {OBJECT_ACCESS_DENIED}' + return False + return True + + +def can_delete_object(wallet: str, cid: str, oid: str, bearer: Optional[str] = None, + wallet_config: Optional[str] = None, xhdr: Optional[dict] = None + ) -> bool: + with allure.step('Try delete object from container'): + try: + delete_object(wallet, cid, oid, bearer=bearer, wallet_config=wallet_config, xhdr=xhdr) + except OPERATION_ERROR_TYPE as err: + assert error_matches_status(err, OBJECT_ACCESS_DENIED), f'Expected {err} to match {OBJECT_ACCESS_DENIED}' + return False + return True + + +def can_get_head_object(wallet: str, cid: str, oid: str, bearer: Optional[str] = None, + wallet_config: Optional[str] = None, xhdr: Optional[dict] = None + ) -> bool: + with allure.step('Try get head of object'): + try: + head_object(wallet, cid, oid, bearer_token=bearer, wallet_config=wallet_config, xhdr=xhdr) + except OPERATION_ERROR_TYPE as err: + assert error_matches_status(err, OBJECT_ACCESS_DENIED), f'Expected {err} to match {OBJECT_ACCESS_DENIED}' + return False + return True + + +def can_get_range_of_object(wallet: str, cid: str, oid: str, bearer: Optional[str] = None, + wallet_config: Optional[str] = None, xhdr: Optional[dict] = None + ) -> bool: + with allure.step('Try get range of object'): + try: + get_range(wallet, cid, oid, bearer=bearer, range_cut='0:10', wallet_config=wallet_config, + xhdr=xhdr) + except OPERATION_ERROR_TYPE as err: + assert error_matches_status(err, OBJECT_ACCESS_DENIED), f'Expected {err} to match {OBJECT_ACCESS_DENIED}' + return False + return True + + +def can_get_range_hash_of_object(wallet: str, cid: str, oid: str, bearer: Optional[str] = None, + wallet_config: Optional[str] = None, xhdr: Optional[dict] = None + ) -> bool: + with allure.step('Try get range hash of object'): + try: + get_range_hash(wallet, cid, oid, bearer_token=bearer, range_cut='0:10', wallet_config=wallet_config, + xhdr=xhdr) + except OPERATION_ERROR_TYPE as err: + assert error_matches_status(err, OBJECT_ACCESS_DENIED), f'Expected {err} to match {OBJECT_ACCESS_DENIED}' + return False + return True + + +def can_search_object(wallet: str, cid: str, oid: Optional[str] = None, bearer: Optional[str] = None, + wallet_config: Optional[str] = None, xhdr: Optional[dict] = None + ) -> bool: + with allure.step('Try search object in container'): + try: + oids = search_object(wallet, cid, bearer=bearer, wallet_config=wallet_config, xhdr=xhdr) + except OPERATION_ERROR_TYPE as err: + assert error_matches_status(err, OBJECT_ACCESS_DENIED), f'Expected {err} to match {OBJECT_ACCESS_DENIED}' + return False + if oid: + return oid in oids + return True diff --git a/robot/resources/lib/python_keywords/tombstone.py b/robot/resources/lib/python_keywords/tombstone.py index 116267a..488c356 100644 --- a/robot/resources/lib/python_keywords/tombstone.py +++ b/robot/resources/lib/python_keywords/tombstone.py @@ -11,11 +11,8 @@ ROBOT_AUTO_KEYWORDS = False @keyword('Verify Head Tombstone') -def verify_head_tombstone(wallet_path: str, cid: str, oid_ts: str, oid: str, - bearer: str = "", options: str = ""): - header = neofs_verbs.head_object(wallet_path, cid, oid_ts, - bearer_token=bearer, - options=options) +def verify_head_tombstone(wallet_path: str, cid: str, oid_ts: str, oid: str): + header = neofs_verbs.head_object(wallet_path, cid, oid_ts) header = header['header'] BuiltIn().should_be_equal(header["containerID"], cid, diff --git a/robot/variables/common.py b/robot/variables/common.py index 2d4d61d..c6dcfcd 100644 --- a/robot/variables/common.py +++ b/robot/variables/common.py @@ -27,7 +27,7 @@ GAS_HASH = '0xd2a4cff31913016155e38e474a2c06d08be276cf' NEOFS_CONTRACT = os.getenv("NEOFS_IR_CONTRACTS_NEOFS") -ASSETS_DIR = os.getenv("ASSETS_DIR", "TemporaryDir/") +ASSETS_DIR = os.getenv("ASSETS_DIR", "TemporaryDir") DEVENV_PATH = os.getenv("DEVENV_PATH", "../neofs-dev-env") CLI_CONFIGS_PATH = os.getenv("CLI_CONFIGS_PATH", f"{os.getcwd()}/neofs_cli_configs")