From 67fdfb4a423acf2a7506ac4e7e2d27981f864de4 Mon Sep 17 00:00:00 2001 From: Evgeniy Zayats Date: Thu, 25 Jan 2024 20:51:10 -0500 Subject: [PATCH 1/2] Bump neofs-teslib 1.1.16 -> 1.1.17 Signed-off-by: Evgeniy Zayats --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 576658da4..d54ebaa70 100644 --- a/requirements.txt +++ b/requirements.txt @@ -29,7 +29,7 @@ mmh3==3.0.0 multidict==6.0.2 mypy==0.950 mypy-extensions==0.4.3 -neofs-testlib==1.1.16 +neofs-testlib==1.1.17 netaddr==0.8.0 packaging==21.3 paramiko==3.4.0 From 5e905e302a4b96f4e3d27ac990f656689883bcee Mon Sep 17 00:00:00 2001 From: Evgeniy Zayats Date: Thu, 25 Jan 2024 20:51:44 -0500 Subject: [PATCH 2/2] dyn_env: adapt session tests, closes #684 Signed-off-by: Evgeniy Zayats --- .../lib/helpers/session_token.py | 286 +++++++ .../lib/helpers/storage_object_info.py | 95 +++ .../lib/helpers/wallet_helpers.py | 24 + dynamic_env_pytest_tests/tests/conftest.py | 27 +- .../tests/session_token/conftest.py | 44 ++ .../test_object_session_token.py | 215 ++++++ .../test_static_object_session_token.py | 726 ++++++++++++++++++ .../test_static_session_token_container.py | 375 +++++++++ .../lib/python_keywords/object_access.py | 5 +- 9 files changed, 1774 insertions(+), 23 deletions(-) create mode 100644 dynamic_env_pytest_tests/lib/helpers/session_token.py create mode 100644 dynamic_env_pytest_tests/lib/helpers/wallet_helpers.py create mode 100644 dynamic_env_pytest_tests/tests/session_token/conftest.py create mode 100644 dynamic_env_pytest_tests/tests/session_token/test_object_session_token.py create mode 100644 dynamic_env_pytest_tests/tests/session_token/test_static_object_session_token.py create mode 100644 dynamic_env_pytest_tests/tests/session_token/test_static_session_token_container.py diff --git a/dynamic_env_pytest_tests/lib/helpers/session_token.py b/dynamic_env_pytest_tests/lib/helpers/session_token.py new file mode 100644 index 000000000..93719efa7 --- /dev/null +++ b/dynamic_env_pytest_tests/lib/helpers/session_token.py @@ -0,0 +1,286 @@ +import base64 +import json +import logging +import os +import uuid +from dataclasses import dataclass +from enum import Enum +from typing import Any, Optional + +import allure +import json_transformers +from common import ASSETS_DIR, NEOFS_CLI_EXEC, TEST_FILES_DIR, WALLET_CONFIG +from data_formatters import get_wallet_public_key +from json_transformers import encode_for_json +from neofs_testlib.cli import NeofsCli +from neofs_testlib.env.env import NodeWallet +from neofs_testlib.shell import Shell + +from helpers.storage_object_info import StorageObjectInfo + +logger = logging.getLogger("NeoLogger") + +UNRELATED_KEY = "unrelated key in the session" +UNRELATED_OBJECT = "unrelated object in the session" +UNRELATED_CONTAINER = "unrelated container in the session" +WRONG_VERB = "wrong verb of the session" +INVALID_SIGNATURE = "invalid signature of the session data" + + +class ObjectVerb(Enum): + PUT = "PUT" + DELETE = "DELETE" + GET = "GET" + RANGEHASH = "RANGEHASH" + RANGE = "RANGE" + HEAD = "HEAD" + SEARCH = "SEARCH" + + +class ContainerVerb(Enum): + CREATE = "PUT" + DELETE = "DELETE" + SETEACL = "SETEACL" + + +@dataclass +class Lifetime: + exp: int = 100000000 + nbf: int = 0 + iat: int = 0 + + +@allure.step("Generate Session Token") +def generate_session_token( + owner_wallet: NodeWallet, + session_wallet: NodeWallet, + session: dict[str, dict[str, Any]], + tokens_dir: str, + lifetime: Optional[Lifetime] = None, +) -> str: + """ + This function generates session token and writes it to the file. + Args: + owner_wallet: wallet of container owner + session_wallet: wallet to which we grant the access via session token + session: Contains allowed operation with parameters + tokens_dir: Dir for token + lifetime: lifetime options for session + Returns: + The path to the generated session token file + """ + + file_path = os.path.join(tokens_dir, str(uuid.uuid4())) + + pub_key_64 = get_wallet_public_key(session_wallet.path, session_wallet.password, "base64") + + lifetime = lifetime or Lifetime() + + session_token = { + "body": { + "id": f"{base64.b64encode(uuid.uuid4().bytes).decode('utf-8')}", + "ownerID": { + "value": f"{json_transformers.encode_for_json(owner_wallet.address)}" + }, + "lifetime": { + "exp": f"{lifetime.exp}", + "nbf": f"{lifetime.nbf}", + "iat": f"{lifetime.iat}", + }, + "sessionKey": pub_key_64, + } + } + session_token["body"].update(session) + + logger.info(f"Got this Session Token: {session_token}") + with open(file_path, "w", encoding="utf-8") as session_token_file: + json.dump(session_token, session_token_file, ensure_ascii=False, indent=4) + + return file_path + + +@allure.step("Generate Session Token For Container") +def generate_container_session_token( + owner_wallet: NodeWallet, + session_wallet: NodeWallet, + verb: ContainerVerb, + tokens_dir: str, + lifetime: Optional[Lifetime] = None, + cid: Optional[str] = None, +) -> str: + """ + This function generates session token for ContainerSessionContext + and writes it to the file. It is able to prepare session token file + for a specific container () or for every container (adds + "wildcard" field). + Args: + owner_wallet: wallet of container owner. + session_wallet: wallet to which we grant the access via session token. + verb: verb to grant access to. + lifetime: lifetime options for session. + cid: container ID of the container + Returns: + The path to the generated session token file + """ + session = { + "container": { + "verb": verb.value, + "wildcard": cid is None, + **({"containerID": {"value": f"{encode_for_json(cid)}"}} if cid is not None else {}), + }, + } + + return generate_session_token( + owner_wallet=owner_wallet, + session_wallet=session_wallet, + session=session, + tokens_dir=tokens_dir, + lifetime=lifetime, + ) + + +@allure.step("Generate Session Token For Object") +def generate_object_session_token( + owner_wallet: NodeWallet, + session_wallet: NodeWallet, + oids: list[str], + cid: str, + verb: ObjectVerb, + tokens_dir: str, + lifetime: Optional[Lifetime] = None, +) -> str: + """ + This function generates session token for ObjectSessionContext + and writes it to the file. + Args: + owner_wallet: wallet of container owner + session_wallet: wallet to which we grant the access via session token + cid: container ID of the container + oids: list of objectIDs to put into session + verb: verb to grant access to; Valid verbs are: ObjectVerb. + lifetime: lifetime options for session + Returns: + The path to the generated session token file + """ + session = { + "object": { + "verb": verb.value, + "target": { + "container": {"value": encode_for_json(cid)}, + "objects": [{"value": encode_for_json(oid)} for oid in oids], + }, + }, + } + + return generate_session_token( + owner_wallet=owner_wallet, + session_wallet=session_wallet, + session=session, + tokens_dir=tokens_dir, + lifetime=lifetime, + ) + + +@allure.step("Get signed token for container session") +def get_container_signed_token( + owner_wallet: NodeWallet, + user_wallet: NodeWallet, + verb: ContainerVerb, + shell: Shell, + tokens_dir: str, + lifetime: Optional[Lifetime] = None, +) -> str: + """ + Returns signed token file path for static container session + """ + session_token_file = generate_container_session_token( + owner_wallet=owner_wallet, + session_wallet=user_wallet, + verb=verb, + tokens_dir=tokens_dir, + lifetime=lifetime, + ) + return sign_session_token(shell, session_token_file, owner_wallet) + + +@allure.step("Get signed token for object session") +def get_object_signed_token( + owner_wallet: NodeWallet, + user_wallet: NodeWallet, + cid: str, + storage_objects: list[StorageObjectInfo], + verb: ObjectVerb, + shell: Shell, + tokens_dir: str, + lifetime: Optional[Lifetime] = None, +) -> str: + """ + Returns signed token file path for static object session + """ + storage_object_ids = [storage_object.oid for storage_object in storage_objects] + session_token_file = generate_object_session_token( + owner_wallet=owner_wallet, + session_wallet=user_wallet, + oids=storage_object_ids, + cid=cid, + verb=verb, + tokens_dir=tokens_dir, + lifetime=lifetime, + ) + return sign_session_token(shell, session_token_file, owner_wallet) + + +@allure.step("Create Session Token") +def create_session_token( + shell: Shell, + owner: str, + wallet_path: str, + wallet_password: str, + rpc_endpoint: str, + lifetime: Optional[int] = None, + expire_at: Optional[int] = None, +) -> str: + """ + Create session token for an object. + Args: + shell: Shell instance. + owner: User that writes the token. + wallet_path: The path to wallet to which we grant the access via session token. + wallet_password: Wallet password. + rpc_endpoint: Remote node address (as 'multiaddr' or ':'). + Returns: + The path to the generated session token file. + """ + session_token = os.path.join(os.getcwd(), ASSETS_DIR, TEST_FILES_DIR, str(uuid.uuid4())) + neofscli = NeofsCli(shell=shell, neofs_cli_exec_path=NEOFS_CLI_EXEC) + neofscli.session.create( + rpc_endpoint=rpc_endpoint, + address=owner, + wallet=wallet_path, + wallet_password=wallet_password, + out=session_token, + lifetime=lifetime, + expire_at=expire_at, + ) + return session_token + + +@allure.step("Sign Session Token") +def sign_session_token(shell: Shell, session_token_file: str, wlt: NodeWallet) -> str: + """ + This function signs the session token by the given wallet. + + Args: + shell: Shell instance. + session_token_file: The path to the session token file. + wlt: The path to the signing wallet. + + Returns: + The path to the signed token. + """ + signed_token_file = os.path.join(os.getcwd(), ASSETS_DIR, TEST_FILES_DIR, str(uuid.uuid4())) + neofscli = NeofsCli(shell=shell, neofs_cli_exec_path=NEOFS_CLI_EXEC, config_file=WALLET_CONFIG) + neofscli.util.sign_session_token( + wallet=wlt.path, from_file=session_token_file, to_file=signed_token_file + ) + return signed_token_file diff --git a/dynamic_env_pytest_tests/lib/helpers/storage_object_info.py b/dynamic_env_pytest_tests/lib/helpers/storage_object_info.py index dd46740e1..d686e13e8 100644 --- a/dynamic_env_pytest_tests/lib/helpers/storage_object_info.py +++ b/dynamic_env_pytest_tests/lib/helpers/storage_object_info.py @@ -1,6 +1,24 @@ +import json +import logging from dataclasses import dataclass +from time import sleep from typing import Optional +import allure +import neofs_env.neofs_epoch as neofs_epoch +import pytest +from common import WALLET_PASS +from grpc_responses import OBJECT_ALREADY_REMOVED +from neo3.wallet import wallet +from neofs_testlib.env.env import NeoFSEnv +from neofs_testlib.shell import Shell +from neofs_verbs import head_object +from python_keywords.neofs_verbs import delete_object, get_object + +logger = logging.getLogger("NeoLogger") + +CLEANUP_TIMEOUT = 10 + @dataclass class ObjectRef: @@ -23,3 +41,80 @@ class StorageObjectInfo(ObjectRef): attributes: Optional[list[dict[str, str]]] = None tombstone: Optional[str] = None locks: Optional[list[LockObjectInfo]] = None + + +@allure.step("Verify Head Tombstone") +def verify_head_tombstone( + wallet_path: str, cid: str, oid_ts: str, oid: str, shell: Shell, endpoint: str +): + header = head_object(wallet_path, cid, oid_ts, shell=shell, endpoint=endpoint)["header"] + + s_oid = header["sessionToken"]["body"]["object"]["target"]["objects"] + logger.info(f"Header Session OIDs is {s_oid}") + logger.info(f"OID is {oid}") + + assert header["containerID"] == cid, "Tombstone Header CID is wrong" + + with open(wallet_path, "r") as file: + wlt_data = json.loads(file.read()) + wlt = wallet.Wallet.from_json(wlt_data, passwords=[WALLET_PASS]) + addr = wlt.accounts[0].address + + assert header["ownerID"] == addr, "Tombstone Owner ID is wrong" + assert header["objectType"] == "TOMBSTONE", "Header Type isn't Tombstone" + assert ( + header["sessionToken"]["body"]["object"]["verb"] == "DELETE" + ), "Header Session Type isn't DELETE" + assert ( + header["sessionToken"]["body"]["object"]["target"]["container"] == cid + ), "Header Session ID is wrong" + assert ( + oid in header["sessionToken"]["body"]["object"]["target"]["objects"] + ), "Header Session OID is wrong" + + +@allure.step("Delete Objects") +def delete_objects( + storage_objects: list[StorageObjectInfo], shell: Shell, neofs_env: NeoFSEnv +) -> None: + """ + Deletes given storage objects. + + Args: + storage_objects: list of objects to delete + shell: executor for cli command + """ + + with allure.step("Delete objects"): + for storage_object in storage_objects: + storage_object.tombstone = delete_object( + storage_object.wallet_file_path, + storage_object.cid, + storage_object.oid, + shell=shell, + endpoint=neofs_env.sn_rpc, + ) + verify_head_tombstone( + wallet_path=storage_object.wallet_file_path, + cid=storage_object.cid, + oid_ts=storage_object.tombstone, + oid=storage_object.oid, + shell=shell, + endpoint=neofs_env.sn_rpc, + ) + + current_epoch = neofs_epoch.get_epoch(neofs_env) + neofs_epoch.tick_epoch(neofs_env) + neofs_epoch.wait_for_epochs_align(neofs_env, current_epoch) + sleep(CLEANUP_TIMEOUT) + + with allure.step("Get objects and check errors"): + for storage_object in storage_objects: + with pytest.raises(Exception, match=OBJECT_ALREADY_REMOVED): + get_object( + storage_object.wallet_file_path, + storage_object.cid, + storage_object.oid, + shell=shell, + endpoint=neofs_env.sn_rpc, + ) diff --git a/dynamic_env_pytest_tests/lib/helpers/wallet_helpers.py b/dynamic_env_pytest_tests/lib/helpers/wallet_helpers.py new file mode 100644 index 000000000..039deb87a --- /dev/null +++ b/dynamic_env_pytest_tests/lib/helpers/wallet_helpers.py @@ -0,0 +1,24 @@ +import os +import uuid +from typing import Optional + +import allure +from common import ASSETS_DIR +from neofs_testlib.env.env import NodeWallet +from neofs_testlib.utils.wallet import init_wallet + + +@allure.title("Prepare wallet and deposit") +def create_wallet(name: Optional[str] = None) -> NodeWallet: + if name is None: + wallet_name = f"{str(uuid.uuid4())}.json" + else: + wallet_name = f"{name}.json" + + wallet_path = os.path.join(os.getcwd(), ASSETS_DIR, wallet_name) + wallet_password = "password" + wallet_address = init_wallet(wallet_path, wallet_password) + + allure.attach.file(wallet_path, os.path.basename(wallet_path), allure.attachment_type.JSON) + + return NodeWallet(path=wallet_path, address=wallet_address, password=wallet_password) diff --git a/dynamic_env_pytest_tests/tests/conftest.py b/dynamic_env_pytest_tests/tests/conftest.py index dbc6319e6..a0ed9183b 100644 --- a/dynamic_env_pytest_tests/tests/conftest.py +++ b/dynamic_env_pytest_tests/tests/conftest.py @@ -1,8 +1,6 @@ import os import shutil -import uuid import time -from typing import Optional import allure import pytest @@ -14,11 +12,12 @@ TEST_FILES_DIR, TEST_OBJECTS_DIR, ) -from neofs_testlib.env.env import NeoFSEnv, NodeWallet +from neofs_testlib.env.env import NeoFSEnv from neofs_testlib.shell import Shell -from neofs_testlib.utils.wallet import init_wallet from python_keywords.neofs_verbs import get_netmap_netinfo +from helpers.wallet_helpers import create_wallet + def pytest_addoption(parser): parser.addoption( @@ -48,10 +47,10 @@ def neofs_env(request): else: if not request.config.getoption("--load-env"): neofs_env.kill() - + logs_path = os.path.join(os.getcwd(), ASSETS_DIR, "logs") os.makedirs(logs_path, exist_ok=True) - + shutil.copyfile(neofs_env.s3_gw.stderr, f"{logs_path}/s3_gw_log.txt") shutil.copyfile(neofs_env.http_gw.stderr, f"{logs_path}/http_gw_log.txt") for idx, ir in enumerate(neofs_env.inner_ring_nodes): @@ -74,22 +73,6 @@ def client_shell(neofs_env: NeoFSEnv) -> Shell: yield neofs_env.shell -@allure.title("Prepare wallet and deposit") -def create_wallet(name: Optional[str] = None) -> NodeWallet: - if name is None: - wallet_name = f"{str(uuid.uuid4())}.json" - else: - wallet_name = f"{name}.json" - - wallet_path = os.path.join(os.getcwd(), ASSETS_DIR, wallet_name) - wallet_password = "password" - wallet_address = init_wallet(wallet_path, wallet_password) - - allure.attach.file(wallet_path, os.path.basename(wallet_path), allure.attachment_type.JSON) - - return NodeWallet(path=wallet_path, address=wallet_address, password=wallet_password) - - @pytest.fixture(scope="session") def max_object_size(neofs_env: NeoFSEnv, client_shell: Shell) -> int: storage_node = neofs_env.storage_nodes[0] diff --git a/dynamic_env_pytest_tests/tests/session_token/conftest.py b/dynamic_env_pytest_tests/tests/session_token/conftest.py new file mode 100644 index 000000000..64be19e85 --- /dev/null +++ b/dynamic_env_pytest_tests/tests/session_token/conftest.py @@ -0,0 +1,44 @@ +import pytest +from neofs_testlib.env.env import NodeWallet + +from helpers.wallet_helpers import create_wallet + + +@pytest.fixture(scope="module") +def owner_wallet(temp_directory) -> NodeWallet: + """ + Returns wallet which owns containers and objects + """ + return create_wallet() + + +@pytest.fixture(scope="module") +def user_wallet(temp_directory) -> NodeWallet: + """ + Returns wallet which will use objects from owner via static session + """ + return create_wallet() + + +@pytest.fixture(scope="module") +def stranger_wallet(temp_directory) -> NodeWallet: + """ + Returns stranger wallet which should fail to obtain data + """ + return create_wallet() + + +@pytest.fixture(scope="module") +def scammer_wallet(temp_directory) -> NodeWallet: + """ + Returns stranger wallet which should fail to obtain data + """ + return create_wallet() + + +@pytest.fixture(scope="module") +def not_owner_wallet(temp_directory) -> NodeWallet: + """ + Returns stranger wallet which should fail to obtain data + """ + return create_wallet() diff --git a/dynamic_env_pytest_tests/tests/session_token/test_object_session_token.py b/dynamic_env_pytest_tests/tests/session_token/test_object_session_token.py new file mode 100644 index 000000000..05bda37c2 --- /dev/null +++ b/dynamic_env_pytest_tests/tests/session_token/test_object_session_token.py @@ -0,0 +1,215 @@ +import random + +import allure +import neofs_env.neofs_epoch as neofs_epoch +import pytest +from file_helper import generate_file +from grpc_responses import EXPIRED_SESSION_TOKEN, SESSION_NOT_FOUND +from neofs_env.neofs_env_test_base import NeofsEnvTestBase +from neofs_testlib.utils.wallet import get_last_address_from_wallet +from python_keywords.container import create_container +from python_keywords.neofs_verbs import delete_object, put_object, put_object_to_random_node + +from helpers.session_token import create_session_token + + +@pytest.mark.session_token +class TestDynamicObjectSession(NeofsEnvTestBase): + @allure.title("Test Object Operations with Session Token") + @pytest.mark.parametrize( + "object_size", + [pytest.lazy_fixture("simple_object_size"), pytest.lazy_fixture("complex_object_size")], + ids=["simple object", "complex object"], + ) + def test_object_session_token(self, default_wallet, object_size): + """ + Test how operations over objects are executed with a session token + + Steps: + 1. Create a private container + 2. Obj operation requests to the node which IS NOT in the container but granted + with a session token + 3. Obj operation requests to the node which IS in the container and NOT granted + with a session token + 4. Obj operation requests to the node which IS NOT in the container and NOT granted + with a session token + """ + + with allure.step("Init wallet"): + wallet = default_wallet + address = get_last_address_from_wallet(wallet.path, wallet.password) + + with allure.step("Nodes Settlements"): + ( + session_token_node, + container_node, + non_container_node, + ) = random.sample(self.neofs_env.storage_nodes, 3) + + with allure.step("Create Session Token"): + session_token = create_session_token( + shell=self.shell, + owner=address, + wallet_path=wallet.path, + wallet_password=wallet.password, + rpc_endpoint=session_token_node.endpoint, + ) + + with allure.step("Create Private Container"): + un_locode = container_node.attrs["NEOFS_NODE_ATTRIBUTE_0"].split(":")[1].strip() + locode = "SPB" if un_locode == "RU LED" else un_locode.split()[1] + placement_policy = ( + f"REP 1 IN LOC_{locode}_PLACE CBF 1 SELECT 1 FROM LOC_{locode} " + f'AS LOC_{locode}_PLACE FILTER "UN-LOCODE" ' + f'EQ "{un_locode}" AS LOC_{locode}' + ) + cid = create_container( + wallet.path, + shell=self.shell, + endpoint=self.neofs_env.sn_rpc, + rule=placement_policy, + ) + + with allure.step("Put Objects"): + file_path = generate_file(object_size) + oid = put_object_to_random_node( + wallet=wallet.path, + path=file_path, + cid=cid, + shell=self.shell, + neofs_env=self.neofs_env, + ) + oid_delete = put_object_to_random_node( + wallet=wallet.path, + path=file_path, + cid=cid, + shell=self.shell, + neofs_env=self.neofs_env, + ) + + with allure.step("Node not in container but granted a session token"): + put_object( + wallet=wallet.path, + path=file_path, + cid=cid, + shell=self.shell, + endpoint=session_token_node.endpoint, + session=session_token, + ) + delete_object( + wallet=wallet.path, + cid=cid, + oid=oid_delete, + shell=self.shell, + endpoint=session_token_node.endpoint, + session=session_token, + ) + + with allure.step("Node in container and not granted a session token"): + with pytest.raises(Exception, match=SESSION_NOT_FOUND): + put_object( + wallet=wallet.path, + path=file_path, + cid=cid, + shell=self.shell, + endpoint=container_node.endpoint, + session=session_token, + ) + with pytest.raises(Exception, match=SESSION_NOT_FOUND): + delete_object( + wallet=wallet.path, + cid=cid, + oid=oid, + shell=self.shell, + endpoint=container_node.endpoint, + session=session_token, + ) + + with allure.step("Node not in container and not granted a session token"): + with pytest.raises(Exception, match=SESSION_NOT_FOUND): + put_object( + wallet=wallet.path, + path=file_path, + cid=cid, + shell=self.shell, + endpoint=non_container_node.endpoint, + session=session_token, + ) + with pytest.raises(Exception, match=SESSION_NOT_FOUND): + delete_object( + wallet=wallet.path, + cid=cid, + oid=oid, + shell=self.shell, + endpoint=non_container_node.endpoint, + session=session_token, + ) + + @allure.title("Verify session token expiration flags") + @pytest.mark.parametrize("expiration_flag", ["lifetime", "expire_at"]) + def test_session_token_expiration_flags( + self, default_wallet, simple_object_size, expiration_flag + ): + rpc_endpoint = self.neofs_env.storage_nodes[0].endpoint + + with allure.step("Create Session Token with Lifetime param"): + current_epoch = neofs_epoch.get_epoch(self.neofs_env) + + session_token = create_session_token( + shell=self.shell, + owner=get_last_address_from_wallet(default_wallet.path, default_wallet.password), + wallet_path=default_wallet.path, + wallet_password=default_wallet.password, + rpc_endpoint=rpc_endpoint, + lifetime=1 if expiration_flag == "lifetime" else None, + expire_at=current_epoch + 1 if expiration_flag == "expire_at" else None, + ) + + with allure.step("Create Private Container"): + first_node = self.neofs_env.storage_nodes[0] + un_locode = first_node.attrs["NEOFS_NODE_ATTRIBUTE_0"].split(":")[1].strip() + locode = "SPB" if un_locode == "RU LED" else un_locode.split()[1] + placement_policy = ( + f"REP 1 IN LOC_{locode}_PLACE CBF 1 SELECT 1 FROM LOC_{locode} " + f'AS LOC_{locode}_PLACE FILTER "UN-LOCODE" ' + f'EQ "{un_locode}" AS LOC_{locode}' + ) + cid = create_container( + default_wallet.path, + shell=self.shell, + endpoint=self.neofs_env.sn_rpc, + rule=placement_policy, + ) + + with allure.step("Verify object operations with created session token are allowed"): + file_path = generate_file(simple_object_size) + oid = put_object( + wallet=default_wallet.path, + path=file_path, + cid=cid, + shell=self.shell, + endpoint=rpc_endpoint, + session=session_token, + ) + delete_object( + wallet=default_wallet.path, + cid=cid, + oid=oid, + shell=self.shell, + endpoint=rpc_endpoint, + session=session_token, + ) + + self.tick_epochs_and_wait(2) + + with allure.step("Verify object operations with created session token are not allowed"): + file_path = generate_file(simple_object_size) + with pytest.raises(RuntimeError, match=EXPIRED_SESSION_TOKEN): + oid = put_object( + wallet=default_wallet.path, + path=file_path, + cid=cid, + shell=self.shell, + endpoint=rpc_endpoint, + session=session_token, + ) diff --git a/dynamic_env_pytest_tests/tests/session_token/test_static_object_session_token.py b/dynamic_env_pytest_tests/tests/session_token/test_static_object_session_token.py new file mode 100644 index 000000000..ffc3e8cde --- /dev/null +++ b/dynamic_env_pytest_tests/tests/session_token/test_static_object_session_token.py @@ -0,0 +1,726 @@ +import logging + +import allure +import neofs_env.neofs_epoch as neofs_epoch +import pytest +from file_helper import generate_file +from grpc_responses import ( + EXPIRED_SESSION_TOKEN, + MALFORMED_REQUEST, + OBJECT_ACCESS_DENIED, + OBJECT_NOT_FOUND, +) +from neofs_env.neofs_env_test_base import NeofsEnvTestBase +from neofs_testlib.env.env import NeoFSEnv, NodeWallet +from neofs_testlib.shell import Shell +from pytest import FixtureRequest +from python_keywords.container import create_container +from python_keywords.neofs_verbs import ( + delete_object, + get_object, + get_object_from_random_node, + get_range, + get_range_hash, + head_object, + put_object_to_random_node, + search_object, +) +from test_control import expect_not_raises + +from helpers.session_token import ( + INVALID_SIGNATURE, + UNRELATED_CONTAINER, + UNRELATED_KEY, + UNRELATED_OBJECT, + WRONG_VERB, + Lifetime, + ObjectVerb, + generate_object_session_token, + get_object_signed_token, + sign_session_token, +) +from helpers.storage_object_info import StorageObjectInfo, delete_objects + +logger = logging.getLogger("NeoLogger") + +RANGE_OFFSET_FOR_COMPLEX_OBJECT = 200 + + +@pytest.fixture(scope="module") +def storage_containers( + owner_wallet: NodeWallet, client_shell: Shell, neofs_env: NeoFSEnv +) -> list[str]: + cid = create_container(owner_wallet.path, shell=client_shell, endpoint=neofs_env.sn_rpc) + other_cid = create_container(owner_wallet.path, shell=client_shell, endpoint=neofs_env.sn_rpc) + yield [cid, other_cid] + + +@pytest.fixture( + params=[pytest.lazy_fixture("simple_object_size"), pytest.lazy_fixture("complex_object_size")], + ids=["simple object", "complex object"], + # Scope module to upload/delete each files set only once + scope="module", +) +def storage_objects( + owner_wallet: NodeWallet, + client_shell: Shell, + storage_containers: list[str], + neofs_env: NeoFSEnv, + request: FixtureRequest, +) -> list[StorageObjectInfo]: + file_path = generate_file(request.param) + storage_objects = [] + + with allure.step("Put objects"): + # upload couple objects + for _ in range(3): + storage_object_id = put_object_to_random_node( + wallet=owner_wallet.path, + path=file_path, + cid=storage_containers[0], + shell=client_shell, + neofs_env=neofs_env, + ) + + storage_object = StorageObjectInfo(storage_containers[0], storage_object_id) + storage_object.size = request.param + storage_object.wallet_file_path = owner_wallet.path + storage_object.file_path = file_path + storage_objects.append(storage_object) + + yield storage_objects + + # Teardown after all tests done with current param + delete_objects(storage_objects, client_shell, neofs_env) + + +@allure.step("Get ranges for test") +def get_ranges( + storage_object: StorageObjectInfo, max_object_size: int, shell: Shell, endpoint: str +) -> list[str]: + """ + Returns ranges to test range/hash methods via static session + """ + object_size = storage_object.size + + if object_size > max_object_size: + assert object_size >= max_object_size + RANGE_OFFSET_FOR_COMPLEX_OBJECT + return [ + "0:10", + f"{object_size-10}:10", + f"{max_object_size - RANGE_OFFSET_FOR_COMPLEX_OBJECT}:" + f"{RANGE_OFFSET_FOR_COMPLEX_OBJECT * 2}", + ] + else: + return ["0:10", f"{object_size-10}:10"] + + +@pytest.fixture(scope="module") +def static_sessions( + owner_wallet: NodeWallet, + user_wallet: NodeWallet, + storage_containers: list[str], + storage_objects: list[StorageObjectInfo], + client_shell: Shell, + temp_directory: str, +) -> dict[ObjectVerb, str]: + """ + Returns dict with static session token file paths for all verbs with default lifetime with + valid container and first two objects + """ + return { + verb: get_object_signed_token( + owner_wallet, + user_wallet, + storage_containers[0], + storage_objects[0:2], + verb, + client_shell, + temp_directory, + ) + for verb in ObjectVerb + } + + +@pytest.mark.static_session +class TestObjectStaticSession(NeofsEnvTestBase): + @allure.title("Validate static session with read operations") + @pytest.mark.parametrize( + "method_under_test,verb", + [ + (head_object, ObjectVerb.HEAD), + (get_object, ObjectVerb.GET), + ], + ) + def test_static_session_read( + self, + user_wallet: NodeWallet, + storage_objects: list[StorageObjectInfo], + static_sessions: dict[ObjectVerb, str], + method_under_test, + verb: ObjectVerb, + request: FixtureRequest, + ): + """ + Validate static session with read operations + """ + allure.dynamic.title( + f"Validate static session with read operations for {request.node.callspec.id}" + ) + + for node in self.neofs_env.storage_nodes: + for storage_object in storage_objects[0:2]: + method_under_test( + wallet=user_wallet.path, + cid=storage_object.cid, + oid=storage_object.oid, + shell=self.shell, + endpoint=node.endpoint, + session=static_sessions[verb], + ) + + @allure.title("Validate static session with range operations") + @pytest.mark.static_session + @pytest.mark.parametrize( + "method_under_test,verb", + [(get_range, ObjectVerb.RANGE), (get_range_hash, ObjectVerb.RANGEHASH)], + ) + def test_static_session_range( + self, + user_wallet: NodeWallet, + storage_objects: list[StorageObjectInfo], + static_sessions: dict[ObjectVerb, str], + method_under_test, + verb: ObjectVerb, + request: FixtureRequest, + max_object_size, + ): + """ + Validate static session with range operations + """ + allure.dynamic.title( + f"Validate static session with range operations for {request.node.callspec.id}" + ) + storage_object = storage_objects[0] + ranges_to_test = get_ranges( + storage_object, max_object_size, self.shell, self.neofs_env.sn_rpc + ) + + for range_to_test in ranges_to_test: + with allure.step(f"Check range {range_to_test}"): + with expect_not_raises(): + method_under_test( + user_wallet.path, + storage_object.cid, + storage_object.oid, + shell=self.shell, + endpoint=self.neofs_env.sn_rpc, + session=static_sessions[verb], + range_cut=range_to_test, + ) + + @allure.title("Validate static session with search operation") + @pytest.mark.static_session + def test_static_session_search( + self, + user_wallet: NodeWallet, + storage_objects: list[StorageObjectInfo], + static_sessions: dict[ObjectVerb, str], + request: FixtureRequest, + ): + """ + Validate static session with search operations + """ + allure.dynamic.title(f"Validate static session with search for {request.node.callspec.id}") + + cid = storage_objects[0].cid + expected_object_ids = [storage_object.oid for storage_object in storage_objects] + actual_object_ids = search_object( + user_wallet.path, + cid, + self.shell, + endpoint=self.neofs_env.sn_rpc, + session=static_sessions[ObjectVerb.SEARCH], + root=True, + ) + assert set(expected_object_ids) == set(actual_object_ids) + + @allure.title("Validate static session with object id not in session") + @pytest.mark.static_session + def test_static_session_unrelated_object( + self, + user_wallet: NodeWallet, + storage_objects: list[StorageObjectInfo], + static_sessions: dict[ObjectVerb, str], + request: FixtureRequest, + ): + """ + Validate static session with object id not in session + """ + allure.dynamic.title( + f"Validate static session with object id not in session for {request.node.callspec.id}" + ) + with pytest.raises(Exception, match=UNRELATED_OBJECT): + head_object( + user_wallet.path, + storage_objects[2].cid, + storage_objects[2].oid, + self.shell, + self.neofs_env.sn_rpc, + session=static_sessions[ObjectVerb.HEAD], + ) + + @allure.title("Validate static session with user id not in session") + @pytest.mark.static_session + def test_static_session_head_unrelated_user( + self, + stranger_wallet: NodeWallet, + storage_objects: list[StorageObjectInfo], + static_sessions: dict[ObjectVerb, str], + request: FixtureRequest, + ): + """ + Validate static session with user id not in session + """ + allure.dynamic.title( + f"Validate static session with user id not in session for {request.node.callspec.id}" + ) + storage_object = storage_objects[0] + + with pytest.raises(Exception, match=UNRELATED_KEY): + head_object( + stranger_wallet.path, + storage_object.cid, + storage_object.oid, + self.shell, + self.neofs_env.sn_rpc, + session=static_sessions[ObjectVerb.HEAD], + ) + + @allure.title("Validate static session with wrong verb in session") + @pytest.mark.static_session + def test_static_session_head_wrong_verb( + self, + user_wallet: NodeWallet, + storage_objects: list[StorageObjectInfo], + static_sessions: dict[ObjectVerb, str], + request: FixtureRequest, + ): + """ + Validate static session with wrong verb in session + """ + allure.dynamic.title( + f"Validate static session with wrong verb in session for {request.node.callspec.id}" + ) + storage_object = storage_objects[0] + + with pytest.raises(Exception, match=WRONG_VERB): + get_object( + user_wallet.path, + storage_object.cid, + storage_object.oid, + self.shell, + self.neofs_env.sn_rpc, + session=static_sessions[ObjectVerb.HEAD], + ) + + @allure.title("Validate static session with container id not in session") + @pytest.mark.static_session + def test_static_session_unrelated_container( + self, + user_wallet: NodeWallet, + storage_objects: list[StorageObjectInfo], + storage_containers: list[str], + static_sessions: dict[ObjectVerb, str], + request: FixtureRequest, + ): + """ + Validate static session with container id not in session + """ + allure.dynamic.title( + f"Validate static session with container id not in session for {request.node.callspec.id}" + ) + storage_object = storage_objects[0] + + with pytest.raises(Exception, match=UNRELATED_CONTAINER): + get_object_from_random_node( + user_wallet.path, + storage_containers[1], + storage_object.oid, + self.shell, + neofs_env=self.neofs_env, + session=static_sessions[ObjectVerb.GET], + ) + + @allure.title("Validate static session which signed by another wallet") + @pytest.mark.static_session + def test_static_session_signed_by_other( + self, + owner_wallet: NodeWallet, + user_wallet: NodeWallet, + stranger_wallet: NodeWallet, + storage_containers: list[str], + storage_objects: list[StorageObjectInfo], + temp_directory: str, + request: FixtureRequest, + ): + """ + Validate static session which signed by another wallet + """ + allure.dynamic.title( + f"Validate static session which signed by another wallet for {request.node.callspec.id}" + ) + storage_object = storage_objects[0] + + session_token_file = generate_object_session_token( + owner_wallet, + user_wallet, + [storage_object.oid], + storage_containers[0], + ObjectVerb.HEAD, + temp_directory, + ) + signed_token_file = sign_session_token(self.shell, session_token_file, stranger_wallet) + with pytest.raises(Exception, match=MALFORMED_REQUEST): + head_object( + user_wallet.path, + storage_object.cid, + storage_object.oid, + self.shell, + self.neofs_env.sn_rpc, + session=signed_token_file, + ) + + @allure.title("Validate static session which signed for another container") + @pytest.mark.static_session + def test_static_session_signed_for_other_container( + self, + owner_wallet: NodeWallet, + user_wallet: NodeWallet, + storage_containers: list[str], + storage_objects: list[StorageObjectInfo], + temp_directory: str, + request: FixtureRequest, + ): + """ + Validate static session which signed for another container + """ + allure.dynamic.title( + f"Validate static session which signed for another container for {request.node.callspec.id}" + ) + storage_object = storage_objects[0] + container = storage_containers[1] + + session_token_file = generate_object_session_token( + owner_wallet, + user_wallet, + [storage_object.oid], + container, + ObjectVerb.HEAD, + temp_directory, + ) + signed_token_file = sign_session_token(self.shell, session_token_file, owner_wallet) + with pytest.raises(Exception, match=OBJECT_NOT_FOUND): + head_object( + user_wallet.path, + container, + storage_object.oid, + self.shell, + self.neofs_env.sn_rpc, + session=signed_token_file, + ) + + @allure.title("Validate static session which wasn't signed") + @pytest.mark.static_session + def test_static_session_without_sign( + self, + owner_wallet: NodeWallet, + user_wallet: NodeWallet, + storage_containers: list[str], + storage_objects: list[StorageObjectInfo], + temp_directory: str, + request: FixtureRequest, + ): + """ + Validate static session which wasn't signed + """ + allure.dynamic.title( + f"Validate static session which wasn't signed for {request.node.callspec.id}" + ) + storage_object = storage_objects[0] + + session_token_file = generate_object_session_token( + owner_wallet, + user_wallet, + [storage_object.oid], + storage_containers[0], + ObjectVerb.HEAD, + temp_directory, + ) + with pytest.raises(Exception, match=INVALID_SIGNATURE): + head_object( + user_wallet.path, + storage_object.cid, + storage_object.oid, + self.shell, + self.neofs_env.sn_rpc, + session=session_token_file, + ) + + @allure.title("Validate static session which expires at next epoch") + @pytest.mark.static_session + def test_static_session_expiration_at_next( + self, + owner_wallet: NodeWallet, + user_wallet: NodeWallet, + storage_containers: list[str], + storage_objects: list[StorageObjectInfo], + temp_directory: str, + request: FixtureRequest, + ): + """ + Validate static session which expires at next epoch + """ + allure.dynamic.title( + f"Validate static session which expires at next epoch for {request.node.callspec.id}" + ) + epoch = neofs_epoch.ensure_fresh_epoch(self.neofs_env) + + container = storage_containers[0] + object_id = storage_objects[0].oid + expiration = Lifetime(epoch + 1, epoch, epoch) + + token_expire_at_next_epoch = get_object_signed_token( + owner_wallet, + user_wallet, + container, + storage_objects, + ObjectVerb.HEAD, + self.shell, + temp_directory, + expiration, + ) + + head_object( + user_wallet.path, + container, + object_id, + self.shell, + self.neofs_env.sn_rpc, + session=token_expire_at_next_epoch, + ) + + self.tick_epochs_and_wait(2) + + with pytest.raises(Exception, match=EXPIRED_SESSION_TOKEN): + head_object( + user_wallet.path, + container, + object_id, + self.shell, + self.neofs_env.sn_rpc, + session=token_expire_at_next_epoch, + ) + + @allure.title("Validate static session which is valid starting from next epoch") + @pytest.mark.static_session + def test_static_session_start_at_next( + self, + owner_wallet: NodeWallet, + user_wallet: NodeWallet, + storage_containers: list[str], + storage_objects: list[StorageObjectInfo], + temp_directory: str, + request: FixtureRequest, + ): + """ + Validate static session which is valid starting from next epoch + """ + allure.dynamic.title( + f"Validate static session which is valid starting from next epoch for {request.node.callspec.id}" + ) + epoch = neofs_epoch.ensure_fresh_epoch(self.neofs_env) + + container = storage_containers[0] + object_id = storage_objects[0].oid + expiration = Lifetime(epoch + 2, epoch + 1, epoch) + + token_start_at_next_epoch = get_object_signed_token( + owner_wallet, + user_wallet, + container, + storage_objects, + ObjectVerb.HEAD, + self.shell, + temp_directory, + expiration, + ) + + with pytest.raises(Exception, match=MALFORMED_REQUEST): + head_object( + user_wallet.path, + container, + object_id, + self.shell, + self.neofs_env.sn_rpc, + session=token_start_at_next_epoch, + ) + + self.tick_epochs_and_wait(1) + head_object( + user_wallet.path, + container, + object_id, + self.shell, + self.neofs_env.sn_rpc, + session=token_start_at_next_epoch, + ) + + self.tick_epochs_and_wait(2) + with pytest.raises(Exception, match=EXPIRED_SESSION_TOKEN): + head_object( + user_wallet.path, + container, + object_id, + self.shell, + self.neofs_env.sn_rpc, + session=token_start_at_next_epoch, + ) + + @allure.title("Validate static session which is already expired") + @pytest.mark.static_session + def test_static_session_already_expired( + self, + owner_wallet: NodeWallet, + user_wallet: NodeWallet, + storage_containers: list[str], + storage_objects: list[StorageObjectInfo], + temp_directory: str, + request: FixtureRequest, + ): + """ + Validate static session which is already expired + """ + allure.dynamic.title( + f"Validate static session which is already expired for {request.node.callspec.id}" + ) + epoch = neofs_epoch.ensure_fresh_epoch(self.neofs_env) + + container = storage_containers[0] + object_id = storage_objects[0].oid + expiration = Lifetime(epoch - 1, epoch - 2, epoch - 2) + + token_already_expired = get_object_signed_token( + owner_wallet, + user_wallet, + container, + storage_objects, + ObjectVerb.HEAD, + self.shell, + temp_directory, + expiration, + ) + + with pytest.raises(Exception, match=EXPIRED_SESSION_TOKEN): + head_object( + user_wallet.path, + container, + object_id, + self.shell, + self.neofs_env.sn_rpc, + session=token_already_expired, + ) + + @allure.title("Delete verb should be restricted for static session") + def test_static_session_delete_verb( + self, + user_wallet: NodeWallet, + storage_objects: list[StorageObjectInfo], + static_sessions: dict[ObjectVerb, str], + request: FixtureRequest, + ): + """ + Delete verb should be restricted for static session + """ + allure.dynamic.title( + f"Delete verb should be restricted for static session for {request.node.callspec.id}" + ) + storage_object = storage_objects[0] + with pytest.raises(Exception, match=OBJECT_ACCESS_DENIED): + delete_object( + user_wallet.path, + storage_object.cid, + storage_object.oid, + self.shell, + endpoint=self.neofs_env.sn_rpc, + session=static_sessions[ObjectVerb.DELETE], + ) + + @allure.title("Put verb should be restricted for static session") + def test_static_session_put_verb( + self, + user_wallet: NodeWallet, + storage_objects: list[StorageObjectInfo], + static_sessions: dict[ObjectVerb, str], + request: FixtureRequest, + ): + """ + Put verb should be restricted for static session + """ + allure.dynamic.title( + f"Put verb should be restricted for static session for {request.node.callspec.id}" + ) + storage_object = storage_objects[0] + with pytest.raises(Exception, match=MALFORMED_REQUEST): + put_object_to_random_node( + user_wallet.path, + storage_object.file_path, + storage_object.cid, + self.shell, + neofs_env=self.neofs_env, + session=static_sessions[ObjectVerb.PUT], + ) + + @allure.title("Validate static session which is issued in future epoch") + @pytest.mark.static_session + def test_static_session_invalid_issued_epoch( + self, + owner_wallet: NodeWallet, + user_wallet: NodeWallet, + storage_containers: list[str], + storage_objects: list[StorageObjectInfo], + temp_directory: str, + request: FixtureRequest, + ): + """ + Validate static session which is issued in future epoch + """ + allure.dynamic.title( + f"Validate static session which is issued in future epoch for {request.node.callspec.id}" + ) + epoch = neofs_epoch.ensure_fresh_epoch(self.neofs_env) + + container = storage_containers[0] + object_id = storage_objects[0].oid + expiration = Lifetime(epoch + 10, 0, epoch + 1) + + token_invalid_issue_time = get_object_signed_token( + owner_wallet, + user_wallet, + container, + storage_objects, + ObjectVerb.HEAD, + self.shell, + temp_directory, + expiration, + ) + + with pytest.raises(Exception, match=MALFORMED_REQUEST): + head_object( + user_wallet.path, + container, + object_id, + self.shell, + self.neofs_env.sn_rpc, + session=token_invalid_issue_time, + ) diff --git a/dynamic_env_pytest_tests/tests/session_token/test_static_session_token_container.py b/dynamic_env_pytest_tests/tests/session_token/test_static_session_token_container.py new file mode 100644 index 000000000..35c5da134 --- /dev/null +++ b/dynamic_env_pytest_tests/tests/session_token/test_static_session_token_container.py @@ -0,0 +1,375 @@ +import allure +import pytest +from file_helper import generate_file +from grpc_responses import CONTAINER_DELETION_TIMED_OUT, EACL_TIMED_OUT, NOT_SESSION_CONTAINER_OWNER +from neofs_env.neofs_env_test_base import NeofsEnvTestBase +from neofs_testlib.env.env import NodeWallet +from neofs_testlib.shell import Shell +from python_keywords.acl import ( + EACLAccess, + EACLOperation, + EACLRole, + EACLRule, + create_eacl, + set_eacl, + wait_for_cache_expired, +) +from python_keywords.container import ( + create_container, + delete_container, + get_container, + list_containers, +) +from python_keywords.object_access import can_put_object +from wellknown_acl import PUBLIC_ACL + +from helpers.session_token import ContainerVerb, get_container_signed_token + + +@pytest.mark.static_session_container +class TestSessionTokenContainer(NeofsEnvTestBase): + @pytest.fixture(scope="module") + def static_sessions( + self, + owner_wallet: NodeWallet, + user_wallet: NodeWallet, + client_shell: Shell, + temp_directory: str, + ) -> dict[ContainerVerb, str]: + """ + Returns dict with static session token file paths for all verbs with default lifetime + """ + return self.static_session_token(owner_wallet, user_wallet, client_shell, temp_directory) + + def static_session_token( + self, + owner_wallet: NodeWallet, + user_wallet: NodeWallet, + client_shell: Shell, + temp_directory: str, + ) -> dict[ContainerVerb, str]: + return { + verb: get_container_signed_token( + owner_wallet, user_wallet, verb, client_shell, temp_directory + ) + for verb in ContainerVerb + } + + def test_static_session_token_container_create( + self, + owner_wallet: NodeWallet, + user_wallet: NodeWallet, + static_sessions: dict[ContainerVerb, str], + ): + """ + Validate static session with create operation + """ + with allure.step("Create container with static session token"): + cid = create_container( + user_wallet.path, + session_token=static_sessions[ContainerVerb.CREATE], + shell=self.shell, + endpoint=self.neofs_env.sn_rpc, + wait_for_creation=False, + ) + + container_info: dict[str, str] = get_container( + owner_wallet.path, cid, shell=self.shell, endpoint=self.neofs_env.sn_rpc + ) + assert container_info["ownerID"] == owner_wallet.address + + assert cid not in list_containers( + user_wallet.path, shell=self.shell, endpoint=self.neofs_env.sn_rpc + ) + assert cid in list_containers( + owner_wallet.path, shell=self.shell, endpoint=self.neofs_env.sn_rpc + ) + + def test_static_session_token_container_create_with_other_verb( + self, + user_wallet: NodeWallet, + static_sessions: dict[ContainerVerb, str], + ): + """ + Validate static session without create operation + """ + with allure.step("Try create container with static session token without PUT rule"): + for verb in [verb for verb in ContainerVerb if verb != ContainerVerb.CREATE]: + with pytest.raises(Exception): + create_container( + user_wallet.path, + session_token=static_sessions[verb], + shell=self.shell, + endpoint=self.neofs_env.sn_rpc, + wait_for_creation=False, + ) + + def test_static_session_token_container_create_with_other_wallet( + self, + stranger_wallet: NodeWallet, + static_sessions: dict[ContainerVerb, str], + ): + """ + Validate static session with create operation for other wallet + """ + with allure.step("Try create container with static session token without PUT rule"): + with pytest.raises(Exception): + create_container( + stranger_wallet.path, + session_token=static_sessions[ContainerVerb.CREATE], + shell=self.shell, + endpoint=self.neofs_env.sn_rpc, + wait_for_creation=False, + ) + + def test_static_session_token_container_delete( + self, + owner_wallet: NodeWallet, + user_wallet: NodeWallet, + static_sessions: dict[ContainerVerb, str], + ): + """ + Validate static session with delete operation + """ + with allure.step("Create container"): + cid = create_container( + owner_wallet.path, + shell=self.shell, + endpoint=self.neofs_env.sn_rpc, + wait_for_creation=False, + ) + with allure.step("Delete container with static session token"): + delete_container( + wallet=user_wallet.path, + cid=cid, + session_token=static_sessions[ContainerVerb.DELETE], + shell=self.shell, + endpoint=self.neofs_env.sn_rpc, + await_mode=True, + ) + + assert cid not in list_containers( + owner_wallet.path, shell=self.shell, endpoint=self.neofs_env.sn_rpc + ) + + @pytest.mark.trusted_party_proved + @allure.title("Not owner user can NOT delete container") + def test_not_owner_user_can_not_delete_container( + self, + owner_wallet: NodeWallet, + user_wallet: NodeWallet, + stranger_wallet: NodeWallet, + scammer_wallet: NodeWallet, + static_sessions: dict[ContainerVerb, str], + temp_directory: str, + not_owner_wallet, + ): + with allure.step("Create container"): + cid = create_container( + owner_wallet.path, + shell=self.shell, + endpoint=self.neofs_env.sn_rpc, + ) + + user_token = self.static_session_token( + owner_wallet, user_wallet, self.shell, temp_directory + ) + stranger_token = self.static_session_token( + user_wallet, stranger_wallet, self.shell, temp_directory + ) + + with allure.step("Try to delete container using stranger token"): + with pytest.raises(RuntimeError, match=NOT_SESSION_CONTAINER_OWNER): + delete_container( + wallet=user_wallet.path, + cid=cid, + session_token=stranger_token[ContainerVerb.DELETE], + shell=self.shell, + endpoint=self.neofs_env.sn_rpc, + await_mode=True, + ) + + with allure.step("Try to force delete container using stranger token"): + with pytest.raises(RuntimeError, match=CONTAINER_DELETION_TIMED_OUT): + delete_container( + wallet=user_wallet.path, + cid=cid, + session_token=stranger_token[ContainerVerb.DELETE], + shell=self.shell, + endpoint=self.neofs_env.sn_rpc, + await_mode=True, + force=True, + ) + + @pytest.mark.trusted_party_proved + @allure.title("Not trusted party user can NOT delete container") + def test_not_trusted_party_user_can_not_delete_container( + self, + owner_wallet: NodeWallet, + user_wallet: NodeWallet, + stranger_wallet: NodeWallet, + scammer_wallet: NodeWallet, + static_sessions: dict[ContainerVerb, str], + temp_directory: str, + not_owner_wallet, + ): + with allure.step("Create container"): + cid = create_container( + owner_wallet.path, + shell=self.shell, + endpoint=self.neofs_env.sn_rpc, + ) + + user_token = self.static_session_token( + owner_wallet, user_wallet, self.shell, temp_directory + ) + stranger_token = self.static_session_token( + user_wallet, stranger_wallet, self.shell, temp_directory + ) + + with allure.step("Try to delete container using scammer token"): + with pytest.raises(RuntimeError, match=CONTAINER_DELETION_TIMED_OUT): + delete_container( + wallet=scammer_wallet.path, + cid=cid, + session_token=user_token[ContainerVerb.DELETE], + shell=self.shell, + endpoint=self.neofs_env.sn_rpc, + await_mode=True, + ) + + with pytest.raises(RuntimeError, match=NOT_SESSION_CONTAINER_OWNER): + delete_container( + wallet=scammer_wallet.path, + cid=cid, + session_token=stranger_token[ContainerVerb.DELETE], + shell=self.shell, + endpoint=self.neofs_env.sn_rpc, + await_mode=True, + ) + + with allure.step("Try to force delete container using scammer token"): + with pytest.raises(RuntimeError, match=CONTAINER_DELETION_TIMED_OUT): + delete_container( + wallet=scammer_wallet.path, + cid=cid, + session_token=user_token[ContainerVerb.DELETE], + shell=self.shell, + endpoint=self.neofs_env.sn_rpc, + await_mode=True, + force=True, + ) + + with pytest.raises(RuntimeError, match=CONTAINER_DELETION_TIMED_OUT): + delete_container( + wallet=scammer_wallet.path, + cid=cid, + session_token=stranger_token[ContainerVerb.DELETE], + shell=self.shell, + endpoint=self.neofs_env.sn_rpc, + await_mode=True, + force=True, + ) + + def test_static_session_token_container_set_eacl( + self, + owner_wallet: NodeWallet, + user_wallet: NodeWallet, + stranger_wallet: NodeWallet, + static_sessions: dict[ContainerVerb, str], + simple_object_size, + ): + """ + Validate static session with set eacl operation + """ + with allure.step("Create container"): + cid = create_container( + owner_wallet.path, + basic_acl=PUBLIC_ACL, + shell=self.shell, + endpoint=self.neofs_env.sn_rpc, + ) + file_path = generate_file(simple_object_size) + assert can_put_object(stranger_wallet.path, cid, file_path, self.shell, neofs_env=self.neofs_env) + + with allure.step(f"Deny all operations for other via eACL"): + eacl_deny = [ + EACLRule(access=EACLAccess.DENY, role=EACLRole.OTHERS, operation=op) + for op in EACLOperation + ] + set_eacl( + user_wallet.path, + cid, + create_eacl(cid, eacl_deny, shell=self.shell), + shell=self.shell, + endpoint=self.neofs_env.sn_rpc, + session_token=static_sessions[ContainerVerb.SETEACL], + ) + wait_for_cache_expired() + + assert not can_put_object(stranger_wallet.path, cid, file_path, self.shell, neofs_env=self.neofs_env) + + @pytest.mark.trusted_party_proved + @allure.title("Not owner and not trusted party can NOT set eacl") + def test_static_session_token_container_set_eacl_only_trusted_party_proved_by_the_container_owner( + self, + owner_wallet: NodeWallet, + user_wallet: NodeWallet, + stranger_wallet: NodeWallet, + scammer_wallet: NodeWallet, + static_sessions: dict[ContainerVerb, str], + temp_directory: str, + not_owner_wallet, + ): + with allure.step("Create container"): + cid = create_container( + owner_wallet.path, + basic_acl=PUBLIC_ACL, + shell=self.shell, + endpoint=self.neofs_env.sn_rpc, + ) + + user_token = self.static_session_token( + owner_wallet, user_wallet, self.shell, temp_directory + ) + stranger_token = self.static_session_token( + user_wallet, stranger_wallet, self.shell, temp_directory + ) + + new_eacl = [ + EACLRule(access=EACLAccess.DENY, role=EACLRole.OTHERS, operation=op) + for op in EACLOperation + ] + + with allure.step(f"Try to deny all operations for other via eACL"): + with pytest.raises(RuntimeError, match=NOT_SESSION_CONTAINER_OWNER): + set_eacl( + user_wallet.path, + cid, + create_eacl(cid, new_eacl, shell=self.shell), + shell=self.shell, + endpoint=self.neofs_env.sn_rpc, + session_token=stranger_token[ContainerVerb.SETEACL], + ) + + with allure.step(f"Try to deny all operations for other via eACL using scammer wallet"): + with allure.step(f"Using user token"): + with pytest.raises(RuntimeError, match=EACL_TIMED_OUT): + set_eacl( + scammer_wallet.path, + cid, + create_eacl(cid, new_eacl, shell=self.shell), + shell=self.shell, + endpoint=self.neofs_env.sn_rpc, + session_token=user_token[ContainerVerb.SETEACL], + ) + with allure.step(f"Using scammer token"): + with pytest.raises(RuntimeError, match=NOT_SESSION_CONTAINER_OWNER): + set_eacl( + scammer_wallet.path, + cid, + create_eacl(cid, new_eacl, shell=self.shell), + shell=self.shell, + endpoint=self.neofs_env.sn_rpc, + session_token=stranger_token[ContainerVerb.SETEACL], + ) diff --git a/robot/resources/lib/python_keywords/object_access.py b/robot/resources/lib/python_keywords/object_access.py index 1b0f0f4ff..beadfc34f 100644 --- a/robot/resources/lib/python_keywords/object_access.py +++ b/robot/resources/lib/python_keywords/object_access.py @@ -5,6 +5,7 @@ from cluster import Cluster from file_helper import get_file_hash from grpc_responses import OBJECT_ACCESS_DENIED, error_matches_status +from neofs_testlib.env.env import NeoFSEnv from neofs_testlib.shell import Shell from python_keywords.neofs_verbs import ( delete_object, @@ -56,7 +57,8 @@ def can_put_object( cid: str, file_name: str, shell: Shell, - cluster: Cluster, + cluster: Optional[Cluster] = None, + neofs_env: Optional[NeoFSEnv] = None, bearer: Optional[str] = None, wallet_config: Optional[str] = None, xhdr: Optional[dict] = None, @@ -74,6 +76,7 @@ def can_put_object( attributes=attributes, shell=shell, cluster=cluster, + neofs_env=neofs_env, ) except OPERATION_ERROR_TYPE as err: assert error_matches_status(