Add ACL and eACL PyTest tests

Signed-off-by: Vladimir Avdeev <v.avdeev@yadro.com>
This commit is contained in:
Vladimir Avdeev 2022-08-25 13:57:55 +03:00 committed by Vladimir Avdeev
parent 590a5cfb0e
commit 6d040c6834
36 changed files with 979 additions and 1318 deletions

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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)',

View file

@ -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:

View file

@ -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"
}
]
}
]
}

View file

@ -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"
}
]
}
]
}

View file

@ -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"
}
]
}
]
}

View file

@ -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"
}
]
}
]
}

View file

@ -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"
}
]
}
]
}

View file

@ -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"
}
]
}
]
}

View file

@ -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"
}
]
}
]
}

View file

@ -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"
}
]
}
]
}

View file

@ -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"
}
]
}
]
}

View file

@ -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"
}
]
}
]
}

View file

@ -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"
}
]
}
]
}

View file

@ -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"
}
]
}
]
}

View file

@ -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"
}
]
}
]
}

View file

@ -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"
}
]
}
]
}

View file

@ -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"
}
]
}
]
}

View file

@ -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"
}
]
}
]
}

View file

@ -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"
}
]
}
]
}

View file

@ -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 <cid> on behalf of <wif>,
then extends it with filters taken from <eacl_records>, signs
then extends it with filters taken from <eacl_rules>, 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

View file

@ -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> <operation> [<filter1> ...] [<target1> ...]
Action is 'allow' or 'deny'.
Operation is an object service verb: 'get', 'head', 'put', 'search', 'delete', 'getrange', or 'getrangehash'.
Filter consists of <typ>:<key><match><value>
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:<key1>,<key2>,...' for exact request sender, where <key> 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']}
)

View file

@ -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)

View file

@ -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}'

View file

@ -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 '<host>:<port>')
session: path to a JSON-encoded container session token
ttl: TTL value in request meta header (default 2)

View file

@ -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']}
)

View file

@ -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:

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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,

View file

@ -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")