From 9b0ac8579b61424bef114704f21cb38d09b7219a Mon Sep 17 00:00:00 2001 From: Vladimir Avdeev Date: Wed, 30 Nov 2022 11:26:38 +0300 Subject: [PATCH] Add static session token container tests Signed-off-by: Vladimir Avdeev --- pytest_tests/helpers/storage_object_info.py | 22 +- pytest_tests/steps/session_token.py | 233 +++++++++++------- .../testsuites/session_token/conftest.py | 26 ++ .../test_static_object_session_token.py | 178 +++++++------ .../test_static_session_token_container.py | 163 ++++++++++++ robot/resources/lib/python_keywords/acl.py | 9 +- .../lib/python_keywords/container.py | 9 +- .../lib/python_keywords/neofs_verbs.py | 4 +- 8 files changed, 444 insertions(+), 200 deletions(-) create mode 100644 pytest_tests/testsuites/session_token/conftest.py create mode 100644 pytest_tests/testsuites/session_token/test_static_session_token_container.py diff --git a/pytest_tests/helpers/storage_object_info.py b/pytest_tests/helpers/storage_object_info.py index b382e287..dd46740e 100644 --- a/pytest_tests/helpers/storage_object_info.py +++ b/pytest_tests/helpers/storage_object_info.py @@ -1,7 +1,5 @@ -import logging from dataclasses import dataclass - -logger = logging.getLogger("NeoLogger") +from typing import Optional @dataclass @@ -12,16 +10,16 @@ class ObjectRef: @dataclass class LockObjectInfo(ObjectRef): - lifetime: int = None - expire_at: int = None + lifetime: Optional[int] = None + expire_at: Optional[int] = None @dataclass class StorageObjectInfo(ObjectRef): - size: str = None - wallet_file_path: str = None - file_path: str = None - file_hash: str = None - attributes: list[dict[str, str]] = None - tombstone: str = None - locks: list[LockObjectInfo] = None + size: Optional[int] = None + wallet_file_path: Optional[str] = None + file_path: Optional[str] = None + file_hash: Optional[str] = None + attributes: Optional[list[dict[str, str]]] = None + tombstone: Optional[str] = None + locks: Optional[list[LockObjectInfo]] = None diff --git a/pytest_tests/steps/session_token.py b/pytest_tests/steps/session_token.py index 1411f480..33390d89 100644 --- a/pytest_tests/steps/session_token.py +++ b/pytest_tests/steps/session_token.py @@ -4,14 +4,14 @@ import logging import os import uuid from dataclasses import dataclass -from typing import Optional +from enum import Enum +from typing import Any, Optional import allure import json_transformers from common import ASSETS_DIR, NEOFS_CLI_EXEC, NEOFS_ENDPOINT, WALLET_CONFIG from data_formatters import get_wallet_public_key from json_transformers import encode_for_json -from neo3 import wallet from neofs_testlib.cli import NeofsCli from neofs_testlib.shell import Shell from storage_object_info import StorageObjectInfo @@ -19,16 +19,6 @@ from wallet import WalletFile logger = logging.getLogger("NeoLogger") -PUT_VERB = "PUT" -DELETE_VERB = "DELETE" -LOCK_VERB = "LOCK" - -GET_VERB = "GET" -RANGEHASH_VERB = "RANGEHASH" -RANGE_VERB = "RANGE" -HEAD_VERB = "HEAD" -SEARCH_VERB = "SEARCH" - UNRELATED_KEY = "unrelated key in the session" UNRELATED_OBJECT = "unrelated object in the session" UNRELATED_CONTAINER = "unrelated container in the session" @@ -36,6 +26,22 @@ 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 @@ -44,76 +50,20 @@ class Lifetime: @allure.step("Generate Session Token") -def generate_session_token(owner: str, session_wallet: str, cid: str = "") -> 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(str): wallet address of container owner - session_wallet(str): the path to wallet to which we grant the - access via session token - cid(optional, str): container ID of the container; if absent, - we assume the session token is generated for any - container - Returns: - (str): the path to the generated session token file - """ - file_path = os.path.join(os.getcwd(), ASSETS_DIR, str(uuid.uuid4())) - - session_wlt_content = "" - with open(session_wallet) as fout: - session_wlt_content = json.load(fout) - session_wlt = wallet.Wallet.from_json(session_wlt_content, password="") - pub_key_64 = base64.b64encode(bytes.fromhex(str(session_wlt.accounts[0].public_key))).decode( - "utf-8" - ) - - session_token = { - "body": { - "id": f"{base64.b64encode(uuid.uuid4().bytes).decode('utf-8')}", - "ownerID": {"value": f"{json_transformers.encode_for_json(owner)}"}, - "lifetime": {"exp": "100000000", "nbf": "0", "iat": "0"}, - "sessionKey": f"{pub_key_64}", - "container": { - "verb": "PUT", - "wildcard": cid != "", - **({"containerID": {"value": f"{encode_for_json(cid)}"}} if cid != "" else {}), - }, - } - } - - 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 Object") -def generate_object_session_token( +def generate_session_token( owner_wallet: WalletFile, session_wallet: WalletFile, - oids: list[str], - cid: str, - verb: str, + session: dict[str, dict[str, Any]], tokens_dir: str, lifetime: Optional[Lifetime] = None, ) -> str: """ - This function generates session token for ObjectSessionContext - 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). + 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 - cid: container ID of the container - oids: list of objectIDs to put into session - verb: verb to grant access to; - Valid verbs are: GET, RANGE, RANGEHASH, HEAD, SEARCH. + 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 @@ -123,7 +73,7 @@ def generate_object_session_token( pub_key_64 = get_wallet_public_key(session_wallet.path, session_wallet.password, "base64") - lifetime = lifetime if lifetime else Lifetime() + lifetime = lifetime or Lifetime() session_token = { "body": { @@ -137,15 +87,9 @@ def generate_object_session_token( "iat": f"{lifetime.iat}", }, "sessionKey": pub_key_64, - "object": { - "verb": verb, - "target": { - "container": {"value": encode_for_json(cid)}, - "objects": [{"value": encode_for_json(oid)} for oid in oids], - }, - }, } } + 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: @@ -154,12 +98,117 @@ def generate_object_session_token( return file_path +@allure.step("Generate Session Token For Container") +def generate_container_session_token( + owner_wallet: WalletFile, + session_wallet: WalletFile, + 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 not 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: WalletFile, + session_wallet: WalletFile, + 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 object session") +def get_container_signed_token( + owner_wallet: WalletFile, + user_wallet: WalletFile, + verb: ContainerVerb, + shell: Shell, + tokens_dir: str, + lifetime: Optional[Lifetime] = None, +) -> str: + """ + Returns signed token file path for static object 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: WalletFile, user_wallet: WalletFile, + cid: str, storage_objects: list[StorageObjectInfo], - verb: str, + verb: ObjectVerb, shell: Shell, tokens_dir: str, lifetime: Optional[Lifetime] = None, @@ -169,15 +218,15 @@ def get_object_signed_token( """ storage_object_ids = [storage_object.oid for storage_object in storage_objects] session_token_file = generate_object_session_token( - owner_wallet, - user_wallet, - storage_object_ids, - owner_wallet.containers[0], - verb, - tokens_dir, + 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.path) + return sign_session_token(shell, session_token_file, owner_wallet) @allure.step("Create Session Token") @@ -212,7 +261,7 @@ def create_session_token( @allure.step("Sign Session Token") -def sign_session_token(shell: Shell, session_token_file: str, wlt: str) -> str: +def sign_session_token(shell: Shell, session_token_file: str, wlt: WalletFile) -> str: """ This function signs the session token by the given wallet. @@ -227,6 +276,6 @@ def sign_session_token(shell: Shell, session_token_file: str, wlt: str) -> str: signed_token_file = os.path.join(os.getcwd(), ASSETS_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, from_file=session_token_file, to_file=signed_token_file + wallet=wlt.path, from_file=session_token_file, to_file=signed_token_file ) return signed_token_file diff --git a/pytest_tests/testsuites/session_token/conftest.py b/pytest_tests/testsuites/session_token/conftest.py new file mode 100644 index 00000000..a80aab53 --- /dev/null +++ b/pytest_tests/testsuites/session_token/conftest.py @@ -0,0 +1,26 @@ +import pytest +from wallet import WalletFactory, WalletFile + + +@pytest.fixture(scope="module") +def owner_wallet(wallet_factory: WalletFactory) -> WalletFile: + """ + Returns wallet which owns containers and objects + """ + return wallet_factory.create_wallet() + + +@pytest.fixture(scope="module") +def user_wallet(wallet_factory: WalletFactory) -> WalletFile: + """ + Returns wallet which will use objects from owner via static session + """ + return wallet_factory.create_wallet() + + +@pytest.fixture(scope="module") +def stranger_wallet(wallet_factory: WalletFactory) -> WalletFile: + """ + Returns stranger wallet which should fail to obtain data + """ + return wallet_factory.create_wallet() diff --git a/pytest_tests/testsuites/session_token/test_static_object_session_token.py b/pytest_tests/testsuites/session_token/test_static_object_session_token.py index 379e42d3..3f6ce97f 100644 --- a/pytest_tests/testsuites/session_token/test_static_object_session_token.py +++ b/pytest_tests/testsuites/session_token/test_static_object_session_token.py @@ -6,7 +6,6 @@ from common import COMPLEX_OBJ_SIZE, SIMPLE_OBJ_SIZE from epoch import ensure_fresh_epoch, tick_epoch from file_helper import generate_file from grpc_responses import MALFORMED_REQUEST, OBJECT_ACCESS_DENIED, OBJECT_NOT_FOUND -from neofs_testlib.hosting import Hosting from neofs_testlib.shell import Shell from pytest import FixtureRequest from python_keywords.container import create_container @@ -20,23 +19,17 @@ from python_keywords.neofs_verbs import ( put_object, search_object, ) -from wallet import WalletFactory, WalletFile +from wallet import WalletFile from helpers.storage_object_info import StorageObjectInfo from steps.session_token import ( - DELETE_VERB, - GET_VERB, - HEAD_VERB, INVALID_SIGNATURE, - PUT_VERB, - RANGE_VERB, - RANGEHASH_VERB, - SEARCH_VERB, UNRELATED_CONTAINER, UNRELATED_KEY, UNRELATED_OBJECT, WRONG_VERB, Lifetime, + ObjectVerb, generate_object_session_token, get_object_signed_token, sign_session_token, @@ -48,6 +41,14 @@ logger = logging.getLogger("NeoLogger") RANGE_OFFSET_FOR_COMPLEX_OBJECT = 200 +@pytest.fixture(scope="module") +def storage_containers(owner_wallet: WalletFile, client_shell: Shell) -> list[str]: + # Separate containers for complex/simple objects to avoid side-effects + cid = create_container(owner_wallet.path, shell=client_shell) + other_cid = create_container(owner_wallet.path, shell=client_shell) + yield [cid, other_cid] + + @pytest.fixture( params=[SIMPLE_OBJ_SIZE, COMPLEX_OBJ_SIZE], ids=["simple object", "complex object"], @@ -55,27 +56,26 @@ RANGE_OFFSET_FOR_COMPLEX_OBJECT = 200 scope="module", ) def storage_objects( - owner_wallet: WalletFile, client_shell: Shell, request: FixtureRequest + owner_wallet: WalletFile, + client_shell: Shell, + storage_containers: list[str], + request: FixtureRequest, ) -> list[StorageObjectInfo]: + file_path = generate_file(request.param) storage_objects = [] - # Separate containers for complex/simple objects to avoid side-effects - cid = create_container(owner_wallet.path, shell=client_shell) - other_cid = create_container(owner_wallet.path, shell=client_shell) - owner_wallet.containers = [cid, other_cid] - with allure.step("Put objects"): # upload couple objects for _ in range(3): storage_object_id = put_object( wallet=owner_wallet.path, path=file_path, - cid=cid, + cid=storage_containers[0], shell=client_shell, ) - storage_object = StorageObjectInfo(cid, storage_object_id) + 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 @@ -102,56 +102,38 @@ def get_ranges(storage_object: StorageObjectInfo, shell: Shell) -> list[str]: return [ "0:10", f"{object_size-10}:10", - f"{max_object_size - RANGE_OFFSET_FOR_COMPLEX_OBJECT}:{RANGE_OFFSET_FOR_COMPLEX_OBJECT * 2}", + 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 owner_wallet(wallet_factory: WalletFactory) -> WalletFile: - """ - Returns wallet which owns containers and objects - """ - return wallet_factory.create_wallet() - - -@pytest.fixture(scope="module") -def user_wallet(wallet_factory: WalletFactory) -> WalletFile: - """ - Returns wallet which will use objects from owner via static session - """ - return wallet_factory.create_wallet() - - -@pytest.fixture(scope="module") -def stranger_wallet(wallet_factory: WalletFactory) -> WalletFile: - """ - Returns stranger wallet which should fail to obtain data - """ - return wallet_factory.create_wallet() - - @pytest.fixture(scope="module") def static_sessions( owner_wallet: WalletFile, user_wallet: WalletFile, + storage_containers: list[str], storage_objects: list[StorageObjectInfo], client_shell: Shell, prepare_tmp_dir: str, -) -> dict[str, 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 + Returns dict with static session token file paths for all verbs with default lifetime with + valid container and first two objects """ - verbs = [GET_VERB, RANGEHASH_VERB, RANGE_VERB, HEAD_VERB, SEARCH_VERB, DELETE_VERB, PUT_VERB] - sessions = {} - - for verb in verbs: - sessions[verb] = get_object_signed_token( - owner_wallet, user_wallet, storage_objects[0:2], verb, client_shell, prepare_tmp_dir + return { + verb: get_object_signed_token( + owner_wallet, + user_wallet, + storage_containers[0], + storage_objects[0:2], + verb, + client_shell, + prepare_tmp_dir, ) - - return sessions + for verb in ObjectVerb + } @allure.title("Validate static session with read operations") @@ -159,17 +141,17 @@ def static_sessions( @pytest.mark.parametrize( "method_under_test,verb", [ - (head_object, HEAD_VERB), - (get_object, GET_VERB), + (head_object, ObjectVerb.HEAD), + (get_object, ObjectVerb.GET), ], ) def test_static_session_read( user_wallet: WalletFile, client_shell: Shell, storage_objects: list[StorageObjectInfo], - static_sessions: list[str], + static_sessions: dict[ObjectVerb, str], method_under_test, - verb: str, + verb: ObjectVerb, request: FixtureRequest, ): """ @@ -193,15 +175,15 @@ def test_static_session_read( @pytest.mark.static_session @pytest.mark.parametrize( "method_under_test,verb", - [(get_range, RANGE_VERB), (get_range_hash, RANGEHASH_VERB)], + [(get_range, ObjectVerb.RANGE), (get_range_hash, ObjectVerb.RANGEHASH)], ) def test_static_session_range( user_wallet: WalletFile, client_shell: Shell, storage_objects: list[StorageObjectInfo], - static_sessions: list[str], + static_sessions: dict[ObjectVerb, str], method_under_test, - verb: str, + verb: ObjectVerb, request: FixtureRequest, ): """ @@ -233,7 +215,7 @@ def test_static_session_search( user_wallet: WalletFile, client_shell: Shell, storage_objects: list[StorageObjectInfo], - static_sessions: list[str], + static_sessions: dict[ObjectVerb, str], request: FixtureRequest, ): """ @@ -244,7 +226,7 @@ def test_static_session_search( cid = storage_objects[0].cid expected_object_ids = [storage_object.oid for storage_object in storage_objects[0:2]] actual_object_ids = search_object( - user_wallet.path, cid, client_shell, session=static_sessions[SEARCH_VERB], root=True + user_wallet.path, cid, client_shell, session=static_sessions[ObjectVerb.SEARCH], root=True ) assert expected_object_ids == actual_object_ids @@ -255,7 +237,7 @@ def test_static_session_unrelated_object( user_wallet: WalletFile, client_shell: Shell, storage_objects: list[StorageObjectInfo], - static_sessions: list[str], + static_sessions: dict[ObjectVerb, str], request: FixtureRequest, ): """ @@ -270,7 +252,7 @@ def test_static_session_unrelated_object( storage_objects[2].cid, storage_objects[2].oid, client_shell, - session=static_sessions[HEAD_VERB], + session=static_sessions[ObjectVerb.HEAD], ) @@ -280,7 +262,7 @@ def test_static_session_head_unrelated_user( stranger_wallet: WalletFile, client_shell: Shell, storage_objects: list[StorageObjectInfo], - static_sessions: list[str], + static_sessions: dict[ObjectVerb, str], request: FixtureRequest, ): """ @@ -297,7 +279,7 @@ def test_static_session_head_unrelated_user( storage_object.cid, storage_object.oid, client_shell, - session=static_sessions[HEAD_VERB], + session=static_sessions[ObjectVerb.HEAD], ) @@ -307,7 +289,7 @@ def test_static_session_head_wrong_verb( user_wallet: WalletFile, client_shell: Shell, storage_objects: list[StorageObjectInfo], - static_sessions: list[str], + static_sessions: dict[ObjectVerb, str], request: FixtureRequest, ): """ @@ -324,7 +306,7 @@ def test_static_session_head_wrong_verb( storage_object.cid, storage_object.oid, client_shell, - session=static_sessions[HEAD_VERB], + session=static_sessions[ObjectVerb.HEAD], ) @@ -334,8 +316,9 @@ def test_static_session_unrelated_container( owner_wallet: WalletFile, user_wallet: WalletFile, client_shell: Shell, + storage_containers: list[str], storage_objects: list[StorageObjectInfo], - static_sessions: list[str], + static_sessions: dict[ObjectVerb, str], request: FixtureRequest, ): """ @@ -349,10 +332,10 @@ def test_static_session_unrelated_container( with pytest.raises(Exception, match=UNRELATED_CONTAINER): get_object( user_wallet.path, - owner_wallet.containers[1], + storage_containers[1], storage_object.oid, client_shell, - session=static_sessions[GET_VERB], + session=static_sessions[ObjectVerb.GET], ) @@ -363,6 +346,7 @@ def test_static_session_signed_by_other( user_wallet: WalletFile, stranger_wallet: WalletFile, client_shell: Shell, + storage_containers: list[str], storage_objects: list[StorageObjectInfo], prepare_tmp_dir: str, request: FixtureRequest, @@ -379,11 +363,11 @@ def test_static_session_signed_by_other( owner_wallet, user_wallet, [storage_object.oid], - owner_wallet.containers[0], - HEAD_VERB, + storage_containers[0], + ObjectVerb.HEAD, prepare_tmp_dir, ) - signed_token_file = sign_session_token(client_shell, session_token_file, stranger_wallet.path) + signed_token_file = sign_session_token(client_shell, session_token_file, stranger_wallet) with pytest.raises(Exception, match=OBJECT_ACCESS_DENIED): head_object( user_wallet.path, @@ -400,6 +384,7 @@ def test_static_session_signed_for_other_container( owner_wallet: WalletFile, user_wallet: WalletFile, client_shell: Shell, + storage_containers: list[str], storage_objects: list[StorageObjectInfo], prepare_tmp_dir: str, request: FixtureRequest, @@ -411,12 +396,12 @@ def test_static_session_signed_for_other_container( f"Validate static session which signed for another container for {request.node.callspec.id}" ) storage_object = storage_objects[0] - container = owner_wallet.containers[1] + container = storage_containers[1] session_token_file = generate_object_session_token( - owner_wallet, user_wallet, [storage_object.oid], container, HEAD_VERB, prepare_tmp_dir + owner_wallet, user_wallet, [storage_object.oid], container, ObjectVerb.HEAD, prepare_tmp_dir ) - signed_token_file = sign_session_token(client_shell, session_token_file, owner_wallet.path) + signed_token_file = sign_session_token(client_shell, session_token_file, owner_wallet) with pytest.raises(Exception, match=OBJECT_NOT_FOUND): head_object( user_wallet.path, container, storage_object.oid, client_shell, session=signed_token_file @@ -429,6 +414,7 @@ def test_static_session_without_sign( owner_wallet: WalletFile, user_wallet: WalletFile, client_shell: Shell, + storage_containers: list[str], storage_objects: list[StorageObjectInfo], prepare_tmp_dir: str, request: FixtureRequest, @@ -445,8 +431,8 @@ def test_static_session_without_sign( owner_wallet, user_wallet, [storage_object.oid], - owner_wallet.containers[0], - HEAD_VERB, + storage_containers[0], + ObjectVerb.HEAD, prepare_tmp_dir, ) with pytest.raises(Exception, match=INVALID_SIGNATURE): @@ -465,6 +451,7 @@ def test_static_session_expiration_at_next( owner_wallet: WalletFile, user_wallet: WalletFile, client_shell: Shell, + storage_containers: list[str], storage_objects: list[StorageObjectInfo], prepare_tmp_dir: str, request: FixtureRequest, @@ -477,15 +464,16 @@ def test_static_session_expiration_at_next( ) epoch = ensure_fresh_epoch(client_shell) - container = owner_wallet.containers[0] + 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, - HEAD_VERB, + ObjectVerb.HEAD, client_shell, prepare_tmp_dir, expiration, @@ -509,6 +497,7 @@ def test_static_session_start_at_next( owner_wallet: WalletFile, user_wallet: WalletFile, client_shell: Shell, + storage_containers: list[str], storage_objects: list[StorageObjectInfo], prepare_tmp_dir: str, request: FixtureRequest, @@ -517,19 +506,21 @@ def test_static_session_start_at_next( 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}" + "Validate static session which is valid starting from next epoch " + f"for {request.node.callspec.id}" ) epoch = ensure_fresh_epoch(client_shell) - container = owner_wallet.containers[0] + 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, - HEAD_VERB, + ObjectVerb.HEAD, client_shell, prepare_tmp_dir, expiration, @@ -558,6 +549,7 @@ def test_static_session_already_expired( owner_wallet: WalletFile, user_wallet: WalletFile, client_shell: Shell, + storage_containers: list[str], storage_objects: list[StorageObjectInfo], prepare_tmp_dir: str, request: FixtureRequest, @@ -570,15 +562,16 @@ def test_static_session_already_expired( ) epoch = ensure_fresh_epoch(client_shell) - container = owner_wallet.containers[0] + 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, - HEAD_VERB, + ObjectVerb.HEAD, client_shell, prepare_tmp_dir, expiration, @@ -594,8 +587,9 @@ def test_static_session_already_expired( def test_static_session_delete_verb( user_wallet: WalletFile, client_shell: Shell, + storage_containers: list[str], storage_objects: list[StorageObjectInfo], - static_sessions: list[str], + static_sessions: dict[ObjectVerb, str], request: FixtureRequest, ): """ @@ -611,7 +605,7 @@ def test_static_session_delete_verb( storage_object.cid, storage_object.oid, client_shell, - session=static_sessions[DELETE_VERB], + session=static_sessions[ObjectVerb.DELETE], ) @@ -620,7 +614,7 @@ def test_static_session_put_verb( user_wallet: WalletFile, client_shell: Shell, storage_objects: list[StorageObjectInfo], - static_sessions: list[str], + static_sessions: dict[ObjectVerb, str], request: FixtureRequest, ): """ @@ -636,7 +630,7 @@ def test_static_session_put_verb( storage_object.file_path, storage_object.cid, client_shell, - session=static_sessions[PUT_VERB], + session=static_sessions[ObjectVerb.PUT], ) @@ -646,6 +640,7 @@ def test_static_session_invalid_issued_epoch( owner_wallet: WalletFile, user_wallet: WalletFile, client_shell: Shell, + storage_containers: list[str], storage_objects: list[StorageObjectInfo], prepare_tmp_dir: str, request: FixtureRequest, @@ -658,15 +653,16 @@ def test_static_session_invalid_issued_epoch( ) epoch = ensure_fresh_epoch(client_shell) - container = owner_wallet.containers[0] + 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, - HEAD_VERB, + ObjectVerb.HEAD, client_shell, prepare_tmp_dir, expiration, diff --git a/pytest_tests/testsuites/session_token/test_static_session_token_container.py b/pytest_tests/testsuites/session_token/test_static_session_token_container.py new file mode 100644 index 00000000..4592155b --- /dev/null +++ b/pytest_tests/testsuites/session_token/test_static_session_token_container.py @@ -0,0 +1,163 @@ +import allure +import pytest +from file_helper import generate_file +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 wallet import WalletFile +from wellknown_acl import PUBLIC_ACL + +from steps.session_token import ContainerVerb, get_container_signed_token + + +class TestSessionTokenContainer: + @pytest.fixture(scope="module") + def static_sessions( + self, + owner_wallet: WalletFile, + user_wallet: WalletFile, + client_shell: Shell, + prepare_tmp_dir: str, + ) -> dict[ContainerVerb, str]: + """ + Returns dict with static session token file paths for all verbs with default lifetime + """ + return { + verb: get_container_signed_token( + owner_wallet, user_wallet, verb, client_shell, prepare_tmp_dir + ) + for verb in ContainerVerb + } + + def test_static_session_token_container_create( + self, + client_shell: Shell, + owner_wallet: WalletFile, + user_wallet: WalletFile, + 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=client_shell, + wait_for_creation=False, + ) + + container_info: dict[str, str] = get_container(owner_wallet.path, cid, shell=client_shell) + assert container_info["ownerID"] == owner_wallet.get_address() + + assert cid not in list_containers(user_wallet.path, shell=client_shell) + assert cid in list_containers(owner_wallet.path, shell=client_shell) + + @pytest.mark.skip("Failed with timeout") + def test_static_session_token_container_create_with_other_verb( + self, + client_shell: Shell, + owner_wallet: WalletFile, + user_wallet: WalletFile, + 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=client_shell, + wait_for_creation=False, + ) + + @pytest.mark.skip("Failed with timeout") + def test_static_session_token_container_create_with_other_wallet( + self, + client_shell: Shell, + owner_wallet: WalletFile, + stranger_wallet: WalletFile, + 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=client_shell, + wait_for_creation=False, + ) + + def test_static_session_token_container_delete( + self, + client_shell: Shell, + owner_wallet: WalletFile, + user_wallet: WalletFile, + static_sessions: dict[ContainerVerb, str], + ): + """ + Validate static session with delete operation + """ + with allure.step("Create container"): + cid = create_container(owner_wallet.path, shell=client_shell, 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=client_shell, + ) + + assert cid not in list_containers(owner_wallet.path, shell=client_shell) + + def test_static_session_token_container_set_eacl( + self, + client_shell: Shell, + owner_wallet: WalletFile, + user_wallet: WalletFile, + stranger_wallet: WalletFile, + static_sessions: dict[ContainerVerb, str], + ): + """ + Validate static session with set eacl operation + """ + with allure.step("Create container"): + cid = create_container(owner_wallet.path, basic_acl=PUBLIC_ACL, shell=client_shell) + file_path = generate_file() + assert can_put_object(stranger_wallet.path, cid, file_path, client_shell) + + 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=client_shell), + shell=client_shell, + session_token=static_sessions[ContainerVerb.SETEACL], + ) + wait_for_cache_expired() + + assert not can_put_object(stranger_wallet.path, cid, file_path, client_shell) diff --git a/robot/resources/lib/python_keywords/acl.py b/robot/resources/lib/python_keywords/acl.py index 0e5a1905..3164b7f7 100644 --- a/robot/resources/lib/python_keywords/acl.py +++ b/robot/resources/lib/python_keywords/acl.py @@ -130,7 +130,13 @@ def get_eacl(wallet_path: str, cid: str, shell: Shell) -> Optional[str]: @allure.title("Set extended ACL") -def set_eacl(wallet_path: str, cid: str, eacl_table_path: str, shell: Shell) -> None: +def set_eacl( + wallet_path: str, + cid: str, + eacl_table_path: str, + shell: Shell, + session_token: Optional[str] = None, +) -> None: cli = NeofsCli(shell, NEOFS_CLI_EXEC, WALLET_CONFIG) cli.container.set_eacl( wallet=wallet_path, @@ -138,6 +144,7 @@ def set_eacl(wallet_path: str, cid: str, eacl_table_path: str, shell: Shell) -> cid=cid, table=eacl_table_path, await_mode=True, + session=session_token, ) diff --git a/robot/resources/lib/python_keywords/container.py b/robot/resources/lib/python_keywords/container.py index 4b4a4cf5..0b749aeb 100644 --- a/robot/resources/lib/python_keywords/container.py +++ b/robot/resources/lib/python_keywords/container.py @@ -165,7 +165,9 @@ def get_container( @allure.step("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, shell: Shell, force: bool = False) -> None: +def delete_container( + wallet: str, cid: str, shell: Shell, force: bool = False, session_token: Optional[str] = None +) -> None: """ A wrapper for `neofs-cli container delete` call. Args: @@ -173,11 +175,14 @@ def delete_container(wallet: str, cid: str, shell: Shell, force: bool = False) - cid (str): ID of the container to delete shell: executor for cli command force (bool): do not check whether container contains locks and remove immediately + session_token: a path to session token file This function doesn't return anything. """ cli = NeofsCli(shell, NEOFS_CLI_EXEC, WALLET_CONFIG) - cli.container.delete(wallet=wallet, cid=cid, rpc_endpoint=NEOFS_ENDPOINT, force=force) + cli.container.delete( + wallet=wallet, cid=cid, rpc_endpoint=NEOFS_ENDPOINT, force=force, session=session_token + ) def _parse_cid(output: str) -> str: diff --git a/robot/resources/lib/python_keywords/neofs_verbs.py b/robot/resources/lib/python_keywords/neofs_verbs.py index 24a2dc45..fd17e616 100644 --- a/robot/resources/lib/python_keywords/neofs_verbs.py +++ b/robot/resources/lib/python_keywords/neofs_verbs.py @@ -4,7 +4,7 @@ import os import random import re import uuid -from typing import Optional +from typing import Any, Optional import allure import json_transformers @@ -406,7 +406,7 @@ def get_netmap_netinfo( address: Optional[str] = None, ttl: Optional[int] = None, xhdr: Optional[dict] = None, -) -> dict[str, object]: +) -> dict[str, Any]: """ Get netmap netinfo output from node