diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 513736975..5b39da40d 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -200,7 +200,7 @@ jobs: ################################################################ - name: Run Sanity tests for pull requests - timeout-minutes: 120 + timeout-minutes: 240 if: github.event_name == 'pull_request' run: | source venv.pytest/bin/activate && pytest --alluredir=${GITHUB_WORKSPACE}/allure-results pytest_tests/tests diff --git a/pytest_tests/lib/helpers/acl.py b/pytest_tests/lib/helpers/acl.py index 04d94081a..130956192 100644 --- a/pytest_tests/lib/helpers/acl.py +++ b/pytest_tests/lib/helpers/acl.py @@ -51,6 +51,21 @@ class EACLHeaderType(Enum): class EACLMatchType(Enum): STRING_EQUAL = "=" # Return true if strings are equal STRING_NOT_EQUAL = "!=" # Return true if strings are different + NUM_GT = ">" + NUM_GE = ">=" + NUM_LT = "<" + NUM_LE = "<=" + + def compare(self, val1, val2): + if self.value == ">": + return val1 > val2 + elif self.value == ">=": + return val1 >= val2 + elif self.value == "<": + return val1 < val2 + elif self.value == "<=": + return val1 <= val2 + raise AssertionError(f"Unsupported value: {self.value} for compare") @dataclass diff --git a/pytest_tests/lib/helpers/grpc_responses.py b/pytest_tests/lib/helpers/grpc_responses.py index 145b3fc4b..df86b431c 100644 --- a/pytest_tests/lib/helpers/grpc_responses.py +++ b/pytest_tests/lib/helpers/grpc_responses.py @@ -41,6 +41,7 @@ ) INVALID_SEARCH_QUERY = ".*invalid search query.*" +INVALID_RULES = ".*unable to parse provided rules.*" def error_matches_status(error: Exception, status_pattern: str) -> bool: diff --git a/pytest_tests/lib/helpers/object_access.py b/pytest_tests/lib/helpers/object_access.py index ad3fc345e..1ff30b446 100644 --- a/pytest_tests/lib/helpers/object_access.py +++ b/pytest_tests/lib/helpers/object_access.py @@ -47,7 +47,9 @@ def can_get_object( 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) + assert get_file_hash(file_name) == get_file_hash( + got_file_path + ), "file hash of downloaded object is not equal to the uploaded" return True diff --git a/pytest_tests/tests/acl/test_eacl_filters.py b/pytest_tests/tests/acl/test_eacl_filters.py index 6ee170ca7..90ec6e780 100644 --- a/pytest_tests/tests/acl/test_eacl_filters.py +++ b/pytest_tests/tests/acl/test_eacl_filters.py @@ -11,11 +11,14 @@ EACLRule, create_eacl, form_bearertoken_file, + get_eacl, set_eacl, wait_for_cache_expired, ) from helpers.container import create_container, delete_container from helpers.container_access import check_full_access_to_container, check_no_access_to_container +from helpers.file_helper import generate_file +from helpers.grpc_responses import INVALID_RULES, OBJECT_ACCESS_DENIED from helpers.neofs_verbs import put_object_to_random_node from helpers.object_access import can_get_head_object, can_get_object, can_put_object from helpers.wellknown_acl import PUBLIC_ACL @@ -62,6 +65,35 @@ class TestEACLFilters(NeofsEnvTestBase): EACLOperation.HEAD, EACLOperation.PUT, ] + OBJECT_NUMERIC_KEY_ATTR_NAME = "numeric_value" + OBJECT_NUMERIC_VALUES = [-(2**64) - 1, -1, 0, 1, 10, 2**64 + 1] + EXPIRATION_OBJECT_ATTR = "__NEOFS__EXPIRATION_EPOCH" + PAYLOAD_LENGTH_OBJECT_ATTR = "$Object:payloadLength" + CREATION_EPOCH_OBJECT_ATTR = "$Object:creationEpoch" + OPERATION_NOT_ALLOWED_ERROR_MESSAGE = "GET is not allowed for this object, while it should be" + OPERATION_ALLOWED_ERROR_MESSAGE = "GET is allowed for this object, while it shouldn't be" + + @pytest.fixture(scope="function") + def eacl_container(self, wallets): + user_wallet = wallets.get_wallet() + + with allure.step("Create eACL public container"): + cid = create_container( + user_wallet.wallet_path, + basic_acl=PUBLIC_ACL, + shell=self.shell, + endpoint=self.neofs_env.sn_rpc, + ) + + yield cid + + with allure.step("Delete eACL public container"): + delete_container( + user_wallet.wallet_path, + cid, + shell=self.shell, + endpoint=self.neofs_env.sn_rpc, + ) @pytest.fixture(scope="function") def eacl_container_with_objects(self, wallets, file_path): @@ -627,3 +659,556 @@ def test_extended_acl_allow_filters_object( attributes=deny_attribute, bearer=bearer_other, ) + + @pytest.mark.parametrize( + "operator", + [ + EACLMatchType.NUM_GT, + EACLMatchType.NUM_GE, + EACLMatchType.NUM_LT, + EACLMatchType.NUM_LE, + ], + ) + @pytest.mark.parametrize( + "object_size", + [pytest.lazy_fixture("simple_object_size"), pytest.lazy_fixture("complex_object_size")], + ids=["simple object", "complex object"], + ) + def test_extended_acl_numeric_values(self, wallets, operator, eacl_container, object_size): + user_wallet = wallets.get_wallet() + + cid = eacl_container + objects = [] + + with allure.step("Add test objects to container"): + for numeric_value in self.OBJECT_NUMERIC_VALUES: + file_path = generate_file(object_size) + + objects.append( + { + self.OBJECT_NUMERIC_KEY_ATTR_NAME: numeric_value, + "id": put_object_to_random_node( + user_wallet.wallet_path, + file_path, + cid, + shell=self.shell, + neofs_env=self.neofs_env, + attributes={self.OBJECT_NUMERIC_KEY_ATTR_NAME: numeric_value}, + ), + "file_path": file_path, + } + ) + + with allure.step(f"GET objects with any numeric value attribute should be allowed"): + for obj in objects: + assert can_get_object( + user_wallet.wallet_path, + cid, + obj["id"], + obj["file_path"], + self.shell, + neofs_env=self.neofs_env, + ), self.OPERATION_NOT_ALLOWED_ERROR_MESSAGE + + for numeric_value in self.OBJECT_NUMERIC_VALUES: + with allure.step(f"Deny GET for all objects {operator.value} {numeric_value}"): + eacl_deny = [ + EACLRule( + access=EACLAccess.DENY, + role=EACLRole.USER, + filters=EACLFilters( + [ + EACLFilter( + header_type=EACLHeaderType.OBJECT, + match_type=operator, + key=self.OBJECT_NUMERIC_KEY_ATTR_NAME, + value=numeric_value, + ) + ] + ), + operation=EACLOperation.GET, + ) + ] + set_eacl( + user_wallet.wallet_path, + cid, + create_eacl(cid, eacl_deny, shell=self.shell), + shell=self.shell, + endpoint=self.neofs_env.sn_rpc, + ) + wait_for_cache_expired() + get_eacl( + user_wallet.wallet_path, + cid, + shell=self.shell, + endpoint=self.neofs_env.sn_rpc, + ) + + with allure.step( + f"GET object with numeric value attribute {operator.value} {numeric_value} should be denied" + ): + for obj in objects: + if operator.compare(obj[self.OBJECT_NUMERIC_KEY_ATTR_NAME], numeric_value): + assert not can_get_object( + user_wallet.wallet_path, + cid, + obj["id"], + obj["file_path"], + self.shell, + neofs_env=self.neofs_env, + ), self.OPERATION_ALLOWED_ERROR_MESSAGE + + with allure.step( + f"GET object with numeric value attribute not {operator.value} {numeric_value} should be allowed" + ): + for obj in objects: + if not operator.compare(obj[self.OBJECT_NUMERIC_KEY_ATTR_NAME], numeric_value): + assert can_get_object( + user_wallet.wallet_path, + cid, + obj["id"], + obj["file_path"], + self.shell, + neofs_env=self.neofs_env, + ), self.OPERATION_NOT_ALLOWED_ERROR_MESSAGE + + @pytest.mark.parametrize( + "operator", + [ + EACLMatchType.NUM_GT, + EACLMatchType.NUM_GE, + EACLMatchType.NUM_LT, + EACLMatchType.NUM_LE, + ], + ) + @pytest.mark.parametrize( + "invalid_attr_value", + ["abc", "92.1", "93-1"], + ) + def test_extended_acl_numeric_values_invalid_filters( + self, wallets, operator, eacl_container, simple_object_size, invalid_attr_value + ): + user_wallet = wallets.get_wallet() + + cid = eacl_container + oid = None + + with allure.step("Add test object to container"): + file_path = generate_file(simple_object_size) + + oid = put_object_to_random_node( + user_wallet.wallet_path, + file_path, + cid, + shell=self.shell, + neofs_env=self.neofs_env, + attributes={self.OBJECT_NUMERIC_KEY_ATTR_NAME: 0}, + ) + + with allure.step(f"GET object with numeric value attribute should be allowed"): + assert can_get_object( + user_wallet.wallet_path, + cid, + oid, + file_path, + self.shell, + neofs_env=self.neofs_env, + ), self.OPERATION_NOT_ALLOWED_ERROR_MESSAGE + + with allure.step(f"Deny GET for all objects {operator.value} {invalid_attr_value}"): + eacl_deny = [ + EACLRule( + access=EACLAccess.DENY, + role=EACLRole.USER, + filters=EACLFilters( + [ + EACLFilter( + header_type=EACLHeaderType.OBJECT, + match_type=operator, + key=self.OBJECT_NUMERIC_KEY_ATTR_NAME, + value=invalid_attr_value, + ) + ] + ), + operation=EACLOperation.GET, + ) + ] + with pytest.raises(Exception, match=INVALID_RULES): + create_eacl(cid, eacl_deny, shell=self.shell) + + def test_extended_acl_numeric_values_attr_str_filter_numeric( + self, wallets, eacl_container, simple_object_size + ): + operator = EACLMatchType.NUM_GT + user_wallet = wallets.get_wallet() + + cid = eacl_container + oid = None + + with allure.step("Add test object to container"): + file_path = generate_file(simple_object_size) + + oid = put_object_to_random_node( + user_wallet.wallet_path, + file_path, + cid, + shell=self.shell, + neofs_env=self.neofs_env, + attributes={self.OBJECT_NUMERIC_KEY_ATTR_NAME: "abc"}, + ) + + with allure.step(f"GET object with numeric value attribute should be allowed"): + assert can_get_object( + user_wallet.wallet_path, + cid, + oid, + file_path, + self.shell, + neofs_env=self.neofs_env, + ), self.OPERATION_NOT_ALLOWED_ERROR_MESSAGE + + with allure.step(f"Deny GET for all objects {operator.value} 0"): + eacl_deny = [ + EACLRule( + access=EACLAccess.DENY, + role=EACLRole.USER, + filters=EACLFilters( + [ + EACLFilter( + header_type=EACLHeaderType.OBJECT, + match_type=operator, + key=self.OBJECT_NUMERIC_KEY_ATTR_NAME, + value=0, + ) + ] + ), + operation=EACLOperation.GET, + ) + ] + set_eacl( + user_wallet.wallet_path, + cid, + create_eacl(cid, eacl_deny, shell=self.shell), + shell=self.shell, + endpoint=self.neofs_env.sn_rpc, + ) + wait_for_cache_expired() + get_eacl( + user_wallet.wallet_path, + cid, + shell=self.shell, + endpoint=self.neofs_env.sn_rpc, + ) + + with allure.step(f"GET object with numeric value attribute should be allowed"): + assert can_get_object( + user_wallet.wallet_path, + cid, + oid, + file_path, + self.shell, + neofs_env=self.neofs_env, + ), self.OPERATION_NOT_ALLOWED_ERROR_MESSAGE + + def test_extended_acl_numeric_values_expiration_attr( + self, wallets, eacl_container, complex_object_size + ): + user_wallet = wallets.get_wallet() + + cid = eacl_container + oid = None + + epoch = self.get_epoch() + + with allure.step(f"Set EACLs for GET/PUT to restrict operations with expiration attribute"): + eacl_deny = [ + EACLRule( + access=EACLAccess.DENY, + role=EACLRole.USER, + filters=EACLFilters( + [ + EACLFilter( + header_type=EACLHeaderType.OBJECT, + match_type=EACLMatchType.NUM_GE, + key=self.EXPIRATION_OBJECT_ATTR, + value=epoch + 2, + ), + ] + ), + operation=EACLOperation.GET, + ), + EACLRule( + access=EACLAccess.DENY, + role=EACLRole.USER, + filters=EACLFilters( + [ + EACLFilter( + header_type=EACLHeaderType.OBJECT, + match_type=EACLMatchType.NUM_GT, + key=self.EXPIRATION_OBJECT_ATTR, + value=epoch + 2, + ), + ] + ), + operation=EACLOperation.PUT, + ), + EACLRule( + access=EACLAccess.DENY, + role=EACLRole.USER, + filters=EACLFilters( + [ + EACLFilter( + header_type=EACLHeaderType.OBJECT, + match_type=EACLMatchType.NUM_LT, + key=self.EXPIRATION_OBJECT_ATTR, + value=epoch + 2, + ), + ] + ), + operation=EACLOperation.PUT, + ), + ] + set_eacl( + user_wallet.wallet_path, + cid, + create_eacl(cid, eacl_deny, shell=self.shell), + shell=self.shell, + endpoint=self.neofs_env.sn_rpc, + ) + wait_for_cache_expired() + get_eacl( + user_wallet.wallet_path, + cid, + shell=self.shell, + endpoint=self.neofs_env.sn_rpc, + ) + + with allure.step("Add test object to container"): + file_path = generate_file(complex_object_size) + + oid = put_object_to_random_node( + user_wallet.wallet_path, + file_path, + cid, + shell=self.shell, + neofs_env=self.neofs_env, + expire_at=epoch + 2, + ) + + with allure.step(f"PUT object should not be allowed because value is GT epoch + 2"): + with pytest.raises(Exception, match=OBJECT_ACCESS_DENIED): + put_object_to_random_node( + user_wallet.wallet_path, + file_path, + cid, + shell=self.shell, + neofs_env=self.neofs_env, + expire_at=epoch + 3, + ) + + with allure.step(f"PUT object should not be allowed because value is LT epoch + 2"): + with pytest.raises(Exception, match=OBJECT_ACCESS_DENIED): + put_object_to_random_node( + user_wallet.wallet_path, + file_path, + cid, + shell=self.shell, + neofs_env=self.neofs_env, + expire_at=epoch + 1, + ) + + with allure.step(f"GET object should not be allowed due to the value of expiration"): + assert not can_get_object( + user_wallet.wallet_path, + cid, + oid, + file_path, + self.shell, + neofs_env=self.neofs_env, + ), self.OPERATION_ALLOWED_ERROR_MESSAGE + + def test_extended_acl_numeric_values_payload_attr( + self, wallets, eacl_container, complex_object_size + ): + user_wallet = wallets.get_wallet() + + cid = eacl_container + + with allure.step(f"Set EACLs for PUT to restrict small objects"): + eacl_deny = [ + EACLRule( + access=EACLAccess.DENY, + role=EACLRole.USER, + filters=EACLFilters( + [ + EACLFilter( + header_type=EACLHeaderType.OBJECT, + match_type=EACLMatchType.NUM_LT, + key=self.PAYLOAD_LENGTH_OBJECT_ATTR, + value=complex_object_size + 1, + ), + ] + ), + operation=EACLOperation.PUT, + ), + ] + set_eacl( + user_wallet.wallet_path, + cid, + create_eacl(cid, eacl_deny, shell=self.shell), + shell=self.shell, + endpoint=self.neofs_env.sn_rpc, + ) + wait_for_cache_expired() + get_eacl( + user_wallet.wallet_path, + cid, + shell=self.shell, + endpoint=self.neofs_env.sn_rpc, + ) + + small_file_path = generate_file(complex_object_size) + + with allure.step( + f"PUT object should not be allowed because size is LT {complex_object_size + 1}" + ): + with pytest.raises(Exception, match=OBJECT_ACCESS_DENIED): + put_object_to_random_node( + user_wallet.wallet_path, + small_file_path, + cid, + shell=self.shell, + neofs_env=self.neofs_env, + ) + + big_file_path = generate_file(complex_object_size + 1) + + with allure.step( + f"PUT object should be allowed because size is EQ {complex_object_size + 1}" + ): + oid1 = put_object_to_random_node( + user_wallet.wallet_path, + big_file_path, + cid, + shell=self.shell, + neofs_env=self.neofs_env, + ) + + with allure.step(f"GET object should be allowed"): + assert can_get_object( + user_wallet.wallet_path, + cid, + oid1, + big_file_path, + self.shell, + neofs_env=self.neofs_env, + ), self.OPERATION_NOT_ALLOWED_ERROR_MESSAGE + + very_big_file_path = generate_file(complex_object_size * 2) + + with allure.step( + f"PUT object should be allowed because value is GT {complex_object_size + 1}" + ): + oid2 = put_object_to_random_node( + user_wallet.wallet_path, + very_big_file_path, + cid, + shell=self.shell, + neofs_env=self.neofs_env, + ) + + with allure.step(f"GET object should be allowed"): + assert can_get_object( + user_wallet.wallet_path, + cid, + oid2, + very_big_file_path, + self.shell, + neofs_env=self.neofs_env, + ), self.OPERATION_NOT_ALLOWED_ERROR_MESSAGE + + def test_extended_acl_numeric_values_epoch_attr( + self, wallets, eacl_container, complex_object_size + ): + user_wallet = wallets.get_wallet() + + epoch = self.get_epoch() + + cid = eacl_container + + with allure.step(f"Set EACLs for GET to restrict old objects"): + eacl_deny = [ + EACLRule( + access=EACLAccess.DENY, + role=EACLRole.USER, + filters=EACLFilters( + [ + EACLFilter( + header_type=EACLHeaderType.OBJECT, + match_type=EACLMatchType.NUM_LT, + key=self.CREATION_EPOCH_OBJECT_ATTR, + value=epoch + 1, + ), + ] + ), + operation=EACLOperation.GET, + ), + ] + set_eacl( + user_wallet.wallet_path, + cid, + create_eacl(cid, eacl_deny, shell=self.shell), + shell=self.shell, + endpoint=self.neofs_env.sn_rpc, + ) + wait_for_cache_expired() + get_eacl( + user_wallet.wallet_path, + cid, + shell=self.shell, + endpoint=self.neofs_env.sn_rpc, + ) + + with allure.step("Add test object to container"): + file_path = generate_file(complex_object_size) + + oid = put_object_to_random_node( + user_wallet.wallet_path, + file_path, + cid, + shell=self.shell, + neofs_env=self.neofs_env, + ) + + with allure.step(f"GET object should not be allowed"): + assert not can_get_object( + user_wallet.wallet_path, + cid, + oid, + file_path, + self.shell, + neofs_env=self.neofs_env, + ), self.OPERATION_ALLOWED_ERROR_MESSAGE + + self.tick_epoch() + + with allure.step("Add test object to container"): + file_path = generate_file(complex_object_size) + + oid = put_object_to_random_node( + user_wallet.wallet_path, + file_path, + cid, + shell=self.shell, + neofs_env=self.neofs_env, + ) + + with allure.step(f"GET object should be allowed"): + assert can_get_object( + user_wallet.wallet_path, + cid, + oid, + file_path, + self.shell, + neofs_env=self.neofs_env, + ), self.OPERATION_NOT_ALLOWED_ERROR_MESSAGE