From 408aae6de4ef5085a47171714764dfc51dac9e8b Mon Sep 17 00:00:00 2001 From: btemplep Date: Sat, 22 Aug 2026 11:58:11 -0400 Subject: [PATCH 1/9] update to 0.5.0 spec with changes for batch validation results. --- CHANGELOG.md | 16 +++++ src/authzee/__init__.py | 4 +- src/authzee/authzee_async.py | 16 +++-- src/authzee/compute/compute_module.py | 2 +- src/authzee/compute/in_process_compute.py | 26 +++++--- src/authzee/core.py | 6 +- src/authzee/jmespath.py | 46 +++++++++---- src/authzee/reference.py | 78 +++++++++++++++-------- src/authzee/types/authzee.py | 27 +++++++- tests/unit/test_in_process_compute.py | 3 +- tests/unit/test_reference.py | 2 +- 11 files changed, 167 insertions(+), 59 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1292109..aeddedd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Security --> +## [0.1.0a6] - 2026-08-22 + +Support for Authzee spec 0.5.0. + +### Added + +- `ValidateBatchRequestResult` TypedDict type +- `validate_request_result_schema` - Return value schema for the `validate_request` function + +### Changed + +- `validate_batch_request` now returns `ValidateBatchRequestResult` with `{error, batch}` instead of `GenericResult` + - `batch` contains per-item validation errors (or None for valid items) +- `validate_batch_request_result_schema` renamed `batch_errors` field to `batch` + + ## [0.1.0a5] - 2026-08-19 New revamp to support Authzee spec 0.4.0. diff --git a/src/authzee/__init__.py b/src/authzee/__init__.py index 512d3c9..9ccc060 100644 --- a/src/authzee/__init__.py +++ b/src/authzee/__init__.py @@ -6,7 +6,7 @@ or [](authzee.authzee_async.AuthzeeAsync) for asyncio support! """ -__version__ = "0.1.0a5" +__version__ = "0.1.0a6" __all__ = [ "Authzee", @@ -28,7 +28,7 @@ logger.disable("authzee") -authzee_specification_version = "0.4.0" +authzee_specification_version = "0.5.0" from authzee import exceptions, reference, types from authzee.authzee import Authzee diff --git a/src/authzee/authzee_async.py b/src/authzee/authzee_async.py index 5eecf99..917f591 100644 --- a/src/authzee/authzee_async.py +++ b/src/authzee/authzee_async.py @@ -2741,7 +2741,7 @@ async def validate_batch_request( self, batch_request: AuthzeeBatchRequest, config: AuthzeeConfigOverride | None=None - ) -> GenericResult: + ) -> ValidateBatchRequestResult: """Validate a batch authorization request without evaluating it. Parameters @@ -2818,10 +2818,17 @@ async def validate_batch_request( Returns ------- - GenericResult + ValidateBatchRequestResult ```python { - "error": None + "error": None, + "batch": [ + None, + { # or None + "error_type": "request", + "message": "Description of what went wrong for this batch item." + } + ] } ``` @@ -2832,7 +2839,8 @@ async def validate_batch_request( "error": { "error_type": "request", "message": "Description of what went wrong." - } + }, + "batch": [] } ``` diff --git a/src/authzee/compute/compute_module.py b/src/authzee/compute/compute_module.py index f70acb8..12b6cda 100644 --- a/src/authzee/compute/compute_module.py +++ b/src/authzee/compute/compute_module.py @@ -125,7 +125,7 @@ async def validate_batch_request( self, batch_request: AuthzeeBatchRequest, config: ValidateBatchRequestConfig - ) -> GenericResult: + ) -> ValidateBatchRequestResult: """Validate a batch request. """ raise NotImplementedError() diff --git a/src/authzee/compute/in_process_compute.py b/src/authzee/compute/in_process_compute.py index be96f76..68a47f0 100644 --- a/src/authzee/compute/in_process_compute.py +++ b/src/authzee/compute/in_process_compute.py @@ -7,7 +7,7 @@ "InProcessCompute" ] -from asyncio import Task, as_completed, create_task +from asyncio import Task, create_task, gather from typing import Any, Callable, Dict, List, Type import jsonschema_rs @@ -232,16 +232,22 @@ async def validate_batch_request( self, batch_request: AuthzeeBatchRequest, config: ValidateBatchRequestConfig - ) -> GenericResult: + ) -> ValidateBatchRequestResult: result = validate_batch_request_schema(batch_request) if result['error'] is not None: - return result + return { + "error": result['error'], + "batch": [] + } base_request: AuthzeeBatchRequest = batch_request.copy() base_request.pop("batch") base_result = await self.validate_request(request=base_request, config=config) if base_result['error'] is not None: - return base_result + return { + "error": base_result['error'], + "batch": [] + } batch_tasks: List[Task] = [] for item in batch_request['batch']: @@ -254,13 +260,17 @@ async def validate_batch_request( ) ) - for bt in as_completed(batch_tasks): - bt_result: GenericResult = await bt + batch_results: List[GenericResult] = await gather(*batch_tasks) + batch: list = [] + for bt_result in batch_results: if bt_result['error'] is not None: - return bt_result + batch.append(bt_result['error']) + else: + batch.append(None) return { - "error": None + "error": None, + "batch": batch } diff --git a/src/authzee/core.py b/src/authzee/core.py index 7d97756..e3277d5 100644 --- a/src/authzee/core.py +++ b/src/authzee/core.py @@ -151,7 +151,7 @@ def validate_context_def(context_def: ContextDef) -> GenericResult: } } - if not( + if not ( "type" in context_def['schema'] and context_def['schema']['type'] == "object" ): @@ -177,7 +177,7 @@ def validate_identity_def(identity_def: IdentityDef) -> GenericResult: } } - if not( + if not ( "type" in identity_def['schema'] and identity_def['schema']['type'] == "object" ): @@ -203,7 +203,7 @@ def validate_resource_def(resource_def: ResourceDef) -> GenericResult: } } - if not( + if not ( "type" in resource_def['schema'] and resource_def['schema']['type'] == "object" ): diff --git a/src/authzee/jmespath.py b/src/authzee/jmespath.py index d64e77f..0c744c8 100644 --- a/src/authzee/jmespath.py +++ b/src/authzee/jmespath.py @@ -99,14 +99,17 @@ def _func_inner_join( for l in lhs: for r in rhs: # expref.visit(expref.expression, element) # this is how they do it internal to jmespath python?? - if search( - expr, - { - "lhs": l, - "rhs": r - }, - options=self._custom_options - ) is True: + if ( + search( + expr, + { + "lhs": l, + "rhs": r + }, + options=self._custom_options + ) + is True + ): result.append( { "lhs": l, @@ -273,7 +276,11 @@ def _func_is_identity_present(itype: str, request: dict) -> bool: def _func_regex_find( pattern: str, subject: Union[str, List[str]] - ) -> Union[None, str, List[Union[None, str]]]: + ) -> Union[ + None, + str, + List[Union[None, str]] + ]: if type(subject) is str: match = re.search(pattern, subject) if match is not None: @@ -310,7 +317,10 @@ def _func_regex_find( def _func_regex_find_all( pattern: str, subject: Union[str, List[str]] - ) -> Union[List[str], List[List[str]]]: + ) -> Union[ + List[str], + List[List[str]] + ]: if type(subject) is str: return re.findall(pattern, subject) @@ -338,7 +348,16 @@ def _func_regex_find_all( def _func_regex_groups( pattern: str, subject: Union[str, List[str]] - ) -> Union[None, List[Union[None, str]], List[Union[None, List[Union[None, str]]]]]: + ) -> Union[ + None, + List[Union[None, str]], + List[ + Union[ + None, + List[Union[None, str]] + ] + ] + ]: if type(subject) is str: match = re.search(pattern, subject) if match is not None: @@ -375,7 +394,10 @@ def _func_regex_groups( def _func_regex_groups_all( pattern: str, subject: Union[str, List[str]] - ) -> Union[List[str], List[List[str]]]: + ) -> Union[ + List[str], + List[List[str]] + ]: if type(subject) is str: return [list(m.groups()) if m is not None else None for m in re.finditer(pattern, subject)] diff --git a/src/authzee/reference.py b/src/authzee/reference.py index 2b94b6f..c117ac0 100644 --- a/src/authzee/reference.py +++ b/src/authzee/reference.py @@ -48,6 +48,7 @@ "validate_grants", "validate_identity_defs", "validate_request", + "validate_request_result_schema", "validate_resource_defs" ] @@ -97,7 +98,6 @@ "https://json-schema.org/draft/2020-12/vocab/content": True }, "$dynamicAnchor": "meta", - "title": "Core and Validation specifications meta-schema", "allOf": [ { @@ -577,19 +577,30 @@ } } } -validate_batch_request_result_schema = { +validate_request_result_schema = { "$schema": "https://json-schema.org/draft/2020-12/schema", "title": "Request Validation Result", "description": "Request Validation Result schema.", "type": "object", "additionalProperties": False, + "required": [ + "error" + ], + "properties": { + "error": generic_error_schema + } +} +validate_batch_request_result_schema = { + "description": "Batch request Validation Result schema.", + "type": "object", + "additionalProperties": False, "required": [ "error", - "batch_errors" + "batch" ], "properties": { "error": generic_error_schema, - "batch_errors": { + "batch": { "type": "array", "description": "Each result corresponds to the batch request item of the same index.", "items": generic_error_schema @@ -955,7 +966,7 @@ def validate_batch_request( "error_type": "request", "message": f"The batch request is not valid. Schema Error: {exc}" }, - "batch_errors": [] + "batch": [] } identity_lut = {i['identity_type']: i for i in identity_defs} @@ -972,7 +983,7 @@ def validate_batch_request( "error_type": "request", "message": err }, - "batch_errors": [] + "batch": [] } err = _validate_request_resource( @@ -987,7 +998,7 @@ def validate_batch_request( "error_type": "request", "message": err }, - "batch_errors": [] + "batch": [] } err = _validate_request_context( @@ -1001,10 +1012,10 @@ def validate_batch_request( "error_type": "request", "message": err }, - "batch_errors": [] + "batch": [] } - batch_errors = [] + batch = [] for item in batch_request['batch']: item_err = None if ( @@ -1016,9 +1027,12 @@ def validate_batch_request( identity_lut=identity_lut ) - if item_err is None and ( - item.get("resource_type", None) is not None - or item.get("resource", None) is not None + if ( + item_err is None + and ( + item.get("resource_type", None) is not None + or item.get("resource", None) is not None + ) ): item_err = _validate_request_resource( resource_type=item.get("resource_type", batch_request['resource_type']), @@ -1027,9 +1041,12 @@ def validate_batch_request( resource_lut=resource_lut ) - if item_err is None and ( - item.get("context_type", None) is not None - or item.get("context", None) is not None + if ( + item_err is None + and ( + item.get("context_type", None) is not None + or item.get("context", None) is not None + ) ): item_err = _validate_request_context( context_type=item.get("context_type", batch_request['context_type']), @@ -1038,18 +1055,18 @@ def validate_batch_request( ) if item_err is not None: - batch_errors.append( + batch.append( { "error_type": "request", "message": item_err } ) else: - batch_errors.append(None) + batch.append(None) return { "error": None, - "batch_errors": batch_errors + "batch": batch } @@ -1093,7 +1110,10 @@ def audit( request: Dict[str, AnyJSON], grants: List[Dict[str, AnyJSON]], execute: Callable[[str, AnyJSON], AnyJSON] -) -> Dict[str, List[Dict[str, AnyJSON]]]: +) -> Dict[ + str, + List[Dict[str, AnyJSON]] +]: result = { "results": [], "error": None @@ -1189,7 +1209,7 @@ def _validate( return { "error": None, - "batch_errors": req_val['batch_errors'] + "batch": req_val['batch'] } req_val = validate_request( @@ -1262,7 +1282,10 @@ def batch_audit( batch_request: Dict[str, AnyJSON], grants: List[Dict[str, AnyJSON]], execute: Callable[[str, AnyJSON], AnyJSON] -) -> Dict[str, List[Dict[str, AnyJSON]]]: +) -> Dict[ + str, + List[Dict[str, AnyJSON]] +]: batch_results = [] for item in batch_request['batch']: request = { @@ -1302,7 +1325,10 @@ def batch_authorize( batch_request: Dict[str, AnyJSON], grants: List[Dict[str, AnyJSON]], execute: Callable[[str, AnyJSON], AnyJSON] -) -> Dict[str, List[Dict[str, AnyJSON]]]: +) -> Dict[ + str, + List[Dict[str, AnyJSON]] +]: results = [] for item in batch_request['batch']: results.append( @@ -1353,9 +1379,9 @@ def batch_audit_workflow( batch = [] batch_results_indexes = [] for error, request, i in zip( - val['batch_errors'], + val['batch'], batch_request['batch'], - range(len(val['batch_errors'])) + range(len(val['batch'])) ): if error is None: batch_results.append(None) @@ -1404,9 +1430,9 @@ def batch_authorize_workflow( batch = [] batch_results_indexes = [] for error, request, i in zip( - val['batch_errors'], + val['batch'], batch_request['batch'], - range(len(val['batch_errors'])) + range(len(val['batch'])) ): if error is None: batch_results.append(None) diff --git a/src/authzee/types/authzee.py b/src/authzee/types/authzee.py index 9f34b6a..173844b 100644 --- a/src/authzee/types/authzee.py +++ b/src/authzee/types/authzee.py @@ -29,7 +29,8 @@ "ResourceDefResult", "ResourceDefsPage", "StorageLatch", - "StorageLatchResult" + "StorageLatchResult", + "ValidateBatchRequestResult" ] from typing import Any, Dict, List, Literal, TypedDict @@ -677,6 +678,30 @@ class AuthzeeBatchRequest(TypedDict): batch: List[BatchItem] +class ValidateBatchRequestResult(TypedDict): + """Result for validating a batch request. + + Examples + -------- + ```python + { + "error": { # OR None + "error_type": "request", + "message": "This is a batch level error, and the whole think fails, + }, + "batch": [ + None, + { # OR None + "error_type": "request", + "message": "This is an error for the batch item." + } + ] + } + """ + error: AuthzeeError | None + batch: List[GenericResult] + + class ExecuteResult(TypedDict): """```python Dict[str, Any] diff --git a/tests/unit/test_in_process_compute.py b/tests/unit/test_in_process_compute.py index 5183f15..db8ecef 100644 --- a/tests/unit/test_in_process_compute.py +++ b/tests/unit/test_in_process_compute.py @@ -783,7 +783,8 @@ def test_in_process_validate_batch_request_invalid_batch_item(seeded_compute): config=config ) ) - assert result['error'] is not None + assert result['error'] is None + assert result['batch'][0] is not None def test_in_process_audit(seeded_compute): diff --git a/tests/unit/test_reference.py b/tests/unit/test_reference.py index 4d0e76a..13c4b58 100644 --- a/tests/unit/test_reference.py +++ b/tests/unit/test_reference.py @@ -558,7 +558,7 @@ def test_validate_batch_request_item_invalid_identity( identity_defs, resource_defs ) - assert r['batch_errors'][0] is not None + assert r['batch'][0] is not None def test_validate_batch_request_item_overrides_resource( From a802d94bbc70f6c54133ea5f0933823a1f2c47c7 Mon Sep 17 00:00:00 2001 From: btemplep Date: Sat, 22 Aug 2026 18:52:36 -0400 Subject: [PATCH 2/9] formatted --- tests/unit/test_authzee.py | 24 ++++-- tests/unit/test_authzee_async.py | 117 +++++++++++++++++++++----- tests/unit/test_in_process_compute.py | 8 +- tests/unit/test_reference.py | 42 ++------- 4 files changed, 128 insertions(+), 63 deletions(-) diff --git a/tests/unit/test_authzee.py b/tests/unit/test_authzee.py index c6c243f..2efe1c5 100644 --- a/tests/unit/test_authzee.py +++ b/tests/unit/test_authzee.py @@ -448,7 +448,8 @@ def test_delete_context_def_not_found(authz): def test_validate_context_def_with_config(authz, context_def): result = authz.validate_context_def( - context_def, config={ + context_def, + config={ "authzee": { "raise_errors": True } @@ -459,7 +460,8 @@ def test_validate_context_def_with_config(authz, context_def): def test_put_context_def_with_config(authz, context_def): result = authz.put_context_def( - context_def, config={ + context_def, + config={ "authzee": { "raise_errors": False } @@ -560,7 +562,8 @@ def test_delete_identity_def_not_found(authz): def test_validate_identity_def_with_config(authz, identity_def): result = authz.validate_identity_def( - identity_def, config={ + identity_def, + config={ "authzee": { "raise_errors": True } @@ -664,7 +667,8 @@ def test_delete_resource_def_not_found(authz): def test_validate_resource_def_with_config(authz, resource_def): result = authz.validate_resource_def( - resource_def, config={ + resource_def, + config={ "authzee": { "raise_errors": True } @@ -882,7 +886,8 @@ def test_authorize_denied_by_deny_grant(seeded_authz, deny_grant): def test_authorize_with_config(seeded_authz, auth_request): result = seeded_authz.authorize( - request=auth_request, config={ + request=auth_request, + config={ "authzee": { "raise_errors": True } @@ -916,7 +921,8 @@ def test_audit_paginator(seeded_authz, auth_request): def test_audit_with_config(seeded_authz, auth_request): result = seeded_authz.audit( - request=auth_request, config={ + request=auth_request, + config={ "authzee": { "raise_errors": True } @@ -940,7 +946,8 @@ def test_batch_authorize_all_authorized(seeded_authz, batch_request): def test_batch_authorize_with_config(seeded_authz, batch_request): result = seeded_authz.batch_authorize( - batch_request=batch_request, config={ + batch_request=batch_request, + config={ "authzee": { "raise_errors": True } @@ -972,7 +979,8 @@ def test_batch_audit_paginator(seeded_authz, batch_request): def test_batch_audit_with_config(seeded_authz, batch_request): result = seeded_authz.batch_audit( - batch_request=batch_request, config={ + batch_request=batch_request, + config={ "authzee": { "raise_errors": True } diff --git a/tests/unit/test_authzee_async.py b/tests/unit/test_authzee_async.py index 7457226..158b361 100644 --- a/tests/unit/test_authzee_async.py +++ b/tests/unit/test_authzee_async.py @@ -474,7 +474,8 @@ def test_delete_context_def_not_found(authz): def test_validate_context_def_with_config(authz, context_def): result = asyncio.run( authz.validate_context_def( - context_def, config={ + context_def, + config={ "authzee": { "raise_errors": True } @@ -606,7 +607,8 @@ def test_delete_identity_def_not_found(authz): def test_validate_identity_def_with_config(authz, identity_def): result = asyncio.run( authz.validate_identity_def( - identity_def, config={ + identity_def, + config={ "authzee": { "raise_errors": True } @@ -729,7 +731,8 @@ def test_delete_resource_def_not_found(authz): def test_validate_resource_def_with_config(authz, resource_def): result = asyncio.run( authz.validate_resource_def( - resource_def, config={ + resource_def, + config={ "authzee": { "raise_errors": True } @@ -745,7 +748,13 @@ def test_validate_grant_valid(authz, grant): def test_validate_grant_invalid(authz): - result = asyncio.run(authz.validate_grant({"effect": "bad"})) + result = asyncio.run( + authz.validate_grant( + { + "effect": "bad" + } + ) + ) assert result['error'] is not None @@ -755,7 +764,13 @@ def test_enact_grant(authz, grant): def test_enact_invalid_grant(authz): - result = asyncio.run(authz.enact({"effect": "bad"})) + result = asyncio.run( + authz.enact( + { + "effect": "bad" + } + ) + ) assert result['error'] is not None @@ -962,7 +977,8 @@ def test_authorize_denied_by_deny_grant(seeded_authz, deny_grant): def test_authorize_with_config(seeded_authz, auth_request): result = asyncio.run( seeded_authz.authorize( - request=auth_request, config={ + request=auth_request, + config={ "authzee": { "raise_errors": True } @@ -1002,7 +1018,8 @@ async def _collect(): def test_audit_with_config(seeded_authz, auth_request): result = asyncio.run( seeded_authz.audit( - request=auth_request, config={ + request=auth_request, + config={ "authzee": { "raise_errors": True } @@ -1032,7 +1049,8 @@ def test_batch_authorize_all_authorized(seeded_authz, batch_request): def test_batch_authorize_with_config(seeded_authz, batch_request): result = asyncio.run( seeded_authz.batch_authorize( - batch_request=batch_request, config={ + batch_request=batch_request, + config={ "authzee": { "raise_errors": True } @@ -1072,7 +1090,8 @@ async def _collect(): def test_batch_audit_with_config(seeded_authz, batch_request): result = asyncio.run( seeded_authz.batch_audit( - batch_request=batch_request, config={ + batch_request=batch_request, + config={ "authzee": { "raise_errors": True } @@ -1191,7 +1210,13 @@ def test_raise_errors_grant_error(): asyncio.run(authz.construct()) asyncio.run(authz.start()) with pytest.raises(exceptions.GrantError): - asyncio.run(authz.validate_grant({"effect": "bad"})) + asyncio.run( + authz.validate_grant( + { + "effect": "bad" + } + ) + ) def test_compute_storage_kwargs_override(): @@ -1367,7 +1392,13 @@ def test_raise_result_raises_on_critical_definition_error(storage_dict): asyncio.run(a.start()) # Try to put an invalid context def - should raise with pytest.raises(exceptions.DefinitionError): - asyncio.run(a.put_context_def({"bad": "data"})) + asyncio.run( + a.put_context_def( + { + "bad": "data" + } + ) + ) def test_raise_result_raises_on_critical_resource_not_found(storage_dict): @@ -1410,7 +1441,13 @@ def test_combine_errors_called_during_start(storage_dict): def test_authorize_validation_failure(seeded_authz): """authorize with an invalid request returns failure without raising.""" - result = asyncio.run(seeded_authz.authorize({"bad": "request"})) + result = asyncio.run( + seeded_authz.authorize( + { + "bad": "request" + } + ) + ) assert result['error'] is not None assert result['is_authorized'] is False assert result['error'] is not None @@ -1435,12 +1472,24 @@ def test_authorize_validation_failure_raises(storage_dict): asyncio.run(a.construct()) asyncio.run(a.start()) with pytest.raises(Exception): - asyncio.run(a.authorize({"bad": "request"})) + asyncio.run( + a.authorize( + { + "bad": "request" + } + ) + ) def test_audit_validation_failure(seeded_authz): """audit with an invalid request returns failure.""" - result = asyncio.run(seeded_authz.audit({"bad": "request"})) + result = asyncio.run( + seeded_authz.audit( + { + "bad": "request" + } + ) + ) assert result['error'] is not None assert result['results'] == [] assert result['results'] == [] @@ -1465,12 +1514,24 @@ def test_audit_validation_failure_raises(storage_dict): asyncio.run(a.construct()) asyncio.run(a.start()) with pytest.raises(Exception): - asyncio.run(a.audit({"bad": "request"})) + asyncio.run( + a.audit( + { + "bad": "request" + } + ) + ) def test_batch_audit_validation_failure(seeded_authz): """batch_audit with an invalid request returns failure.""" - result = asyncio.run(seeded_authz.batch_audit({"bad": "request"})) + result = asyncio.run( + seeded_authz.batch_audit( + { + "bad": "request" + } + ) + ) assert result['error'] is not None assert result['grants'] == [] assert result['batch'] == [] @@ -1495,7 +1556,13 @@ def test_batch_audit_validation_failure_raises(storage_dict): asyncio.run(a.construct()) asyncio.run(a.start()) with pytest.raises(Exception): - asyncio.run(a.batch_audit({"bad": "request"})) + asyncio.run( + a.batch_audit( + { + "bad": "request" + } + ) + ) def test_batch_authorize_validation_failure(seeded_authz): @@ -1530,7 +1597,13 @@ def test_batch_authorize_validation_failure_raises(storage_dict): asyncio.run(a.construct()) asyncio.run(a.start()) with pytest.raises(Exception): - asyncio.run(a.batch_authorize({"bad": "request"})) + asyncio.run( + a.batch_authorize( + { + "bad": "request" + } + ) + ) def test_compute_storage_kwargs_override(storage_dict): @@ -1620,7 +1693,13 @@ def test_validate_request_valid(seeded_authz): def test_validate_request_invalid(seeded_authz): - result = asyncio.run(seeded_authz.validate_request({"bad": "data"})) + result = asyncio.run( + seeded_authz.validate_request( + { + "bad": "data" + } + ) + ) assert result['error'] is not None diff --git a/tests/unit/test_in_process_compute.py b/tests/unit/test_in_process_compute.py index db8ecef..32ccf94 100644 --- a/tests/unit/test_in_process_compute.py +++ b/tests/unit/test_in_process_compute.py @@ -277,7 +277,13 @@ async def run(): def test_in_process_compute_shutdown(compute): - result = asyncio.run(compute.shutdown(config={"storage": {}})) + result = asyncio.run( + compute.shutdown( + config={ + "storage": {} + } + ) + ) assert result['error'] is None diff --git a/tests/unit/test_reference.py b/tests/unit/test_reference.py index 13c4b58..c2fde3e 100644 --- a/tests/unit/test_reference.py +++ b/tests/unit/test_reference.py @@ -710,11 +710,7 @@ def test_evaluate_one_query_failure_applicable_on_failure( def test_audit_applicable_grant(admin_request, allow_grant): - r = audit( - admin_request, - [allow_grant], - execute - ) + r = audit(admin_request, [allow_grant], execute) assert r['results'][0]['is_applicable'] is True assert r['error'] is None @@ -724,11 +720,7 @@ def test_audit_no_applicable_grant(guest_request, allow_grant): def test_audit_failure_recorded(admin_request, allow_grant): - r = audit( - admin_request, - [allow_grant], - failing_execute - ) + r = audit(admin_request, [allow_grant], failing_execute) assert r['results'][0]['is_applicable'] is False assert r['results'][0]['failure'] is not None @@ -740,11 +732,7 @@ def test_audit_empty_grants(admin_request): def test_authorize_allow_grant(admin_request, allow_grant): - r = authorize( - admin_request, - [allow_grant], - execute - ) + r = authorize(admin_request, [allow_grant], execute) assert r['is_authorized'] is True assert r['grant'] == allow_grant @@ -760,11 +748,7 @@ def test_authorize_deny_grant(banned_request, allow_grant, deny_grant): def test_authorize_no_applicable_grant(guest_request, allow_grant): - r = authorize( - guest_request, - [allow_grant], - execute - ) + r = authorize(guest_request, [allow_grant], execute) assert r['is_authorized'] is False assert r['grant'] is None assert "implicitly denied" in r['message'] @@ -779,21 +763,13 @@ def test_authorize_deny_checked_before_allow( **deny_grant, "query": "request.identities.User[0].role == 'admin'" } - r = authorize( - admin_request, - [allow_grant, deny], - execute - ) + r = authorize(admin_request, [allow_grant, deny], execute) assert r['is_authorized'] is False assert r['grant']['effect'] == "deny" def test_batch_audit_basic(base_batch, allow_grant): - r = batch_audit( - base_batch, - [allow_grant], - execute - ) + r = batch_audit(base_batch, [allow_grant], execute) assert len(r['batch']) == 1 assert r['batch'][0]['results'][0]['is_applicable'] is True @@ -829,11 +805,7 @@ def test_batch_audit_multiple_items(base_batch, allow_grant): def test_batch_authorize_basic(base_batch, allow_grant): - r = batch_authorize( - base_batch, - [allow_grant], - execute - ) + r = batch_authorize(base_batch, [allow_grant], execute) assert r['batch'][0]['is_authorized'] is True From 6b40f5e902b089d128248cac430a4605a5bc1db3 Mon Sep 17 00:00:00 2001 From: btemplep Date: Sun, 23 Aug 2026 23:50:21 -0400 Subject: [PATCH 3/9] request_validation to follow config is almost done --- src/authzee/compute/in_process_compute.py | 202 +++++++++++++++++++--- src/authzee/module_locality.py | 4 +- 2 files changed, 179 insertions(+), 27 deletions(-) diff --git a/src/authzee/compute/in_process_compute.py b/src/authzee/compute/in_process_compute.py index 68a47f0..f001f1c 100644 --- a/src/authzee/compute/in_process_compute.py +++ b/src/authzee/compute/in_process_compute.py @@ -7,7 +7,7 @@ "InProcessCompute" ] -from asyncio import Task, create_task, gather +from asyncio import as_completed, create_task, gather, Task from typing import Any, Callable, Dict, List, Type import jsonschema_rs @@ -131,24 +131,178 @@ async def validate_request( if result['error'] is not None: return result - context_def_task = create_task( - self._storage.get_context_def( - request['context_type'], - config['get_identity_def'] - ) - ) - resource_def_task = create_task( - self._storage.get_resource_def( - request['resource_type'], - config['get_resource_def'] - ) - ) - identity_def_tasks = [ - create_task(self._storage.get_identity_def(it, config['get_identity_def'])) - for it in request['identities'] - ] + context_def: ContextDef = None + cd_page_ref = None + cd_task: Task = None + cd_stop = False + id_lookup: Dict[str, IdentityDef] = {id_type: None for id_type in request['identities']} + id_page_ref = None + id_task: Task = None + id_stop = False + resource_def: ResourceDef = None + rd_page_ref = None + rd_task: Task = None + rd_stop = False + while ( + cd_stop is False + or id_stop is False + or rd_stop is None + ): + if cd_stop is False: + if config['use_list_context_defs'] is True: + if cd_task is None: + cd_task = create_task( + self._storage.list_context_defs( + page_ref=None, + config=config['list_context_defs'] + ) + ) + else: + cd_page: ContextDefsPage = await cd_task + if cd_page['error'] is not None: + # TODO cancel all tasks + return { + "error": cd_page['error'] + } + + cd_page_ref = cd_page['next_page_ref'] + for cd in cd_page['context_defs']: + if cd['context_type'] == request['context_type']: + context_def = cd + cd_stop = True + break + + if cd_page_ref is None: + cd_stop = True + elif cd_stop is False: + cd_task = create_task( + self._storage.list_context_defs( + page_ref=cd_page_ref, + config=config['list_context_defs'] + ) + ) + + else: + if cd_task is None: + cd_task = create_task( + self._storage.get_context_def( + request['context_type'], + config['get_identity_def'] + ) + ) + else: + cd_result: ContextDefResult = await cd_task + if ( + cd_result['error'] is not None + and cd_result['error']['error_type'] != "resource_not_found" + ): + # TODO cancel all tasks + return { + "error": cd_result['error'] + } + + context_def = cd_result['context_def'] + cd_stop = True + + if id_stop is False: + if config['use_list_identity_defs'] is True: + if id_task is None: + id_task = create_task( + self._storage.list_identity_defs( + page_ref=None, + config=config['list_identity_defs'] + ) + ) + else: + id_page: IdentityDefsPage = await id_task + id_page_ref = id_page['next_page_ref'] + for id in id_page['identity_defs']: + if id['identity_type'] in request['identities']: + id_lookup[id['identity_type']] = id + id_stop = True + for id in id_lookup.values(): + if id is None: + id_stop = False + break + + if id_page_ref is None: + id_stop = True + elif id_stop is False: + id_task = create_task( + self._storage.list_identity_defs( + page_ref=id_page_ref, + config=config['list_identity_defs'] + ) + ) + + else: + if id_task is None: + id_task = [ + create_task(self._storage.get_identity_def(it, config['get_identity_def'])) + for it in request['identities'] + ] + else: + cancel_tasks + for t in as_completed(id_task): + idr: IdentityDefResult = await t + if idr['error'] is not None: + if idr['error']['error_type'] != "resource_not_found": + # TODO cancel all tasks + return { + "error": idr['error'] + } + + else: + # TOD cancel all IDR tasks + break + + id_stop = True + + if rd_stop is False: + if config['use_list_resource_defs'] is True: + if rd_task is None: + rd_task = create_task( + self._storage.list_resource_defs( + page_ref=None, + config=config['list_resource_defs'] + ) + ) + else: + rd_page: ResourceDefsPage = await rd_task + if rd_page['error'] is not None: + # TODO cancel all tasks + return { + "error": rd_page['error'] + } + + rd_page_ref = rd_page['next_page_ref'] + for rd in rd_page['resource_defs']: + if rd['resource_type'] == request['resource_type']: + resource_def = rd + rd_stop = True + + if rd_page_ref is None: + rd_stop = True + elif rd_stop is False: + rd_task = create_task( + self._storage.list_resource_defs( + page_ref=rd_page_ref, + config=config['list_resource_defs'] + ) + ) + + else: + if rd_task is None: + rd_task = create_task( + self._storage.get_resource_def( + request['resource_type'], + config['get_identity_def'] + ) + ) + else: + resource_def = (await rd_task)['resource_def'] + rd_stop = True - context_def = (await context_def_task)['context_def'] if context_def is None: return { "error": { @@ -168,7 +322,6 @@ async def validate_request( } } - resource_def = (await resource_def_task)['resource_def'] if resource_def is None: return { "error": { @@ -200,9 +353,8 @@ async def validate_request( } } - for id_task, i_type in zip(identity_def_tasks, request['identities']): - identity_def = (await id_task)['identity_def'] - if identity_def is None: + for i_type, id in id_lookup.items(): + if id is None: return { "error": { "error_type": "request", @@ -210,12 +362,12 @@ async def validate_request( } } - id_validator = jsonschema_rs.validator_for(identity_def['schema']) - for id, i in zip( + identity_validator = jsonschema_rs.validator_for(id['schema']) + for identity, i in zip( request['identities'][i_type], range(len(request['identities'][i_type])) ): - if id_validator.is_valid(id) is False: + if identity_validator.is_valid(identity) is False: return { "error": { "error_type": "request", diff --git a/src/authzee/module_locality.py b/src/authzee/module_locality.py index 5d0b06e..382cf6f 100644 --- a/src/authzee/module_locality.py +++ b/src/authzee/module_locality.py @@ -1,8 +1,8 @@ """See [](authzee.module_locality.ModuleLocality)""" __all__ = [ - "ModuleLocality", - "locality_compatibility" + "locality_compatibility", + "ModuleLocality" ] from enum import Enum From 1757615ad4fe23be2ac3751920a839f3e9a75a65 Mon Sep 17 00:00:00 2001 From: btemplep Date: Tue, 25 Aug 2026 00:04:34 -0400 Subject: [PATCH 4/9] first try for request validate revamp. Needs tested --- src/authzee/compute/in_process_compute.py | 83 +++++++++++++++++++---- 1 file changed, 71 insertions(+), 12 deletions(-) diff --git a/src/authzee/compute/in_process_compute.py b/src/authzee/compute/in_process_compute.py index f001f1c..4ac7bf6 100644 --- a/src/authzee/compute/in_process_compute.py +++ b/src/authzee/compute/in_process_compute.py @@ -7,8 +7,8 @@ "InProcessCompute" ] -from asyncio import as_completed, create_task, gather, Task -from typing import Any, Callable, Dict, List, Type +from asyncio import as_completed, create_task, gather, sleep, Task +from typing import Any, Callable, Dict, Iterable, List, Type import jsonschema_rs @@ -47,6 +47,25 @@ class InProcessCompute(ComputeModule): + def __init__(self): + super().__init__() + self._bg_cancel_tasks: set[Task] = set() + + + def _bg_cancel(self, t: Task | Iterable[Task]) -> None: + if isinstance(t, Task) is True: + t = [t] + + for task in t: + task.done() + if task.done() is True: + continue + + self._bg_cancel_tasks.add(task) + task.add_done_callback(self._bg_cancel_tasks.discard) + task.cancel() + + async def start( self, execute: Callable[[str, Any], Any], @@ -72,6 +91,11 @@ async def start( async def shutdown(self, config: ComputeShutdownConfig) -> GenericResult: await self._storage.shutdown(config['storage']) + while True: + if len(self._bg_cancel_tasks) > 0: + await sleep(1) + else: + break return { "error": None @@ -131,13 +155,14 @@ async def validate_request( if result['error'] is not None: return result + all_tasks: set[Task] = set() context_def: ContextDef = None cd_page_ref = None cd_task: Task = None cd_stop = False id_lookup: Dict[str, IdentityDef] = {id_type: None for id_type in request['identities']} id_page_ref = None - id_task: Task = None + id_task: Task | set[Task] = None id_stop = False resource_def: ResourceDef = None rd_page_ref = None @@ -157,10 +182,13 @@ async def validate_request( config=config['list_context_defs'] ) ) + all_tasks.add(cd_task) + cd_task.add_done_callback(all_tasks.discard) else: cd_page: ContextDefsPage = await cd_task if cd_page['error'] is not None: - # TODO cancel all tasks + self._bg_cancel_tasks(all_tasks) + return { "error": cd_page['error'] } @@ -181,6 +209,8 @@ async def validate_request( config=config['list_context_defs'] ) ) + all_tasks.add(cd_task) + cd_task.add_done_callback(all_tasks.discard) else: if cd_task is None: @@ -190,13 +220,16 @@ async def validate_request( config['get_identity_def'] ) ) + all_tasks.add(cd_task) + cd_task.add_done_callback(all_tasks.discard) else: cd_result: ContextDefResult = await cd_task if ( cd_result['error'] is not None and cd_result['error']['error_type'] != "resource_not_found" ): - # TODO cancel all tasks + self._bg_cancel_tasks(all_tasks) + return { "error": cd_result['error'] } @@ -213,6 +246,8 @@ async def validate_request( config=config['list_identity_defs'] ) ) + all_tasks.add(id_task) + id_task.add_done_callback(all_tasks.discard) else: id_page: IdentityDefsPage = await id_task id_page_ref = id_page['next_page_ref'] @@ -234,26 +269,31 @@ async def validate_request( config=config['list_identity_defs'] ) ) + all_tasks.add(id_task) + id_task.add_done_callback(all_tasks.discard) else: if id_task is None: - id_task = [ + id_task = { create_task(self._storage.get_identity_def(it, config['get_identity_def'])) for it in request['identities'] - ] + } + all_tasks.update(id_task) + for t in id_task: + t.add_done_callback(all_tasks.discard) else: - cancel_tasks for t in as_completed(id_task): idr: IdentityDefResult = await t if idr['error'] is not None: if idr['error']['error_type'] != "resource_not_found": - # TODO cancel all tasks + self._bg_cancel_tasks(all_tasks) + return { "error": idr['error'] } else: - # TOD cancel all IDR tasks + self._bg_cancel_tasks(id_task) break id_stop = True @@ -267,10 +307,13 @@ async def validate_request( config=config['list_resource_defs'] ) ) + all_tasks.add(rd_task) + rd_task.add_done_callback(all_tasks.discard) else: rd_page: ResourceDefsPage = await rd_task if rd_page['error'] is not None: - # TODO cancel all tasks + self._bg_cancel_tasks(all_tasks) + return { "error": rd_page['error'] } @@ -280,6 +323,7 @@ async def validate_request( if rd['resource_type'] == request['resource_type']: resource_def = rd rd_stop = True + break if rd_page_ref is None: rd_stop = True @@ -290,6 +334,8 @@ async def validate_request( config=config['list_resource_defs'] ) ) + all_tasks.add(rd_task) + rd_task.add_done_callback(all_tasks.discard) else: if rd_task is None: @@ -299,8 +345,21 @@ async def validate_request( config['get_identity_def'] ) ) + all_tasks.add(rd_task) + rd_task.add_done_callback(all_tasks.discard) else: - resource_def = (await rd_task)['resource_def'] + rd_result: ResourceDefResult = await rd_task + if ( + rd_result['error'] is not None + and rd_result['error']['error_type'] != "resource_not_found" + ): + self._bg_cancel_tasks(all_tasks) + + return { + "error": rd_result['error'] + } + + resource_def = rd_result['resource_def'] rd_stop = True if context_def is None: From 391cdf85d355ef31032d9fdbb7d5212c4e0cf6d5 Mon Sep 17 00:00:00 2001 From: btemplep Date: Tue, 25 Aug 2026 23:53:32 -0400 Subject: [PATCH 5/9] some pre-prep for the batch request validate --- .vscode/settings.json | 7 + CHANGELOG.md | 14 +- full_example.py | 1 - src/authzee/authzee.py | 14 +- src/authzee/authzee_async.py | 16 +- src/authzee/compute/compute_module.py | 4 +- src/authzee/compute/in_process_compute.py | 67 +--- src/authzee/config.py | 78 ++--- src/authzee/jmespath.py | 55 +-- src/authzee/reference.py | 152 ++++----- src/authzee/storage/dict_storage.py | 5 +- src/authzee/types/authzee.py | 60 ++-- tests/unit/test_authzee.py | 20 -- tests/unit/test_authzee_async.py | 39 --- tests/unit/test_in_process_compute.py | 391 ++++++++++++++++++++-- 15 files changed, 560 insertions(+), 363 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 11f85e7..9da5117 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,6 +1,13 @@ { "autoDocstring.docstringFormat": "numpy", "python.defaultInterpreterPath": "./venv/bin/python", + "emeraldwalk.runonsave": { + "commands": [ + { + "cmd": "./venv/bin/cleer inspect --log-level DEBUG ${file}" + } + ] + }, "cSpell.words": [ "afunc", "aioboto", diff --git a/CHANGELOG.md b/CHANGELOG.md index aeddedd..3a88215 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,7 +22,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Security --> -## [0.1.0a6] - 2026-08-22 +## [0.1.0a6] - 2026-08-25 Support for Authzee spec 0.5.0. @@ -33,9 +33,21 @@ Support for Authzee spec 0.5.0. ### Changed +- Updated typing for Python 3.11+ + - `List[X]` → `list[X]`, `Dict[X, Y]` → `dict[X, Y]`, `Union[X, Y]` → `X | Y` + - Removed `List`, `Dict`, `Union` from typing imports - `validate_batch_request` now returns `ValidateBatchRequestResult` with `{error, batch}` instead of `GenericResult` - `batch` contains per-item validation errors (or None for valid items) - `validate_batch_request_result_schema` renamed `batch_errors` field to `batch` +- `validate_request` in `InProcessCompute` now respects the full `ValidateRequestConfig` + - Uses `use_list_context_defs`, `use_list_identity_defs`, `use_list_resource_defs` config options +- `validate_batch_request` in `InProcessCompute` now returns per-item errors in `batch` instead of failing fast + +### Removed + +- `compute_storage_kwargs` parameter from `Authzee` and `AuthzeeAsync` + - Compute module now receives `storage_kwargs` directly + - If different storage kwargs are needed, create a separate Authzee instance with `InProcessCompute` ## [0.1.0a5] - 2026-08-19 diff --git a/full_example.py b/full_example.py index d22fc4c..5fa6f19 100644 --- a/full_example.py +++ b/full_example.py @@ -33,7 +33,6 @@ storage_kwargs={ # KWArgs for storage module instances "storage_dict": storage_dict }, - compute_storage_kwargs=None, # Optional override storage KWArgs for compute module config={ # Optional AuthzeeConfigOverride "authzee": { "raise_errors": True # raise exceptions on errors diff --git a/src/authzee/authzee.py b/src/authzee/authzee.py index a709c37..0b5ec13 100644 --- a/src/authzee/authzee.py +++ b/src/authzee/authzee.py @@ -6,7 +6,7 @@ import asyncio import datetime -from typing import Any, Callable, Dict, Type +from typing import Any, Callable, Type from authzee.authzee_async import AuthzeeAsync from authzee.compute.compute_module import ComputeModule @@ -24,14 +24,12 @@ class Authzee: JSON query function. compute_type : Type[ComputeModule] Compute Module Type. - compute_kwargs : Dict[str, Any] + compute_kwargs : dict[str, Any] Compute module KWArgs used to create instances. storage_type : Type[StorageModule] Storage Module Type. - storage_kwargs : Dict[str, Any] + storage_kwargs : dict[str, Any] Storage module KWArgs used to create instances. - compute_storage_kwargs : Dict[str, Any], optional - Override storage module KWArgs that the compute module will use. May only include KWArgs you want to override. config : AuthzeeConfigOverride, optional Authzee configuration. May only include config keys you want to override. @@ -187,10 +185,9 @@ def __init__( self, execute: Callable[[str, Any], Any], compute_type: Type[ComputeModule], - compute_kwargs: Dict[str, Any], + compute_kwargs: dict[str, Any], storage_type: Type[StorageModule], - storage_kwargs: Dict[str, Any], - compute_storage_kwargs: Dict[str, Any]=None, + storage_kwargs: dict[str, Any], config: AuthzeeConfigOverride=None ): self._authzee_async = AuthzeeAsync( @@ -199,7 +196,6 @@ def __init__( compute_kwargs=compute_kwargs, storage_type=storage_type, storage_kwargs=storage_kwargs, - compute_storage_kwargs=compute_storage_kwargs, config=config ) diff --git a/src/authzee/authzee_async.py b/src/authzee/authzee_async.py index 917f591..9a458a7 100644 --- a/src/authzee/authzee_async.py +++ b/src/authzee/authzee_async.py @@ -6,7 +6,7 @@ from asyncio import gather import datetime -from typing import Any, Callable, Dict, Type +from typing import Any, Callable, Type from authzee.compute.compute_module import ComputeModule from authzee.config import default_config, override_config @@ -28,14 +28,12 @@ class AuthzeeAsync: JSON query function. compute_type : Type[ComputeModule] Compute Module Type. - compute_kwargs : Dict[str, Any] + compute_kwargs : dict[str, Any] Compute module KWArgs used to create instances. storage_type : Type[StorageModule] Storage Module Type. - storage_kwargs : Dict[str, Any] + storage_kwargs : dict[str, Any] Storage module KWArgs used to create instances. - compute_storage_kwargs : Dict[str, Any], optional - Override storage module KWArgs that the compute module will use. May only include KWArgs you want to override. config : AuthzeeConfigOverride, optional Authzee configuration. May only include config keys you want to override. @@ -188,10 +186,9 @@ def __init__( self, execute: Callable[[str, Any], Any], compute_type: Type[ComputeModule], - compute_kwargs: Dict[str, Any], + compute_kwargs: dict[str, Any], storage_type: Type[StorageModule], - storage_kwargs: Dict[str, Any], - compute_storage_kwargs: Dict[str, Any]=None, + storage_kwargs: dict[str, Any], config: AuthzeeConfigOverride=None ): self._execute = execute @@ -199,7 +196,6 @@ def __init__( self._compute_kwargs = compute_kwargs self._storage_type = storage_type self._storage_kwargs = storage_kwargs - self._compute_storage_kwargs = storage_kwargs if compute_storage_kwargs is None else storage_kwargs | compute_storage_kwargs self._config: AuthzeeConfig = override_config(config, default_config) self._compute: ComputeModule = None self._storage: StorageModule = None @@ -287,7 +283,7 @@ async def start( self._compute.start( execute=self._execute, storage_type=self._storage_type, - storage_kwargs=self._compute_storage_kwargs, + storage_kwargs=self._storage_kwargs, config=config['start']['compute_start'] ), self._storage.start(config['start']['storage_start']) diff --git a/src/authzee/compute/compute_module.py b/src/authzee/compute/compute_module.py index 12b6cda..a9c0003 100644 --- a/src/authzee/compute/compute_module.py +++ b/src/authzee/compute/compute_module.py @@ -7,7 +7,7 @@ "ComputeModule" ] -from typing import Any, Callable, Dict, Type +from typing import Any, Callable, Type from authzee.exceptions import NotImplementedError from authzee.module_locality import ModuleLocality @@ -38,7 +38,7 @@ async def start( self, execute: Callable[[str, Any], Any], storage_type: Type[StorageModule], - storage_kwargs: Dict[str, Any], + storage_kwargs: dict[str, Any], config: ComputeStartConfig ) -> GenericResult: """Start up compute module. diff --git a/src/authzee/compute/in_process_compute.py b/src/authzee/compute/in_process_compute.py index 4ac7bf6..5ea75a3 100644 --- a/src/authzee/compute/in_process_compute.py +++ b/src/authzee/compute/in_process_compute.py @@ -8,7 +8,7 @@ ] from asyncio import as_completed, create_task, gather, sleep, Task -from typing import Any, Callable, Dict, Iterable, List, Type +from typing import Any, Callable, Type import jsonschema_rs @@ -47,30 +47,11 @@ class InProcessCompute(ComputeModule): - def __init__(self): - super().__init__() - self._bg_cancel_tasks: set[Task] = set() - - - def _bg_cancel(self, t: Task | Iterable[Task]) -> None: - if isinstance(t, Task) is True: - t = [t] - - for task in t: - task.done() - if task.done() is True: - continue - - self._bg_cancel_tasks.add(task) - task.add_done_callback(self._bg_cancel_tasks.discard) - task.cancel() - - async def start( self, execute: Callable[[str, Any], Any], storage_type: Type[StorageModule], - storage_kwargs: Dict[str, Any], + storage_kwargs: dict[str, Any], config: ComputeStartConfig ) -> GenericResult: await super().start( @@ -91,11 +72,6 @@ async def start( async def shutdown(self, config: ComputeShutdownConfig) -> GenericResult: await self._storage.shutdown(config['storage']) - while True: - if len(self._bg_cancel_tasks) > 0: - await sleep(1) - else: - break return { "error": None @@ -155,12 +131,11 @@ async def validate_request( if result['error'] is not None: return result - all_tasks: set[Task] = set() context_def: ContextDef = None cd_page_ref = None cd_task: Task = None cd_stop = False - id_lookup: Dict[str, IdentityDef] = {id_type: None for id_type in request['identities']} + id_lookup: dict[str, IdentityDef] = {id_type: None for id_type in request['identities']} id_page_ref = None id_task: Task | set[Task] = None id_stop = False @@ -182,13 +157,9 @@ async def validate_request( config=config['list_context_defs'] ) ) - all_tasks.add(cd_task) - cd_task.add_done_callback(all_tasks.discard) else: cd_page: ContextDefsPage = await cd_task if cd_page['error'] is not None: - self._bg_cancel_tasks(all_tasks) - return { "error": cd_page['error'] } @@ -209,8 +180,6 @@ async def validate_request( config=config['list_context_defs'] ) ) - all_tasks.add(cd_task) - cd_task.add_done_callback(all_tasks.discard) else: if cd_task is None: @@ -220,16 +189,12 @@ async def validate_request( config['get_identity_def'] ) ) - all_tasks.add(cd_task) - cd_task.add_done_callback(all_tasks.discard) else: cd_result: ContextDefResult = await cd_task if ( cd_result['error'] is not None and cd_result['error']['error_type'] != "resource_not_found" ): - self._bg_cancel_tasks(all_tasks) - return { "error": cd_result['error'] } @@ -246,8 +211,6 @@ async def validate_request( config=config['list_identity_defs'] ) ) - all_tasks.add(id_task) - id_task.add_done_callback(all_tasks.discard) else: id_page: IdentityDefsPage = await id_task id_page_ref = id_page['next_page_ref'] @@ -269,8 +232,6 @@ async def validate_request( config=config['list_identity_defs'] ) ) - all_tasks.add(id_task) - id_task.add_done_callback(all_tasks.discard) else: if id_task is None: @@ -278,22 +239,18 @@ async def validate_request( create_task(self._storage.get_identity_def(it, config['get_identity_def'])) for it in request['identities'] } - all_tasks.update(id_task) - for t in id_task: - t.add_done_callback(all_tasks.discard) else: for t in as_completed(id_task): idr: IdentityDefResult = await t if idr['error'] is not None: if idr['error']['error_type'] != "resource_not_found": - self._bg_cancel_tasks(all_tasks) - return { "error": idr['error'] } else: - self._bg_cancel_tasks(id_task) + # remove references to tasks so GC cleans + id_task = None break id_stop = True @@ -307,13 +264,9 @@ async def validate_request( config=config['list_resource_defs'] ) ) - all_tasks.add(rd_task) - rd_task.add_done_callback(all_tasks.discard) else: rd_page: ResourceDefsPage = await rd_task if rd_page['error'] is not None: - self._bg_cancel_tasks(all_tasks) - return { "error": rd_page['error'] } @@ -334,8 +287,6 @@ async def validate_request( config=config['list_resource_defs'] ) ) - all_tasks.add(rd_task) - rd_task.add_done_callback(all_tasks.discard) else: if rd_task is None: @@ -345,16 +296,12 @@ async def validate_request( config['get_identity_def'] ) ) - all_tasks.add(rd_task) - rd_task.add_done_callback(all_tasks.discard) else: rd_result: ResourceDefResult = await rd_task if ( rd_result['error'] is not None and rd_result['error']['error_type'] != "resource_not_found" ): - self._bg_cancel_tasks(all_tasks) - return { "error": rd_result['error'] } @@ -460,7 +407,7 @@ async def validate_batch_request( "batch": [] } - batch_tasks: List[Task] = [] + batch_tasks: list[Task] = [] for item in batch_request['batch']: batch_tasks.append( create_task( @@ -471,7 +418,7 @@ async def validate_batch_request( ) ) - batch_results: List[GenericResult] = await gather(*batch_tasks) + batch_results: list[GenericResult] = await gather(*batch_tasks) batch: list = [] for bt_result in batch_results: if bt_result['error'] is not None: diff --git a/src/authzee/config.py b/src/authzee/config.py index 6ee0fe0..5f61085 100644 --- a/src/authzee/config.py +++ b/src/authzee/config.py @@ -82,9 +82,9 @@ "get_context_def": { "use_cache": True }, - "use_list_context_defs": True, + "use_list_context_defs": False, "list_context_defs": { - "page_size": 100, + "page_size": 1000, "use_cache": True }, "get_identity_def": { @@ -92,15 +92,15 @@ }, "use_list_identity_defs": True, "list_identity_defs": { - "page_size": 100, + "page_size": 1000, "use_cache": True }, "get_resource_def": { "use_cache": True }, - "use_list_resource_defs": True, + "use_list_resource_defs": False, "list_resource_defs": { - "page_size": 100, + "page_size": 1000, "use_cache": True } }, @@ -110,7 +110,7 @@ }, "use_list_context_defs": True, "list_context_defs": { - "page_size": 100, + "page_size": 1000, "use_cache": True }, "get_identity_def": { @@ -118,7 +118,7 @@ }, "use_list_identity_defs": True, "list_identity_defs": { - "page_size": 100, + "page_size": 1000, "use_cache": True }, "get_resource_def": { @@ -126,7 +126,7 @@ }, "use_list_resource_defs": True, "list_resource_defs": { - "page_size": 100, + "page_size": 1000, "use_cache": True } }, @@ -135,9 +135,9 @@ "get_context_def": { "use_cache": True }, - "use_list_context_defs": True, + "use_list_context_defs": False, "list_context_defs": { - "page_size": 100, + "page_size": 1000, "use_cache": True }, "get_identity_def": { @@ -145,15 +145,15 @@ }, "use_list_identity_defs": True, "list_identity_defs": { - "page_size": 100, + "page_size": 1000, "use_cache": True }, "get_resource_def": { "use_cache": True }, - "use_list_resource_defs": True, + "use_list_resource_defs": False, "list_resource_defs": { - "page_size": 100, + "page_size": 1000, "use_cache": True } }, @@ -169,7 +169,7 @@ }, "use_list_context_defs": True, "list_context_defs": { - "page_size": 100, + "page_size": 1000, "use_cache": True }, "get_identity_def": { @@ -177,7 +177,7 @@ }, "use_list_identity_defs": True, "list_identity_defs": { - "page_size": 100, + "page_size": 1000, "use_cache": True }, "get_resource_def": { @@ -185,7 +185,7 @@ }, "use_list_resource_defs": True, "list_resource_defs": { - "page_size": 100, + "page_size": 1000, "use_cache": True } }, @@ -199,9 +199,9 @@ "get_context_def": { "use_cache": True }, - "use_list_context_defs": True, + "use_list_context_defs": False, "list_context_defs": { - "page_size": 100, + "page_size": 1000, "use_cache": True }, "get_identity_def": { @@ -209,20 +209,20 @@ }, "use_list_identity_defs": True, "list_identity_defs": { - "page_size": 100, + "page_size": 1000, "use_cache": True }, "get_resource_def": { "use_cache": True }, - "use_list_resource_defs": True, + "use_list_resource_defs": False, "list_resource_defs": { - "page_size": 100, + "page_size": 1000, "use_cache": True } }, "list_grants": { - "page_size": 100, + "page_size": 1000, "use_cache": True }, "parallel_paging": True, @@ -238,7 +238,7 @@ }, "use_list_context_defs": True, "list_context_defs": { - "page_size": 100, + "page_size": 1000, "use_cache": True }, "get_identity_def": { @@ -246,7 +246,7 @@ }, "use_list_identity_defs": True, "list_identity_defs": { - "page_size": 100, + "page_size": 1000, "use_cache": True }, "get_resource_def": { @@ -254,38 +254,12 @@ }, "use_list_resource_defs": True, "list_resource_defs": { - "page_size": 100, - "use_cache": True - } - }, - "validate_request": { - "get_context_def": { - "use_cache": True - }, - "use_list_context_defs": True, - "list_context_defs": { - "page_size": 100, - "use_cache": True - }, - "get_identity_def": { - "use_cache": True - }, - "use_list_identity_defs": True, - "list_identity_defs": { - "page_size": 100, - "use_cache": True - }, - "get_resource_def": { - "use_cache": True - }, - "use_list_resource_defs": True, - "list_resource_defs": { - "page_size": 100, + "page_size": 1000, "use_cache": True } }, "list_grants": { - "page_size": 100, + "page_size": 1000, "use_cache": True }, "parallel_paging": True, diff --git a/src/authzee/jmespath.py b/src/authzee/jmespath.py index 0c744c8..54c7122 100644 --- a/src/authzee/jmespath.py +++ b/src/authzee/jmespath.py @@ -10,7 +10,7 @@ ] import re -from typing import Any, Dict, List, Union +from typing import Any try: @@ -91,10 +91,10 @@ def __init__(self): ) def _func_inner_join( self, - lhs: List[Any], - rhs: List[Any], + lhs: list[Any], + rhs: list[Any], expr: str - ) -> List[Dict[str, Any]]: + ) -> list[dict[str, Any]]: result = [] for l in lhs: for r in rhs: @@ -139,10 +139,10 @@ def _func_inner_join( ) def _func_left_join( self, - lhs: List[Any], - rhs: List[Any], + lhs: list[Any], + rhs: list[Any], expr: str - ) -> List[Dict[str, Any]]: + ) -> list[dict[str, Any]]: result = [] for l in lhs: lhs_match = False @@ -193,10 +193,10 @@ def _func_left_join( ) def _func_outer_join( self, - lhs: List[Any], - rhs: List[Any], + lhs: list[Any], + rhs: list[Any], expr: str - ) -> List[Dict[str, Any]]: + ) -> list[dict[str, Any]]: result = [] unmatched_rhs = set(rhs) for l in lhs: @@ -275,12 +275,8 @@ def _func_is_identity_present(itype: str, request: dict) -> bool: ) def _func_regex_find( pattern: str, - subject: Union[str, List[str]] - ) -> Union[ - None, - str, - List[Union[None, str]] - ]: + subject: str | list[str] + ) -> None | str | list[None | str]: if type(subject) is str: match = re.search(pattern, subject) if match is not None: @@ -316,11 +312,8 @@ def _func_regex_find( ) def _func_regex_find_all( pattern: str, - subject: Union[str, List[str]] - ) -> Union[ - List[str], - List[List[str]] - ]: + subject: str | list[str] + ) -> list[str] | list[list[str]]: if type(subject) is str: return re.findall(pattern, subject) @@ -347,17 +340,8 @@ def _func_regex_find_all( ) def _func_regex_groups( pattern: str, - subject: Union[str, List[str]] - ) -> Union[ - None, - List[Union[None, str]], - List[ - Union[ - None, - List[Union[None, str]] - ] - ] - ]: + subject: str | list[str] + ) -> None | list[None | str] | list[None | list[None | str]]: if type(subject) is str: match = re.search(pattern, subject) if match is not None: @@ -393,11 +377,8 @@ def _func_regex_groups( ) def _func_regex_groups_all( pattern: str, - subject: Union[str, List[str]] - ) -> Union[ - List[str], - List[List[str]] - ]: + subject: str | list[str] + ) -> list[str] | list[list[str]]: if type(subject) is str: return [list(m.groups()) if m is not None else None for m in re.finditer(pattern, subject)] diff --git a/src/authzee/reference.py b/src/authzee/reference.py index c117ac0..0f7ab9b 100644 --- a/src/authzee/reference.py +++ b/src/authzee/reference.py @@ -52,20 +52,20 @@ "validate_resource_defs" ] -from typing import Callable, Dict, List, Union +from typing import Callable import jsonschema_rs -AnyJSON = Union[ - bool, - str, - int, - float, - None, - list, - dict -] +AnyJSON = ( + bool + | str + | int + | float + | None + | list + | dict +) _type_regex = "^[A-Za-z0-9_]*$" _type_schema = { @@ -690,8 +690,8 @@ def validate_context_defs( - context_defs: List[Dict[str, AnyJSON]] -) -> Dict[str, AnyJSON]: + context_defs: list[dict[str, AnyJSON]] +) -> dict[str, AnyJSON]: context_types = set() for c_def in context_defs: try: @@ -731,8 +731,8 @@ def validate_context_defs( def validate_identity_defs( - identity_defs: List[Dict[str, AnyJSON]] -) -> Dict[str, AnyJSON]: + identity_defs: list[dict[str, AnyJSON]] +) -> dict[str, AnyJSON]: id_types = [] for id_def in identity_defs: try: @@ -772,8 +772,8 @@ def validate_identity_defs( def validate_resource_defs( - resource_defs: List[Dict[str, AnyJSON]] -) -> Dict[str, AnyJSON]: + resource_defs: list[dict[str, AnyJSON]] +) -> dict[str, AnyJSON]: r_types = set() for r_def in resource_defs: try: @@ -812,7 +812,7 @@ def validate_resource_defs( } -def validate_grants(grants: List[Dict[str, AnyJSON]]) -> Dict[str, AnyJSON]: +def validate_grants(grants: list[dict[str, AnyJSON]]) -> dict[str, AnyJSON]: for g in grants: try: jsonschema_rs.validate(grant_schema, g) @@ -830,7 +830,7 @@ def validate_grants(grants: List[Dict[str, AnyJSON]]) -> Dict[str, AnyJSON]: def _validate_request_identities( - identities: Dict[str, AnyJSON], + identities: dict[str, AnyJSON], identity_lut: dict ) -> str | None: for i_type in identities: @@ -893,11 +893,11 @@ def _validate_request_context( def validate_request( - request: Dict[str, AnyJSON], - context_defs: List[Dict[str, AnyJSON]], - identity_defs: List[Dict[str, AnyJSON]], - resource_defs: List[Dict[str, AnyJSON]] -) -> Dict[str, AnyJSON]: + request: dict[str, AnyJSON], + context_defs: list[dict[str, AnyJSON]], + identity_defs: list[dict[str, AnyJSON]], + resource_defs: list[dict[str, AnyJSON]] +) -> dict[str, AnyJSON]: try: jsonschema_rs.validate(request_schema, request) except jsonschema_rs.ValidationError as exc: @@ -953,11 +953,11 @@ def validate_request( def validate_batch_request( - batch_request: Dict[str, AnyJSON], - context_defs: List[Dict[str, AnyJSON]], - identity_defs: List[Dict[str, AnyJSON]], - resource_defs: List[Dict[str, AnyJSON]] -) -> Dict[str, AnyJSON]: + batch_request: dict[str, AnyJSON], + context_defs: list[dict[str, AnyJSON]], + identity_defs: list[dict[str, AnyJSON]], + resource_defs: list[dict[str, AnyJSON]] +) -> dict[str, AnyJSON]: try: jsonschema_rs.validate(batch_request_schema, batch_request) except jsonschema_rs.ValidationError as exc: @@ -1071,10 +1071,10 @@ def validate_batch_request( def evaluate_one( - request: Dict[str, AnyJSON], - grant: Dict[str, AnyJSON], + request: dict[str, AnyJSON], + grant: dict[str, AnyJSON], execute: Callable[[str, AnyJSON], AnyJSON] -) -> Dict[str, AnyJSON]: +) -> dict[str, AnyJSON]: result = { "is_applicable": False, "query_result": None, @@ -1107,12 +1107,12 @@ def evaluate_one( def audit( - request: Dict[str, AnyJSON], - grants: List[Dict[str, AnyJSON]], + request: dict[str, AnyJSON], + grants: list[dict[str, AnyJSON]], execute: Callable[[str, AnyJSON], AnyJSON] -) -> Dict[ +) -> dict[ str, - List[Dict[str, AnyJSON]] + list[dict[str, AnyJSON]] ]: result = { "results": [], @@ -1133,10 +1133,10 @@ def audit( def authorize( - request: Dict[str, AnyJSON], - grants: List[Dict[str, AnyJSON]], + request: dict[str, AnyJSON], + grants: list[dict[str, AnyJSON]], execute: Callable[[str, AnyJSON], AnyJSON] -) -> Dict[str, AnyJSON]: +) -> dict[str, AnyJSON]: allow_grants = [] deny_grants = [] for g in grants: @@ -1174,13 +1174,13 @@ def authorize( def _validate( - context_defs: List[Dict[str, AnyJSON]], - identity_defs: List[Dict[str, AnyJSON]], - resource_defs: List[Dict[str, AnyJSON]], - grants: List[Dict[str, AnyJSON]], - request: Dict[str, AnyJSON], + context_defs: list[dict[str, AnyJSON]], + identity_defs: list[dict[str, AnyJSON]], + resource_defs: list[dict[str, AnyJSON]], + grants: list[dict[str, AnyJSON]], + request: dict[str, AnyJSON], is_batch: bool -) -> Dict[str, AnyJSON]: +) -> dict[str, AnyJSON]: c_val = validate_context_defs(context_defs) if c_val['error'] is not None: return c_val @@ -1227,13 +1227,13 @@ def _validate( def audit_workflow( - context_defs: List[Dict[str, AnyJSON]], - identity_defs: List[Dict[str, AnyJSON]], - resource_defs: List[Dict[str, AnyJSON]], - grants: List[Dict[str, AnyJSON]], - request: Dict[str, AnyJSON], + context_defs: list[dict[str, AnyJSON]], + identity_defs: list[dict[str, AnyJSON]], + resource_defs: list[dict[str, AnyJSON]], + grants: list[dict[str, AnyJSON]], + request: dict[str, AnyJSON], execute: Callable[[str, AnyJSON], AnyJSON] -) -> Dict[str, AnyJSON]: +) -> dict[str, AnyJSON]: val = _validate( context_defs, identity_defs, @@ -1252,13 +1252,13 @@ def audit_workflow( def authorize_workflow( - context_defs: List[Dict[str, AnyJSON]], - identity_defs: List[Dict[str, AnyJSON]], - resource_defs: List[Dict[str, AnyJSON]], - grants: List[Dict[str, AnyJSON]], - request: Dict[str, AnyJSON], + context_defs: list[dict[str, AnyJSON]], + identity_defs: list[dict[str, AnyJSON]], + resource_defs: list[dict[str, AnyJSON]], + grants: list[dict[str, AnyJSON]], + request: dict[str, AnyJSON], execute: Callable[[str, AnyJSON], AnyJSON] -) -> Dict[str, AnyJSON]: +) -> dict[str, AnyJSON]: val = _validate( context_defs, identity_defs, @@ -1279,12 +1279,12 @@ def authorize_workflow( def batch_audit( - batch_request: Dict[str, AnyJSON], - grants: List[Dict[str, AnyJSON]], + batch_request: dict[str, AnyJSON], + grants: list[dict[str, AnyJSON]], execute: Callable[[str, AnyJSON], AnyJSON] -) -> Dict[ +) -> dict[ str, - List[Dict[str, AnyJSON]] + list[dict[str, AnyJSON]] ]: batch_results = [] for item in batch_request['batch']: @@ -1322,12 +1322,12 @@ def batch_audit( def batch_authorize( - batch_request: Dict[str, AnyJSON], - grants: List[Dict[str, AnyJSON]], + batch_request: dict[str, AnyJSON], + grants: list[dict[str, AnyJSON]], execute: Callable[[str, AnyJSON], AnyJSON] -) -> Dict[ +) -> dict[ str, - List[Dict[str, AnyJSON]] + list[dict[str, AnyJSON]] ]: results = [] for item in batch_request['batch']: @@ -1353,13 +1353,13 @@ def batch_authorize( def batch_audit_workflow( - context_defs: List[Dict[str, AnyJSON]], - identity_defs: List[Dict[str, AnyJSON]], - resource_defs: List[Dict[str, AnyJSON]], - grants: List[Dict[str, AnyJSON]], - batch_request: Dict[str, AnyJSON], + context_defs: list[dict[str, AnyJSON]], + identity_defs: list[dict[str, AnyJSON]], + resource_defs: list[dict[str, AnyJSON]], + grants: list[dict[str, AnyJSON]], + batch_request: dict[str, AnyJSON], execute: Callable[[str, AnyJSON], AnyJSON] -) -> Dict[str, AnyJSON]: +) -> dict[str, AnyJSON]: val = _validate( context_defs, identity_defs, @@ -1405,13 +1405,13 @@ def batch_audit_workflow( def batch_authorize_workflow( - context_defs: List[Dict[str, AnyJSON]], - identity_defs: List[Dict[str, AnyJSON]], - resource_defs: List[Dict[str, AnyJSON]], - grants: List[Dict[str, AnyJSON]], - batch_request: Dict[str, AnyJSON], + context_defs: list[dict[str, AnyJSON]], + identity_defs: list[dict[str, AnyJSON]], + resource_defs: list[dict[str, AnyJSON]], + grants: list[dict[str, AnyJSON]], + batch_request: dict[str, AnyJSON], execute: Callable[[str, AnyJSON], AnyJSON] -) -> Dict[str, AnyJSON]: +) -> dict[str, AnyJSON]: val = _validate( context_defs, identity_defs, diff --git a/src/authzee/storage/dict_storage.py b/src/authzee/storage/dict_storage.py index 4b6daa7..52ea93c 100644 --- a/src/authzee/storage/dict_storage.py +++ b/src/authzee/storage/dict_storage.py @@ -8,7 +8,6 @@ ] import datetime -from typing import List from uuid import uuid4 from authzee.module_locality import ModuleLocality @@ -358,7 +357,7 @@ async def list_grants( else: start_index = int(page_ref) - grants: List[Grant] = list(self._storage_dict['grants_lut'].values()) + grants: list[Grant] = list(self._storage_dict['grants_lut'].values()) if effect is not None: grants = [g for g in grants if g['effect'] == effect] @@ -386,7 +385,7 @@ async def list_grant_refs( else: start_index = int(page_ref) - grants: List[Grant] = list(self._storage_dict['grants_lut'].values()) + grants: list[Grant] = list(self._storage_dict['grants_lut'].values()) if effect is not None: grants = [g for g in grants if g['effect'] == effect] diff --git a/src/authzee/types/authzee.py b/src/authzee/types/authzee.py index 173844b..ecc2e66 100644 --- a/src/authzee/types/authzee.py +++ b/src/authzee/types/authzee.py @@ -33,7 +33,7 @@ "ValidateBatchRequestResult" ] -from typing import Any, Dict, List, Literal, TypedDict +from typing import Any, Literal, TypedDict AnyJSON = ( @@ -107,7 +107,7 @@ class ContextDef(TypedDict): ``` """ context_type: str - schema: Dict[str, AnyJSON] + schema: dict[str, AnyJSON] class ContextDefResult(TypedDict): @@ -171,7 +171,7 @@ class ContextDefsPage(TypedDict): } ``` """ - context_defs: List[ContextDef] + context_defs: list[ContextDef] next_page_ref: str | None error: AuthzeeError | None @@ -262,7 +262,7 @@ class IdentityDefsPage(TypedDict): } ``` """ - identity_defs: List[IdentityDef] + identity_defs: list[IdentityDef] next_page_ref: str | None error: AuthzeeError | None @@ -292,7 +292,7 @@ class ResourceDef(TypedDict): ``` """ resource_type: str - actions: List[str] + actions: list[str] schema: dict @@ -363,7 +363,7 @@ class ResourceDefsPage(TypedDict): } ``` """ - resource_defs: List[ResourceDef] + resource_defs: list[ResourceDef] next_page_ref: str | None error: AuthzeeError | None @@ -399,13 +399,13 @@ class Grant(TypedDict): grant_uuid: str name: str description: str - tags: Dict[str, str] + tags: dict[str, str] effect: Literal["allow", "deny"] - actions: List[str] + actions: list[str] query: str equality: AnyJSON applicable_on_failure: bool - data: Dict[str, Any] + data: dict[str, Any] class GrantResult(TypedDict): @@ -477,7 +477,7 @@ class GrantsPage(TypedDict): } ``` """ - grants: List[Grant] + grants: list[Grant] next_page_ref: str | None error: AuthzeeError | None @@ -502,7 +502,7 @@ class PageRefsPage(TypedDict): } ``` """ - page_refs: List[str] + page_refs: list[str] next_page_ref: str | None error: AuthzeeError | None @@ -581,15 +581,15 @@ class AuthzeeRequest(TypedDict): } ``` """ - identities: Dict[ + identities: dict[ str, - List[Dict[str, AnyJSON]] + list[dict[str, AnyJSON]] ] action: str resource_type: str - resource: Dict[str, AnyJSON] + resource: dict[str, AnyJSON] context_type: str - context: Dict[str, AnyJSON] + context: dict[str, AnyJSON] class BatchItem(TypedDict, total=False): @@ -619,14 +619,14 @@ class BatchItem(TypedDict, total=False): } ``` """ - identities: Dict[ + identities: dict[ str, - List[Dict[str, AnyJSON]] + list[dict[str, AnyJSON]] ] | None resource_type: str | None - resource: Dict[str, AnyJSON] | None + resource: dict[str, AnyJSON] | None context_type: str | None - context: Dict[str, AnyJSON] | None + context: dict[str, AnyJSON] | None class AuthzeeBatchRequest(TypedDict): @@ -666,16 +666,16 @@ class AuthzeeBatchRequest(TypedDict): } ``` """ - identities: Dict[ + identities: dict[ str, - List[Dict[str, AnyJSON]] + list[dict[str, AnyJSON]] ] action: str resource_type: str - resource: Dict[str, AnyJSON] + resource: dict[str, AnyJSON] context_type: str - context: Dict[str, AnyJSON] - batch: List[BatchItem] + context: dict[str, AnyJSON] + batch: list[BatchItem] class ValidateBatchRequestResult(TypedDict): @@ -699,7 +699,7 @@ class ValidateBatchRequestResult(TypedDict): } """ error: AuthzeeError | None - batch: List[GenericResult] + batch: list[GenericResult] class ExecuteResult(TypedDict): @@ -807,7 +807,7 @@ class AuditResultPage(TypedDict): } ``` """ - results: List[AuditResultItem] + results: list[AuditResultItem] next_page_ref: str | None error: AuthzeeError | None @@ -872,7 +872,7 @@ class BatchAuditResultItem(TypedDict): } ``` """ - results: List[EvaluateResult] + results: list[EvaluateResult] error: AuthzeeError | None @@ -921,8 +921,8 @@ class BatchAuditResultPage(TypedDict): } ``` """ - grants: List[Grant] - batch: List[BatchAuditResultItem] + grants: list[Grant] + batch: list[BatchAuditResultItem] next_page_ref: str | None error: AuthzeeError | None @@ -964,5 +964,5 @@ class BatchAuthorizeResult(TypedDict): } ``` """ - batch: List[AuthorizeResult] + batch: list[AuthorizeResult] error: AuthzeeError | None diff --git a/tests/unit/test_authzee.py b/tests/unit/test_authzee.py index 2efe1c5..188b771 100644 --- a/tests/unit/test_authzee.py +++ b/tests/unit/test_authzee.py @@ -1099,26 +1099,6 @@ def test_raise_errors_grant_error(): ) -def test_compute_storage_kwargs_override(): - """Test that compute_storage_kwargs is accepted.""" - storage_dict = {} - authz = Authzee( - execute=jmespath_execute, - compute_type=InProcessCompute, - compute_kwargs={}, - storage_type=DictStorage, - storage_kwargs={ - "storage_dict": storage_dict - }, - compute_storage_kwargs={ - "storage_dict": storage_dict - } - ) - authz.construct() - result = authz.start() - assert result['error'] is None - - def test_put_context_def_overwrite(authz, context_def): """Putting the same context_type twice should succeed (upsert).""" authz.put_context_def(context_def) diff --git a/tests/unit/test_authzee_async.py b/tests/unit/test_authzee_async.py index 158b361..e92fec9 100644 --- a/tests/unit/test_authzee_async.py +++ b/tests/unit/test_authzee_async.py @@ -1219,26 +1219,6 @@ def test_raise_errors_grant_error(): ) -def test_compute_storage_kwargs_override(): - """Test that compute_storage_kwargs is accepted.""" - storage_dict = {} - authz = AuthzeeAsync( - execute=jmespath_execute, - compute_type=InProcessCompute, - compute_kwargs={}, - storage_type=DictStorage, - storage_kwargs={ - "storage_dict": storage_dict - }, - compute_storage_kwargs={ - "storage_dict": storage_dict - } - ) - asyncio.run(authz.construct()) - result = asyncio.run(authz.start()) - assert result['error'] is None - - def test_put_context_def_overwrite(authz, context_def): """Putting the same context_type twice should succeed (upsert).""" asyncio.run(authz.put_context_def(context_def)) @@ -1606,25 +1586,6 @@ def test_batch_authorize_validation_failure_raises(storage_dict): ) -def test_compute_storage_kwargs_override(storage_dict): - """Test that compute_storage_kwargs overrides storage_kwargs for compute.""" - a = AuthzeeAsync( - execute=jmespath_execute, - compute_type=InProcessCompute, - compute_kwargs={}, - storage_type=DictStorage, - storage_kwargs={ - "storage_dict": storage_dict - }, - compute_storage_kwargs={ - "storage_dict": storage_dict - } - ) - asyncio.run(a.construct()) - result = asyncio.run(a.start()) - assert result['error'] is None - - def test_validate_batch_request_valid(seeded_authz): batch_request = { "identities": { diff --git a/tests/unit/test_in_process_compute.py b/tests/unit/test_in_process_compute.py index 32ccf94..44da952 100644 --- a/tests/unit/test_in_process_compute.py +++ b/tests/unit/test_in_process_compute.py @@ -440,8 +440,23 @@ def test_in_process_validate_request_valid(seeded_compute): } config = { "get_context_def": {}, + "use_list_context_defs": False, + "list_context_defs": { + "page_size": 1000, + "use_cache": True + }, "get_identity_def": {}, - "get_resource_def": {} + "use_list_identity_defs": True, + "list_identity_defs": { + "page_size": 1000, + "use_cache": True + }, + "get_resource_def": {}, + "use_list_resource_defs": False, + "list_resource_defs": { + "page_size": 1000, + "use_cache": True + } } result = asyncio.run( seeded_compute.validate_request( @@ -485,8 +500,23 @@ def test_in_process_validate_request_unknown_context_type(seeded_compute): } config = { "get_context_def": {}, + "use_list_context_defs": False, + "list_context_defs": { + "page_size": 1000, + "use_cache": True + }, "get_identity_def": {}, - "get_resource_def": {} + "use_list_identity_defs": True, + "list_identity_defs": { + "page_size": 1000, + "use_cache": True + }, + "get_resource_def": {}, + "use_list_resource_defs": False, + "list_resource_defs": { + "page_size": 1000, + "use_cache": True + } } result = asyncio.run( seeded_compute.validate_request( @@ -521,8 +551,23 @@ def test_in_process_validate_request_invalid_context_data(seeded_compute): } config = { "get_context_def": {}, + "use_list_context_defs": False, + "list_context_defs": { + "page_size": 1000, + "use_cache": True + }, "get_identity_def": {}, - "get_resource_def": {} + "use_list_identity_defs": True, + "list_identity_defs": { + "page_size": 1000, + "use_cache": True + }, + "get_resource_def": {}, + "use_list_resource_defs": False, + "list_resource_defs": { + "page_size": 1000, + "use_cache": True + } } result = asyncio.run( seeded_compute.validate_request( @@ -554,8 +599,23 @@ def test_in_process_validate_request_unknown_resource_type(seeded_compute): } config = { "get_context_def": {}, + "use_list_context_defs": False, + "list_context_defs": { + "page_size": 1000, + "use_cache": True + }, "get_identity_def": {}, - "get_resource_def": {} + "use_list_identity_defs": True, + "list_identity_defs": { + "page_size": 1000, + "use_cache": True + }, + "get_resource_def": {}, + "use_list_resource_defs": False, + "list_resource_defs": { + "page_size": 1000, + "use_cache": True + } } result = asyncio.run( seeded_compute.validate_request( @@ -587,8 +647,23 @@ def test_in_process_validate_request_invalid_resource_data(seeded_compute): } config = { "get_context_def": {}, + "use_list_context_defs": False, + "list_context_defs": { + "page_size": 1000, + "use_cache": True + }, "get_identity_def": {}, - "get_resource_def": {} + "use_list_identity_defs": True, + "list_identity_defs": { + "page_size": 1000, + "use_cache": True + }, + "get_resource_def": {}, + "use_list_resource_defs": False, + "list_resource_defs": { + "page_size": 1000, + "use_cache": True + } } result = asyncio.run( seeded_compute.validate_request( @@ -620,8 +695,23 @@ def test_in_process_validate_request_invalid_action(seeded_compute): } config = { "get_context_def": {}, + "use_list_context_defs": False, + "list_context_defs": { + "page_size": 1000, + "use_cache": True + }, "get_identity_def": {}, - "get_resource_def": {} + "use_list_identity_defs": True, + "list_identity_defs": { + "page_size": 1000, + "use_cache": True + }, + "get_resource_def": {}, + "use_list_resource_defs": False, + "list_resource_defs": { + "page_size": 1000, + "use_cache": True + } } result = asyncio.run( seeded_compute.validate_request( @@ -653,8 +743,23 @@ def test_in_process_validate_request_unknown_identity_type(seeded_compute): } config = { "get_context_def": {}, + "use_list_context_defs": False, + "list_context_defs": { + "page_size": 1000, + "use_cache": True + }, "get_identity_def": {}, - "get_resource_def": {} + "use_list_identity_defs": True, + "list_identity_defs": { + "page_size": 1000, + "use_cache": True + }, + "get_resource_def": {}, + "use_list_resource_defs": False, + "list_resource_defs": { + "page_size": 1000, + "use_cache": True + } } result = asyncio.run( seeded_compute.validate_request( @@ -686,8 +791,23 @@ def test_in_process_validate_request_invalid_identity_data(seeded_compute): } config = { "get_context_def": {}, + "use_list_context_defs": False, + "list_context_defs": { + "page_size": 1000, + "use_cache": True + }, "get_identity_def": {}, - "get_resource_def": {} + "use_list_identity_defs": True, + "list_identity_defs": { + "page_size": 1000, + "use_cache": True + }, + "get_resource_def": {}, + "use_list_resource_defs": False, + "list_resource_defs": { + "page_size": 1000, + "use_cache": True + } } result = asyncio.run( seeded_compute.validate_request( @@ -727,8 +847,23 @@ def test_in_process_validate_batch_request_valid(seeded_compute): } config = { "get_context_def": {}, + "use_list_context_defs": False, + "list_context_defs": { + "page_size": 1000, + "use_cache": True + }, "get_identity_def": {}, - "get_resource_def": {} + "use_list_identity_defs": True, + "list_identity_defs": { + "page_size": 1000, + "use_cache": True + }, + "get_resource_def": {}, + "use_list_resource_defs": False, + "list_resource_defs": { + "page_size": 1000, + "use_cache": True + } } result = asyncio.run( seeded_compute.validate_batch_request( @@ -780,8 +915,23 @@ def test_in_process_validate_batch_request_invalid_batch_item(seeded_compute): } config = { "get_context_def": {}, + "use_list_context_defs": False, + "list_context_defs": { + "page_size": 1000, + "use_cache": True + }, "get_identity_def": {}, - "get_resource_def": {} + "use_list_identity_defs": True, + "list_identity_defs": { + "page_size": 1000, + "use_cache": True + }, + "get_resource_def": {}, + "use_list_resource_defs": False, + "list_resource_defs": { + "page_size": 1000, + "use_cache": True + } } result = asyncio.run( seeded_compute.validate_batch_request( @@ -815,8 +965,23 @@ def test_in_process_audit(seeded_compute): config = { "validate_request": { "get_context_def": {}, + "use_list_context_defs": False, + "list_context_defs": { + "page_size": 1000, + "use_cache": True + }, "get_identity_def": {}, - "get_resource_def": {} + "use_list_identity_defs": True, + "list_identity_defs": { + "page_size": 1000, + "use_cache": True + }, + "get_resource_def": {}, + "use_list_resource_defs": False, + "list_resource_defs": { + "page_size": 1000, + "use_cache": True + } }, "list_grants": { "page_size": 100, @@ -857,8 +1022,23 @@ def test_in_process_authorize_allowed(seeded_compute): config = { "validate_request": { "get_context_def": {}, + "use_list_context_defs": False, + "list_context_defs": { + "page_size": 1000, + "use_cache": True + }, "get_identity_def": {}, - "get_resource_def": {} + "use_list_identity_defs": True, + "list_identity_defs": { + "page_size": 1000, + "use_cache": True + }, + "get_resource_def": {}, + "use_list_resource_defs": False, + "list_resource_defs": { + "page_size": 1000, + "use_cache": True + } }, "list_grants": { "page_size": 100, @@ -894,8 +1074,23 @@ def test_in_process_authorize_denied(seeded_compute): config = { "validate_request": { "get_context_def": {}, + "use_list_context_defs": False, + "list_context_defs": { + "page_size": 1000, + "use_cache": True + }, "get_identity_def": {}, - "get_resource_def": {} + "use_list_identity_defs": True, + "list_identity_defs": { + "page_size": 1000, + "use_cache": True + }, + "get_resource_def": {}, + "use_list_resource_defs": False, + "list_resource_defs": { + "page_size": 1000, + "use_cache": True + } }, "list_grants": { "page_size": 100, @@ -932,8 +1127,23 @@ def test_in_process_authorize_implicit_deny(seeded_compute): config = { "validate_request": { "get_context_def": {}, + "use_list_context_defs": False, + "list_context_defs": { + "page_size": 1000, + "use_cache": True + }, "get_identity_def": {}, - "get_resource_def": {} + "use_list_identity_defs": True, + "list_identity_defs": { + "page_size": 1000, + "use_cache": True + }, + "get_resource_def": {}, + "use_list_resource_defs": False, + "list_resource_defs": { + "page_size": 1000, + "use_cache": True + } }, "list_grants": { "page_size": 100, @@ -984,8 +1194,23 @@ def test_in_process_batch_audit(seeded_compute): config = { "validate_batch_request": { "get_context_def": {}, + "use_list_context_defs": False, + "list_context_defs": { + "page_size": 1000, + "use_cache": True + }, "get_identity_def": {}, - "get_resource_def": {} + "use_list_identity_defs": True, + "list_identity_defs": { + "page_size": 1000, + "use_cache": True + }, + "get_resource_def": {}, + "use_list_resource_defs": False, + "list_resource_defs": { + "page_size": 1000, + "use_cache": True + } }, "list_grants": { "page_size": 100, @@ -1039,8 +1264,23 @@ def test_in_process_batch_authorize_mixed(seeded_compute): config = { "validate_batch_request": { "get_context_def": {}, + "use_list_context_defs": False, + "list_context_defs": { + "page_size": 1000, + "use_cache": True + }, "get_identity_def": {}, - "get_resource_def": {} + "use_list_identity_defs": True, + "list_identity_defs": { + "page_size": 1000, + "use_cache": True + }, + "get_resource_def": {}, + "use_list_resource_defs": False, + "list_resource_defs": { + "page_size": 1000, + "use_cache": True + } }, "list_grants": { "page_size": 100, @@ -1090,8 +1330,23 @@ def test_in_process_batch_authorize_deny(seeded_compute): config = { "validate_batch_request": { "get_context_def": {}, + "use_list_context_defs": False, + "list_context_defs": { + "page_size": 1000, + "use_cache": True + }, "get_identity_def": {}, - "get_resource_def": {} + "use_list_identity_defs": True, + "list_identity_defs": { + "page_size": 1000, + "use_cache": True + }, + "get_resource_def": {}, + "use_list_resource_defs": False, + "list_resource_defs": { + "page_size": 1000, + "use_cache": True + } }, "list_grants": { "page_size": 100, @@ -1140,8 +1395,23 @@ def test_in_process_batch_authorize_implicit_deny(seeded_compute): config = { "validate_batch_request": { "get_context_def": {}, + "use_list_context_defs": False, + "list_context_defs": { + "page_size": 1000, + "use_cache": True + }, "get_identity_def": {}, - "get_resource_def": {} + "use_list_identity_defs": True, + "list_identity_defs": { + "page_size": 1000, + "use_cache": True + }, + "get_resource_def": {}, + "use_list_resource_defs": False, + "list_resource_defs": { + "page_size": 1000, + "use_cache": True + } }, "list_grants": { "page_size": 100, @@ -1317,8 +1587,23 @@ def test_in_process_audit_storage_failure(seeded_failing_compute): config = { "validate_request": { "get_context_def": {}, + "use_list_context_defs": False, + "list_context_defs": { + "page_size": 1000, + "use_cache": True + }, "get_identity_def": {}, - "get_resource_def": {} + "use_list_identity_defs": True, + "list_identity_defs": { + "page_size": 1000, + "use_cache": True + }, + "get_resource_def": {}, + "use_list_resource_defs": False, + "list_resource_defs": { + "page_size": 1000, + "use_cache": True + } }, "list_grants": { "page_size": 100, @@ -1356,8 +1641,23 @@ def test_in_process_authorize_storage_failure(seeded_failing_compute): config = { "validate_request": { "get_context_def": {}, + "use_list_context_defs": False, + "list_context_defs": { + "page_size": 1000, + "use_cache": True + }, "get_identity_def": {}, - "get_resource_def": {} + "use_list_identity_defs": True, + "list_identity_defs": { + "page_size": 1000, + "use_cache": True + }, + "get_resource_def": {}, + "use_list_resource_defs": False, + "list_resource_defs": { + "page_size": 1000, + "use_cache": True + } }, "list_grants": { "page_size": 100, @@ -1401,8 +1701,23 @@ def test_in_process_batch_audit_storage_failure(seeded_failing_compute): config = { "validate_batch_request": { "get_context_def": {}, + "use_list_context_defs": False, + "list_context_defs": { + "page_size": 1000, + "use_cache": True + }, "get_identity_def": {}, - "get_resource_def": {} + "use_list_identity_defs": True, + "list_identity_defs": { + "page_size": 1000, + "use_cache": True + }, + "get_resource_def": {}, + "use_list_resource_defs": False, + "list_resource_defs": { + "page_size": 1000, + "use_cache": True + } }, "list_grants": { "page_size": 100, @@ -1447,8 +1762,23 @@ def test_in_process_batch_authorize_storage_failure(seeded_failing_compute): config = { "validate_batch_request": { "get_context_def": {}, + "use_list_context_defs": False, + "list_context_defs": { + "page_size": 1000, + "use_cache": True + }, "get_identity_def": {}, - "get_resource_def": {} + "use_list_identity_defs": True, + "list_identity_defs": { + "page_size": 1000, + "use_cache": True + }, + "get_resource_def": {}, + "use_list_resource_defs": False, + "list_resource_defs": { + "page_size": 1000, + "use_cache": True + } }, "list_grants": { "page_size": 100, @@ -1716,8 +2046,23 @@ def test_in_process_validate_batch_request_base_request_invalid(seeded_compute): } config = { "get_context_def": {}, + "use_list_context_defs": False, + "list_context_defs": { + "page_size": 1000, + "use_cache": True + }, "get_identity_def": {}, - "get_resource_def": {} + "use_list_identity_defs": True, + "list_identity_defs": { + "page_size": 1000, + "use_cache": True + }, + "get_resource_def": {}, + "use_list_resource_defs": False, + "list_resource_defs": { + "page_size": 1000, + "use_cache": True + } } result = asyncio.run( seeded_compute.validate_batch_request( From b55d6b07c97dcbe69d0a02f41b0135d709d5331d Mon Sep 17 00:00:00 2001 From: btemplep Date: Tue, 25 Aug 2026 23:54:50 -0400 Subject: [PATCH 6/9] format --- tests/unit/test_authzee.py | 4 ++-- tests/unit/test_authzee_async.py | 4 ++-- tests/unit/test_exceptions.py | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/unit/test_authzee.py b/tests/unit/test_authzee.py index 188b771..e0f7621 100644 --- a/tests/unit/test_authzee.py +++ b/tests/unit/test_authzee.py @@ -11,10 +11,10 @@ from authzee import ( Authzee, - DictStorage, - InProcessCompute, authzee_specification_version, + DictStorage, exceptions, + InProcessCompute, jmespath_execute, paginator ) diff --git a/tests/unit/test_authzee_async.py b/tests/unit/test_authzee_async.py index e92fec9..26d600b 100644 --- a/tests/unit/test_authzee_async.py +++ b/tests/unit/test_authzee_async.py @@ -13,10 +13,10 @@ from authzee import ( AuthzeeAsync, - DictStorage, - InProcessCompute, authzee_specification_version, + DictStorage, exceptions, + InProcessCompute, jmespath_execute, paginator_async ) diff --git a/tests/unit/test_exceptions.py b/tests/unit/test_exceptions.py index 81cf3bb..e3abc69 100644 --- a/tests/unit/test_exceptions.py +++ b/tests/unit/test_exceptions.py @@ -8,14 +8,14 @@ AuthzeeSpecError, ComputeError, DefinitionError, + _exception_map, GrantError, LocalityIncompatibilityError, NotImplementedError as AuthzeeNotImplementedError, ParallelPaginationNotSupported, RequestError, ResourceNotFoundError, - StorageError, - _exception_map + StorageError ) From 78240370eb1ae05e05e755e423cf6bc94a87da77 Mon Sep 17 00:00:00 2001 From: btemplep Date: Thu, 27 Aug 2026 22:28:00 -0400 Subject: [PATCH 7/9] tests done --- src/authzee/compute/in_process_compute.py | 566 +++-- tests/unit/compute_module_test_base.py | 1706 +++++++++++++++ tests/unit/test_in_process_compute.py | 2404 ++------------------- 3 files changed, 2329 insertions(+), 2347 deletions(-) create mode 100644 tests/unit/compute_module_test_base.py diff --git a/src/authzee/compute/in_process_compute.py b/src/authzee/compute/in_process_compute.py index 5ea75a3..dd4b7bf 100644 --- a/src/authzee/compute/in_process_compute.py +++ b/src/authzee/compute/in_process_compute.py @@ -7,7 +7,8 @@ "InProcessCompute" ] -from asyncio import as_completed, create_task, gather, sleep, Task +from asyncio import as_completed, create_task, gather, Task +import copy from typing import Any, Callable, Type import jsonschema_rs @@ -122,6 +123,93 @@ async def validate_grant( return validate_grant(grant) + def _validate_request_from_cache( + self, + request: AuthzeeRequest | BatchItem, + cd_lookup: dict[str, ContextDef | None], + id_lookup: dict[str, IdentityDef | None], + rd_lookup: dict[str, IdentityDef | None] + ) -> GenericResult: + """Must validate schema and pull of defs before this. + """ + if "context_type" in request: + cd = cd_lookup[request['context_type']] + if cd is None: + return { + "error": { + "error_type": "request", + "message": f"context_type '{request['context_type']}' is not a registered context type." + } + } + + if ( + jsonschema_rs.validator_for(cd['schema']).is_valid(request['context']) + is False + ): + return { + "error": { + "error_type": "request", + "message": f"The given context is not valid against the '{request['context_type']}' context type." + } + } + + if "resource_type" in request: + rd = rd_lookup[request['resource_type']] + if rd is None: + return { + "error": { + "error_type": "request", + "message": f"resource_type '{request['resource_type']}' is not a registered resource type." + } + } + + if ( + jsonschema_rs.validator_for(rd['schema']).is_valid(request['resource']) + is False + ): + return { + "error": { + "error_type": "request", + "message": f"The given resource is not valid against the '{request['resource_type']}' resource type." + } + } + + if request['action'] not in rd['actions']: + return { + "error": { + "error_type": "request", + "message": f"The given resource action is not valid for the '{request['resource_type']}' resource type." + } + } + + if "identities" in request: + for i_type, id in id_lookup.items(): + if id is None: + return { + "error": { + "error_type": "request", + "message": f"identity_type '{i_type}' is not a registered identity type." + } + } + + identity_validator = jsonschema_rs.validator_for(id['schema']) + for identity, i in zip( + request['identities'][i_type], + range(len(request['identities'][i_type])) + ): + if identity_validator.is_valid(identity) is False: + return { + "error": { + "error_type": "request", + "message": f"The given identity in '{i_type}[{i}]' is not valid against the '{i_type}' identity type." + } + } + + return { + "error": None + } + + async def validate_request( self, request: AuthzeeRequest, @@ -131,22 +219,22 @@ async def validate_request( if result['error'] is not None: return result - context_def: ContextDef = None + context_def: ContextDef | None = None cd_page_ref = None cd_task: Task = None cd_stop = False - id_lookup: dict[str, IdentityDef] = {id_type: None for id_type in request['identities']} + id_lookup: dict[str, IdentityDef | None] = {id_type: None for id_type in request['identities']} id_page_ref = None - id_task: Task | set[Task] = None + id_task: Task | list[Task] = None id_stop = False - resource_def: ResourceDef = None + resource_def: ResourceDef | None = None rd_page_ref = None rd_task: Task = None rd_stop = False while ( cd_stop is False or id_stop is False - or rd_stop is None + or rd_stop is False ): if cd_stop is False: if config['use_list_context_defs'] is True: @@ -186,18 +274,24 @@ async def validate_request( cd_task = create_task( self._storage.get_context_def( request['context_type'], - config['get_identity_def'] + config['get_context_def'] ) ) else: cd_result: ContextDefResult = await cd_task - if ( - cd_result['error'] is not None - and cd_result['error']['error_type'] != "resource_not_found" - ): - return { - "error": cd_result['error'] - } + if cd_result['error'] is not None: + if cd_result['error']['error_type'] == "resource_not_found": + return { + "error": { + "error_type": "request", + "message": f"context_type '{request['context_type']}' is not a registered context type." + } + } + + else: + return { + "error": cd_result['error'] + } context_def = cd_result['context_def'] cd_stop = True @@ -213,6 +307,11 @@ async def validate_request( ) else: id_page: IdentityDefsPage = await id_task + if id_page['error'] is not None: + return { + "error": id_page['error'] + } + id_page_ref = id_page['next_page_ref'] for id in id_page['identity_defs']: if id['identity_type'] in request['identities']: @@ -235,23 +334,28 @@ async def validate_request( else: if id_task is None: - id_task = { - create_task(self._storage.get_identity_def(it, config['get_identity_def'])) - for it in request['identities'] - } + id_task = [ + create_task(self._storage.get_identity_def(i_type, config['get_identity_def'])) + for i_type in id_lookup + ] else: - for t in as_completed(id_task): - idr: IdentityDefResult = await t - if idr['error'] is not None: - if idr['error']['error_type'] != "resource_not_found": + id_results: list[IdentityDefResult] = await gather(*id_task) + for id_result, i_type in zip(id_results, id_lookup): + if id_result['error'] is not None: + if id_result['error']['error_type'] == "resource_not_found": return { - "error": idr['error'] + "error": { + "error_type": "request", + "message": f"identity_type '{i_type}' is not a registered identity type." + } } else: - # remove references to tasks so GC cleans - id_task = None - break + return { + "error": id_result['error'] + } + + id_lookup[i_type] = id_result['identity_def'] id_stop = True @@ -293,97 +397,38 @@ async def validate_request( rd_task = create_task( self._storage.get_resource_def( request['resource_type'], - config['get_identity_def'] + config['get_resource_def'] ) ) else: rd_result: ResourceDefResult = await rd_task - if ( - rd_result['error'] is not None - and rd_result['error']['error_type'] != "resource_not_found" - ): - return { - "error": rd_result['error'] - } + if rd_result['error'] is not None: + if rd_result['error']['error_type'] == "resource_not_found": + return { + "error": { + "error_type": "request", + "message": f"resource_type '{request['resource_type']}' is not a registered resource type." + } + } + + else: + return { + "error": rd_result['error'] + } resource_def = rd_result['resource_def'] rd_stop = True - if context_def is None: - return { - "error": { - "error_type": "request", - "message": f"context_type '{request['context_type']}' is not a registered context type." - } - } - - if ( - jsonschema_rs.validator_for(context_def['schema']).is_valid(request['context']) - is False - ): - return { - "error": { - "error_type": "request", - "message": f"The given context is not valid against the '{request['context_type']}' context type." - } + return self._validate_request_from_cache( + request=request, + cd_lookup={ + request['context_type']: context_def + }, + id_lookup=id_lookup, + rd_lookup={ + request['resource_type']: resource_def } - - if resource_def is None: - return { - "error": { - "error_type": "request", - "message": f"resource_type '{request['resource_type']}' is not a registered resource type." - } - } - - if ( - jsonschema_rs.validator_for( - resource_def['schema'] - ).is_valid( - request['resource'] - ) - is False - ): - return { - "error": { - "error_type": "request", - "message": f"The given resource is not valid against the '{request['resource_type']}' resource type." - } - } - - if request['action'] not in resource_def['actions']: - return { - "error": { - "error_type": "request", - "message": f"The given resource action is not valid for the '{request['resource_type']}' resource type." - } - } - - for i_type, id in id_lookup.items(): - if id is None: - return { - "error": { - "error_type": "request", - "message": f"identity_type '{i_type}' is not a registered identity type." - } - } - - identity_validator = jsonschema_rs.validator_for(id['schema']) - for identity, i in zip( - request['identities'][i_type], - range(len(request['identities'][i_type])) - ): - if identity_validator.is_valid(identity) is False: - return { - "error": { - "error_type": "request", - "message": f"The given identity in '{i_type}[{i}]' is not valid against the '{i_type}' identity type." - } - } - - return { - "error": None - } + ) async def validate_batch_request( @@ -398,38 +443,291 @@ async def validate_batch_request( "batch": [] } - base_request: AuthzeeBatchRequest = batch_request.copy() - base_request.pop("batch") - base_result = await self.validate_request(request=base_request, config=config) - if base_result['error'] is not None: + #first collect lookups for context defs, identity defs and resource defs + cd_lookup: dict[str, ContextDef] = { + batch_request['context_type']: None + } + id_lookup: dict[str, IdentityDef] = {id_type: None for id_type in batch_request['identities']} + rd_lookup: dict[str, ResourceDef] = { + batch_request['resource_type']: None + } + for item in batch_request['batch']: + if ( + "context_type" in item + and item['context_type'] not in cd_lookup + ): + cd_lookup[item['context_type']] = None + + if "identities" in item: + for i_type in item['identities']: + if i_type not in id_lookup: + id_lookup[i_type] = None + + if ( + "resource_type" in item + and item['resource_type'] not in rd_lookup + ): + rd_lookup[item['resource_type']] = None + + cd_page_ref = None + cd_task: Task | list[Task] = None + cd_stop = False + id_page_ref = None + id_task: Task | list[Task] = None + id_stop = False + rd_page_ref = None + rd_task: Task | list[Task] = None + rd_stop = False + while ( + cd_stop is False + or id_stop is False + or rd_stop is False + ): + if cd_stop is False: + if config['use_list_context_defs'] is True: + if cd_task is None: + cd_task = create_task( + self._storage.list_context_defs( + page_ref=None, + config=config['list_context_defs'] + ) + ) + else: + cd_page: ContextDefsPage = await cd_task + if cd_page['error'] is not None: + return { + "batch": [], + "error": cd_page['error'] + } + + cd_page_ref = cd_page['next_page_ref'] + for cd in cd_page['context_defs']: + if cd['context_type'] in cd_lookup: + cd_lookup[cd['context_type']] = cd + cd_stop = True + for cd in cd_lookup.values(): + if cd is None: + cd_stop = False + break + + if cd_page_ref is None: + cd_stop = True + elif cd_stop is False: + cd_task = create_task( + self._storage.list_context_defs( + page_ref=cd_page_ref, + config=config['list_context_defs'] + ) + ) + + else: + if cd_task is None: + cd_task = [ + create_task(self._storage.get_context_def(c_type, config['get_context_def'])) + for c_type in cd_lookup + ] + else: + cd_results: list[ContextDefResult] = await gather(*cd_task) + for cd_result, c_type in zip(cd_results, cd_lookup): + if cd_result['error'] is None: + cd_lookup[c_type] = cd_result['context_def'] + elif c_type == batch_request['context_type']: + # if it's a root request level error, then return base errors + if cd_result['error']['error_type'] == "resource_not_found": + return { + "batch": [], + "error": { + "error_type": "request", + "message": f"context_type '{batch_request['context_type']}' is not a registered context type." + } + } + + else: + return { + "batch": [], + "error": cd_result['error'] + } + + cd_stop = True + + if id_stop is False: + if config['use_list_identity_defs'] is True: + if id_task is None: + id_task = create_task( + self._storage.list_identity_defs( + page_ref=None, + config=config['list_identity_defs'] + ) + ) + else: + id_page: IdentityDefsPage = await id_task + if id_page['error'] is not None: + return { + "batch": [], + "error": id_page['error'] + } + + id_page_ref = id_page['next_page_ref'] + for id in id_page['identity_defs']: + if id['identity_type'] in id_lookup: + id_lookup[id['identity_type']] = id + id_stop = True + for id in id_lookup.values(): + if id is None: + id_stop = False + break + + if id_page_ref is None: + id_stop = True + elif id_stop is False: + id_task = create_task( + self._storage.list_identity_defs( + page_ref=id_page_ref, + config=config['list_identity_defs'] + ) + ) + + else: + if id_task is None: + id_task = [ + create_task(self._storage.get_identity_def(i_type, config['get_identity_def'])) + for i_type in id_lookup + ] + else: + id_results: list[IdentityDefResult] = await gather(*id_task) + for id_result, i_type in zip(id_results, id_lookup): + if id_result['error'] is None: + id_lookup[i_type] = id_result['identity_def'] + elif i_type in batch_request['identities']: + # if it's a root request level error, then return base errors + if id_result['error']['error_type'] == "resource_not_found": + return { + "batch": [], + "error": { + "error_type": "request", + "message": f"identity_type '{i_type}' is not a registered identity type." + } + } + + else: + return { + "batch": [], + "error": id_result['error'] + } + + id_stop = True + + if rd_stop is False: + if config['use_list_resource_defs'] is True: + if rd_task is None: + rd_task = create_task( + self._storage.list_resource_defs( + page_ref=None, + config=config['list_resource_defs'] + ) + ) + else: + rd_page: ContextDefsPage = await rd_task + if rd_page['error'] is not None: + return { + "batch": [], + "error": rd_page['error'] + } + + rd_page_ref = rd_page['next_page_ref'] + for rd in rd_page['resource_defs']: + if rd['resource_type'] in rd_lookup: + rd_lookup[rd['resource_type']] = rd + rd_stop = True + for rd in rd_lookup.values(): + if rd is None: + rd_stop = False + break + + if rd_page_ref is None: + rd_stop = True + elif rd_stop is False: + rd_task = create_task( + self._storage.list_resource_defs( + page_ref=rd_page_ref, + config=config['list_resource_defs'] + ) + ) + + else: + if rd_task is None: + rd_task = [ + create_task(self._storage.get_resource_def(r_type, config['get_resource_def'])) + for r_type in rd_lookup + ] + else: + rd_results: list[ContextDefResult] = await gather(*rd_task) + for rd_result, r_type in zip(rd_results, rd_lookup): + if rd_result['error'] is None: + rd_lookup[r_type] = rd_result['resource_def'] + elif r_type == batch_request['resource_type']: + # if it's a root request level error, then return base errors + if rd_result['error']['error_type'] == "resource_not_found": + return { + "batch": [], + "error": { + "error_type": "request", + "message": f"resource_type '{batch_request['resource_type']}' is not a registered resource type." + } + } + + else: + return { + "batch": [], + "error": rd_result['error'] + } + + rd_stop = True + + val = self._validate_request_from_cache( + request=batch_request, + cd_lookup=cd_lookup, + id_lookup=id_lookup, + rd_lookup=rd_lookup + ) + if val['error'] is not None: return { - "error": base_result['error'], - "batch": [] + "batch": [], + "error": val['error'] } - batch_tasks: list[Task] = [] - for item in batch_request['batch']: - batch_tasks.append( - create_task( - self.validate_request( - request=base_request | item, - config=config - ) + result = { + "batch": [], + "error": None + } + for b in batch_request['batch']: + request = copy.deepcopy(b) + if "context_type" in request or "context" in request: + if "context" not in request: + request['context'] = batch_request['context'] + + if "context_type" not in request: + request['context_type'] = batch_request['context_type'] + + if "resource_type" in request or "resource" in request: + # must copy because we add to the original + request['action'] = batch_request['action'] + if "resource_type" not in request: + request['resource_type'] = batch_request['resource_type'] + + if "resource" not in request: + request['resource'] = batch_request['resource'] + + result['batch'].append( + self._validate_request_from_cache( + request=request, + cd_lookup=cd_lookup, + id_lookup=id_lookup, + rd_lookup=rd_lookup ) ) - batch_results: list[GenericResult] = await gather(*batch_tasks) - batch: list = [] - for bt_result in batch_results: - if bt_result['error'] is not None: - batch.append(bt_result['error']) - else: - batch.append(None) - - return { - "error": None, - "batch": batch - } + return result async def audit( diff --git a/tests/unit/compute_module_test_base.py b/tests/unit/compute_module_test_base.py new file mode 100644 index 0000000..bfccb8d --- /dev/null +++ b/tests/unit/compute_module_test_base.py @@ -0,0 +1,1706 @@ +"""Reusable base test suite for Authzee compute modules. + +Any concrete compute module test file can reuse this suite by importing all of +its test functions via ``from compute_module_test_base import *`` and supplying +the required pytest fixtures: + +- ``storage_dict`` +- ``compute`` +- ``seeded_compute`` +- ``failing_compute`` +- ``fail_on_allow_compute`` + +The shared test functions reference these fixtures by name so pytest resolves +them against whatever the concrete test module defines. +""" + +import asyncio +from uuid import uuid4 + + +def vr_config(use_list=False, page_size=1000): + """Build a full validate_request / validate_batch_request config. + + Arguments + --------- + use_list : bool, default=False + If ``True``, toggle all ``use_list_*_defs`` flags to ``True`` so the + list-based def lookup paths are exercised. + page_size : int, default=1000 + Page size for all ``list_*`` config blocks. Use a small value (e.g. 1) + to force multi-page pagination. + + Returns + ------- + dict + The full config object. + """ + return { + "get_context_def": {}, + "use_list_context_defs": use_list, + "list_context_defs": { + "page_size": page_size, + "use_cache": True + }, + "get_identity_def": {}, + "use_list_identity_defs": use_list, + "list_identity_defs": { + "page_size": page_size, + "use_cache": True + }, + "get_resource_def": {}, + "use_list_resource_defs": use_list, + "list_resource_defs": { + "page_size": page_size, + "use_cache": True + } + } + + +def op_config( + use_list=False, + page_size=100, + grants_page_size=100 +): + """Build a full audit / authorize / batch_* config. + + Arguments + --------- + use_list : bool, default=False + Toggle for the nested validate config def lookup paths. + page_size : int, default=100 + Page size for the nested validate config ``list_*`` blocks. + grants_page_size : int, default=100 + Page size for the ``list_grants`` block. + + Returns + ------- + dict + The full config object with nested ``validate_request`` / + ``validate_batch_request`` and a ``list_grants`` block. + """ + validate = vr_config(use_list=use_list, page_size=page_size) + + return { + "validate_request": validate, + "validate_batch_request": validate, + "list_grants": { + "page_size": grants_page_size, + "use_cache": False + } + } + + +def _base_request( + context_type="NONE", + resource_type="balloon", + action="balloon:inflate", + department="Balloon Dept", + color="blue", + is_inflated=False +): + return { + "identities": { + "user": [ + { + "username": "balloon_person", + "department": department + } + ] + }, + "action": action, + "resource_type": resource_type, + "resource": { + "color": color, + "is_inflated": is_inflated + }, + "context_type": context_type, + "context": {} + } + + +def _base_batch_request( + batch, + department="Balloon Dept", + action="balloon:inflate" +): + request = _base_request(department=department, action=action) + request['batch'] = batch + + return request + + +def _install_error_method(compute_instance, method_name): + """Patch a storage method to return a non-resource_not_found storage error. + + Arguments + --------- + compute_instance : ComputeModule + The compute instance whose ``_storage`` will be patched. + method_name : str + The storage method to patch. One of the ``get_*`` or ``list_*`` def + lookup methods. + """ + error = { + "error_type": "storage", + "message": "forced storage failure" + } + payloads = { + "get_context_def": { + "context_def": None, + "error": error + }, + "list_context_defs": { + "context_defs": [], + "next_page_ref": None, + "error": error + }, + "get_identity_def": { + "identity_def": None, + "error": error + }, + "list_identity_defs": { + "identity_defs": [], + "next_page_ref": None, + "error": error + }, + "get_resource_def": { + "resource_def": None, + "error": error + }, + "list_resource_defs": { + "resource_defs": [], + "next_page_ref": None, + "error": error + } + } + payload = payloads[method_name] + + async def _method(*args, **kwargs): + return payload + + setattr(compute_instance._storage, method_name, _method) + + +def test_base_start_locality_and_paging(compute): + from authzee.module_locality import ModuleLocality + assert compute.locality == ModuleLocality.PROCESS + assert compute.has_parallel_paging is False + + +def test_base_shutdown_returns_no_error(compute): + result = asyncio.run( + compute.shutdown( + config={ + "storage": {} + } + ) + ) + assert result['error'] is None + + +def test_base_construct_returns_no_error(compute): + result = asyncio.run(compute.construct(config={})) + assert result['error'] is None + + +def test_base_destroy_returns_no_error(compute): + result = asyncio.run(compute.destroy(config={})) + assert result['error'] is None + + +def test_base_validate_context_def_valid(compute): + result = asyncio.run( + compute.validate_context_def( + context_def={ + "context_type": "NONE", + "schema": { + "type": "object", + "additionalProperties": False + } + }, + config={} + ) + ) + assert result['error'] is None + + +def test_base_validate_context_def_invalid(compute): + result = asyncio.run( + compute.validate_context_def( + context_def={ + "bad": "data" + }, + config={} + ) + ) + assert result['error'] is not None + + +def test_base_validate_identity_def_valid(compute): + result = asyncio.run( + compute.validate_identity_def( + identity_def={ + "identity_type": "user", + "schema": { + "type": "object" + } + }, + config={} + ) + ) + assert result['error'] is None + + +def test_base_validate_identity_def_invalid(compute): + result = asyncio.run( + compute.validate_identity_def( + identity_def={ + "bad": "data" + }, + config={} + ) + ) + assert result['error'] is not None + + +def test_base_validate_resource_def_valid(compute): + result = asyncio.run( + compute.validate_resource_def( + resource_def={ + "resource_type": "file", + "actions": [ + "read" + ], + "schema": { + "type": "object" + } + }, + config={} + ) + ) + assert result['error'] is None + + +def test_base_validate_resource_def_invalid(compute): + result = asyncio.run( + compute.validate_resource_def( + resource_def={ + "bad": "data" + }, + config={} + ) + ) + assert result['error'] is not None + + +def test_base_validate_grant_valid(compute): + result = asyncio.run( + compute.validate_grant( + grant={ + "grant_uuid": str(uuid4()), + "name": "Test", + "description": "", + "tags": {}, + "effect": "allow", + "actions": [ + "read" + ], + "query": "`true`", + "equality": True, + "applicable_on_failure": False, + "data": {} + }, + config={} + ) + ) + assert result['error'] is None + + +def test_base_validate_grant_invalid(compute): + result = asyncio.run( + compute.validate_grant( + grant={ + "bad": "data" + }, + config={} + ) + ) + assert result['error'] is not None + + +def test_base_validate_request_valid_get_path(seeded_compute): + result = asyncio.run( + seeded_compute.validate_request( + request=_base_request(), + config=vr_config(use_list=False) + ) + ) + assert result['error'] is None + + +def test_base_validate_request_valid_list_path(seeded_compute): + result = asyncio.run( + seeded_compute.validate_request( + request=_base_request(), + config=vr_config(use_list=True) + ) + ) + assert result['error'] is None + + +def test_base_validate_request_valid_list_path_paginated(seeded_compute): + result = asyncio.run( + seeded_compute.validate_request( + request=_base_request(), + config=vr_config(use_list=True, page_size=1) + ) + ) + assert result['error'] is None + + +def test_base_validate_request_invalid_schema(seeded_compute): + result = asyncio.run( + seeded_compute.validate_request( + request={ + "bad": "data" + }, + config=vr_config() + ) + ) + assert result['error'] is not None + + +def test_base_validate_request_unknown_context_type_get(seeded_compute): + request = _base_request(context_type="UNKNOWN") + result = asyncio.run( + seeded_compute.validate_request( + request=request, + config=vr_config(use_list=False) + ) + ) + assert result['error'] is not None + assert result['error']['error_type'] == "request" + + +def test_base_validate_request_unknown_context_type_list(seeded_compute): + request = _base_request(context_type="UNKNOWN") + result = asyncio.run( + seeded_compute.validate_request( + request=request, + config=vr_config(use_list=True) + ) + ) + assert result['error'] is not None + assert result['error']['error_type'] == "request" + + +def test_base_validate_request_unknown_resource_type_get(seeded_compute): + request = _base_request(resource_type="UNKNOWN") + result = asyncio.run( + seeded_compute.validate_request( + request=request, + config=vr_config(use_list=False) + ) + ) + assert result['error'] is not None + assert result['error']['error_type'] == "request" + + +def test_base_validate_request_unknown_resource_type_list(seeded_compute): + request = _base_request(resource_type="UNKNOWN") + result = asyncio.run( + seeded_compute.validate_request( + request=request, + config=vr_config(use_list=True) + ) + ) + assert result['error'] is not None + assert result['error']['error_type'] == "request" + + +def test_base_validate_request_unknown_identity_type_get(seeded_compute): + request = _base_request() + request['identities'] = { + "unknown_id": [ + { + "username": "a", + "department": "b" + } + ] + } + result = asyncio.run( + seeded_compute.validate_request( + request=request, + config=vr_config(use_list=False) + ) + ) + assert result['error'] is not None + assert result['error']['error_type'] == "request" + + +def test_base_validate_request_unknown_identity_type_list(seeded_compute): + request = _base_request() + request['identities'] = { + "unknown_id": [ + { + "username": "a", + "department": "b" + } + ] + } + result = asyncio.run( + seeded_compute.validate_request( + request=request, + config=vr_config(use_list=True) + ) + ) + assert result['error'] is not None + assert result['error']['error_type'] == "request" + + +def test_base_validate_request_invalid_context_data(seeded_compute): + request = _base_request() + request['context'] = { + "extra_field": "not allowed" + } + result = asyncio.run( + seeded_compute.validate_request( + request=request, + config=vr_config() + ) + ) + assert result['error'] is not None + assert result['error']['error_type'] == "request" + + +def test_base_validate_request_invalid_resource_data(seeded_compute): + request = _base_request() + request['resource'] = { + "color": 123, + "is_inflated": "not_bool" + } + result = asyncio.run( + seeded_compute.validate_request( + request=request, + config=vr_config() + ) + ) + assert result['error'] is not None + assert result['error']['error_type'] == "request" + + +def test_base_validate_request_invalid_action(seeded_compute): + request = _base_request(action="balloon:NONEXISTENT") + result = asyncio.run( + seeded_compute.validate_request( + request=request, + config=vr_config() + ) + ) + assert result['error'] is not None + assert result['error']['error_type'] == "request" + + +def test_base_validate_request_invalid_identity_data(seeded_compute): + request = _base_request() + request['identities'] = { + "user": [ + { + "username": 123, + "department": 456 + } + ] + } + result = asyncio.run( + seeded_compute.validate_request( + request=request, + config=vr_config() + ) + ) + assert result['error'] is not None + assert result['error']['error_type'] == "request" + + +def test_base_validate_request_storage_error_get_context(seeded_compute): + _install_error_method(seeded_compute, "get_context_def") + result = asyncio.run( + seeded_compute.validate_request( + request=_base_request(), + config=vr_config(use_list=False) + ) + ) + assert result['error'] is not None + assert result['error']['error_type'] == "storage" + + +def test_base_validate_request_storage_error_list_context(seeded_compute): + _install_error_method(seeded_compute, "list_context_defs") + result = asyncio.run( + seeded_compute.validate_request( + request=_base_request(), + config=vr_config(use_list=True) + ) + ) + assert result['error'] is not None + assert result['error']['error_type'] == "storage" + + +def test_base_validate_request_storage_error_get_identity(seeded_compute): + _install_error_method(seeded_compute, "get_identity_def") + result = asyncio.run( + seeded_compute.validate_request( + request=_base_request(), + config=vr_config(use_list=False) + ) + ) + assert result['error'] is not None + assert result['error']['error_type'] == "storage" + + +def test_base_validate_request_storage_error_list_identity(seeded_compute): + _install_error_method(seeded_compute, "list_identity_defs") + result = asyncio.run( + seeded_compute.validate_request( + request=_base_request(), + config=vr_config(use_list=True) + ) + ) + assert result['error'] is not None + assert result['error']['error_type'] == "storage" + + +def test_base_validate_request_storage_error_get_resource(seeded_compute): + _install_error_method(seeded_compute, "get_resource_def") + result = asyncio.run( + seeded_compute.validate_request( + request=_base_request(), + config=vr_config(use_list=False) + ) + ) + assert result['error'] is not None + assert result['error']['error_type'] == "storage" + + +def test_base_validate_request_storage_error_list_resource(seeded_compute): + _install_error_method(seeded_compute, "list_resource_defs") + result = asyncio.run( + seeded_compute.validate_request( + request=_base_request(), + config=vr_config(use_list=True) + ) + ) + assert result['error'] is not None + assert result['error']['error_type'] == "storage" + + +def test_base_validate_batch_request_valid_get_path(seeded_compute): + batch_request = _base_batch_request( + batch=[ + { + "resource": { + "color": "red", + "is_inflated": True + } + } + ] + ) + result = asyncio.run( + seeded_compute.validate_batch_request( + batch_request=batch_request, + config=vr_config(use_list=False) + ) + ) + assert result['error'] is None + assert result['batch'][0]['error'] is None + + +def test_base_validate_batch_request_valid_list_path(seeded_compute): + batch_request = _base_batch_request( + batch=[ + { + "resource": { + "color": "red", + "is_inflated": True + } + } + ] + ) + result = asyncio.run( + seeded_compute.validate_batch_request( + batch_request=batch_request, + config=vr_config(use_list=True) + ) + ) + assert result['error'] is None + assert result['batch'][0]['error'] is None + + +def test_base_validate_batch_request_valid_list_path_paginated(seeded_compute): + batch_request = _base_batch_request( + batch=[ + { + "resource": { + "color": "red", + "is_inflated": True + } + } + ] + ) + result = asyncio.run( + seeded_compute.validate_batch_request( + batch_request=batch_request, + config=vr_config(use_list=True, page_size=1) + ) + ) + assert result['error'] is None + assert result['batch'][0]['error'] is None + + +def test_base_validate_batch_request_invalid_schema(seeded_compute): + result = asyncio.run( + seeded_compute.validate_batch_request( + batch_request={ + "bad": "data" + }, + config=vr_config() + ) + ) + assert result['error'] is not None + assert result['batch'] == [] + + +def test_base_validate_batch_request_item_context_override(seeded_compute): + batch_request = _base_batch_request( + batch=[ + { + "context_type": "NONE", + "context": {} + } + ] + ) + result = asyncio.run( + seeded_compute.validate_batch_request( + batch_request=batch_request, + config=vr_config() + ) + ) + assert result['error'] is None + assert result['batch'][0]['error'] is None + + +def test_base_validate_batch_request_item_identities_override(seeded_compute): + batch_request = _base_batch_request( + batch=[ + { + "identities": { + "user": [ + { + "username": "other", + "department": "Other Dept" + } + ] + } + } + ] + ) + result = asyncio.run( + seeded_compute.validate_batch_request( + batch_request=batch_request, + config=vr_config() + ) + ) + assert result['error'] is None + assert result['batch'][0]['error'] is None + + +def test_base_validate_batch_request_item_invalid_data(seeded_compute): + batch_request = _base_batch_request( + batch=[ + { + "resource": { + "color": 123, + "is_inflated": "bad" + } + } + ] + ) + result = asyncio.run( + seeded_compute.validate_batch_request( + batch_request=batch_request, + config=vr_config() + ) + ) + assert result['error'] is None + assert result['batch'][0]['error'] is not None + + +def test_base_validate_batch_request_root_unknown_context_get(seeded_compute): + batch_request = _base_batch_request( + batch=[ + { + "resource": { + "color": "red", + "is_inflated": True + } + } + ] + ) + batch_request['context_type'] = "NONEXISTENT" + result = asyncio.run( + seeded_compute.validate_batch_request( + batch_request=batch_request, + config=vr_config(use_list=False) + ) + ) + assert result['error'] is not None + assert result['batch'] == [] + + +def test_base_validate_batch_request_root_unknown_identity_get(seeded_compute): + batch_request = _base_batch_request( + batch=[ + { + "resource": { + "color": "red", + "is_inflated": True + } + } + ] + ) + batch_request['identities'] = { + "unknown_id": [ + { + "username": "a", + "department": "b" + } + ] + } + result = asyncio.run( + seeded_compute.validate_batch_request( + batch_request=batch_request, + config=vr_config(use_list=False) + ) + ) + assert result['error'] is not None + assert result['batch'] == [] + + +def test_base_validate_batch_request_root_unknown_resource_get(seeded_compute): + batch_request = _base_batch_request( + batch=[ + { + "resource": { + "color": "red", + "is_inflated": True + } + } + ] + ) + batch_request['resource_type'] = "NONEXISTENT" + result = asyncio.run( + seeded_compute.validate_batch_request( + batch_request=batch_request, + config=vr_config(use_list=False) + ) + ) + assert result['error'] is not None + assert result['batch'] == [] + + +def test_base_validate_batch_request_storage_error_get_context(seeded_compute): + _install_error_method(seeded_compute, "get_context_def") + batch_request = _base_batch_request( + batch=[ + { + "resource": { + "color": "red", + "is_inflated": True + } + } + ] + ) + result = asyncio.run( + seeded_compute.validate_batch_request( + batch_request=batch_request, + config=vr_config(use_list=False) + ) + ) + assert result['error'] is not None + assert result['error']['error_type'] == "storage" + assert result['batch'] == [] + + +def test_base_validate_batch_request_storage_error_list_context(seeded_compute): + _install_error_method(seeded_compute, "list_context_defs") + batch_request = _base_batch_request( + batch=[ + { + "resource": { + "color": "red", + "is_inflated": True + } + } + ] + ) + result = asyncio.run( + seeded_compute.validate_batch_request( + batch_request=batch_request, + config=vr_config(use_list=True) + ) + ) + assert result['error'] is not None + assert result['error']['error_type'] == "storage" + assert result['batch'] == [] + + +def test_base_validate_batch_request_storage_error_get_identity(seeded_compute): + _install_error_method(seeded_compute, "get_identity_def") + batch_request = _base_batch_request( + batch=[ + { + "resource": { + "color": "red", + "is_inflated": True + } + } + ] + ) + result = asyncio.run( + seeded_compute.validate_batch_request( + batch_request=batch_request, + config=vr_config(use_list=False) + ) + ) + assert result['error'] is not None + assert result['error']['error_type'] == "storage" + assert result['batch'] == [] + + +def test_base_validate_batch_request_storage_error_list_identity( + seeded_compute +): + _install_error_method(seeded_compute, "list_identity_defs") + batch_request = _base_batch_request( + batch=[ + { + "resource": { + "color": "red", + "is_inflated": True + } + } + ] + ) + result = asyncio.run( + seeded_compute.validate_batch_request( + batch_request=batch_request, + config=vr_config(use_list=True) + ) + ) + assert result['error'] is not None + assert result['error']['error_type'] == "storage" + assert result['batch'] == [] + + +def test_base_validate_batch_request_storage_error_get_resource(seeded_compute): + _install_error_method(seeded_compute, "get_resource_def") + batch_request = _base_batch_request( + batch=[ + { + "resource": { + "color": "red", + "is_inflated": True + } + } + ] + ) + result = asyncio.run( + seeded_compute.validate_batch_request( + batch_request=batch_request, + config=vr_config(use_list=False) + ) + ) + assert result['error'] is not None + assert result['error']['error_type'] == "storage" + assert result['batch'] == [] + + +def test_base_validate_batch_request_storage_error_list_resource( + seeded_compute +): + _install_error_method(seeded_compute, "list_resource_defs") + batch_request = _base_batch_request( + batch=[ + { + "resource": { + "color": "red", + "is_inflated": True + } + } + ] + ) + result = asyncio.run( + seeded_compute.validate_batch_request( + batch_request=batch_request, + config=vr_config(use_list=True) + ) + ) + assert result['error'] is not None + assert result['error']['error_type'] == "storage" + assert result['batch'] == [] + + +def test_base_audit_valid(seeded_compute): + result = asyncio.run( + seeded_compute.audit( + request=_base_request(), + page_ref=None, + config=op_config() + ) + ) + assert result['error'] is None + assert len(result['results']) > 0 + assert result['results'][0]['grant'] is not None + assert "is_applicable" in result['results'][0] + assert "query_result" in result['results'][0] + assert "failure" in result['results'][0] + + +def test_base_audit_pagination(seeded_compute): + first = asyncio.run( + seeded_compute.audit( + request=_base_request(action="balloon:read"), + page_ref=None, + config=op_config(grants_page_size=1) + ) + ) + assert first['error'] is None + assert len(first['results']) == 1 + if first['next_page_ref'] is not None: + second = asyncio.run( + seeded_compute.audit( + request=_base_request(action="balloon:read"), + page_ref=first['next_page_ref'], + config=op_config(grants_page_size=1) + ) + ) + assert second['error'] is None + + +def test_base_audit_storage_error(failing_compute): + result = asyncio.run( + failing_compute.audit( + request=_base_request(), + page_ref=None, + config=op_config() + ) + ) + assert result['error'] is not None + + +def test_base_authorize_allow(seeded_compute): + result = asyncio.run( + seeded_compute.authorize( + request=_base_request(), + config=op_config() + ) + ) + assert result['is_authorized'] is True + assert result['error'] is None + assert result['grant'] is not None + + +def test_base_authorize_deny(seeded_compute): + request = _base_request( + action="balloon:pop", + department="Intern", + is_inflated=True + ) + result = asyncio.run( + seeded_compute.authorize( + request=request, + config=op_config() + ) + ) + assert result['is_authorized'] is False + assert result['error'] is None + assert "deny grant" in result['message'] + + +def test_base_authorize_implicit_deny(seeded_compute): + request = _base_request(department="None") + result = asyncio.run( + seeded_compute.authorize( + request=request, + config=op_config() + ) + ) + assert result['is_authorized'] is False + assert result['error'] is None + assert "implicitly denied" in result['message'] + + +def test_base_authorize_deny_phase_storage_error(failing_compute): + result = asyncio.run( + failing_compute.authorize( + request=_base_request(), + config=op_config() + ) + ) + assert result['is_authorized'] is False + assert result['error'] is not None + + +def test_base_authorize_allow_phase_storage_error(fail_on_allow_compute): + result = asyncio.run( + fail_on_allow_compute.authorize( + request=_base_request(), + config=op_config() + ) + ) + assert result['is_authorized'] is False + assert result['error'] is not None + + +def test_base_batch_audit_valid(seeded_compute): + batch_request = _base_batch_request( + batch=[ + { + "resource": { + "color": "red", + "is_inflated": True + } + }, + { + "resource": { + "color": "green", + "is_inflated": False + } + } + ] + ) + result = asyncio.run( + seeded_compute.batch_audit( + batch_request=batch_request, + page_ref=None, + config=op_config() + ) + ) + assert result['error'] is None + assert len(result['batch']) == 2 + + +def test_base_batch_audit_pagination(seeded_compute): + batch_request = _base_batch_request( + batch=[ + { + "resource": { + "color": "red", + "is_inflated": True + } + } + ], + action="balloon:read" + ) + first = asyncio.run( + seeded_compute.batch_audit( + batch_request=batch_request, + page_ref=None, + config=op_config(grants_page_size=1) + ) + ) + assert first['error'] is None + if first['next_page_ref'] is not None: + second = asyncio.run( + seeded_compute.batch_audit( + batch_request=batch_request, + page_ref=first['next_page_ref'], + config=op_config(grants_page_size=1) + ) + ) + assert second['error'] is None + + +def test_base_batch_audit_storage_error(failing_compute): + batch_request = _base_batch_request( + batch=[ + { + "resource": { + "color": "red", + "is_inflated": True + } + } + ] + ) + result = asyncio.run( + failing_compute.batch_audit( + batch_request=batch_request, + page_ref=None, + config=op_config() + ) + ) + assert result['error'] is not None + + +def test_base_batch_authorize_allow(seeded_compute): + batch_request = _base_batch_request( + batch=[ + { + "resource": { + "color": "red", + "is_inflated": True + } + }, + { + "resource": { + "color": "green", + "is_inflated": False + } + } + ] + ) + result = asyncio.run( + seeded_compute.batch_authorize( + batch_request=batch_request, + config=op_config() + ) + ) + assert result['error'] is None + assert len(result['batch']) == 2 + for br in result['batch']: + assert br['is_authorized'] is True + + +def test_base_batch_authorize_deny(seeded_compute): + batch_request = _base_batch_request( + batch=[ + { + "resource": { + "color": "red", + "is_inflated": True + } + } + ], + department="Intern", + action="balloon:pop" + ) + result = asyncio.run( + seeded_compute.batch_authorize( + batch_request=batch_request, + config=op_config() + ) + ) + assert result['error'] is None + for br in result['batch']: + assert br['is_authorized'] is False + assert "deny grant" in br['message'] + + +def test_base_batch_authorize_implicit_deny(seeded_compute): + batch_request = _base_batch_request( + batch=[ + { + "resource": { + "color": "red", + "is_inflated": True + } + } + ], + department="None" + ) + result = asyncio.run( + seeded_compute.batch_authorize( + batch_request=batch_request, + config=op_config() + ) + ) + assert result['error'] is None + for br in result['batch']: + assert br['is_authorized'] is False + assert "implicitly denied" in br['message'] + + +def test_base_batch_authorize_deny_phase_storage_error(failing_compute): + batch_request = _base_batch_request( + batch=[ + { + "resource": { + "color": "red", + "is_inflated": True + } + } + ] + ) + result = asyncio.run( + failing_compute.batch_authorize( + batch_request=batch_request, + config=op_config() + ) + ) + assert result['error'] is not None + + +def test_base_batch_authorize_allow_phase_storage_error(fail_on_allow_compute): + batch_request = _base_batch_request( + batch=[ + { + "resource": { + "color": "red", + "is_inflated": True + } + } + ] + ) + result = asyncio.run( + fail_on_allow_compute.batch_authorize( + batch_request=batch_request, + config=op_config() + ) + ) + assert result['error'] is not None + + +def _seed_extra_defs(storage_dict): + """Insert extra context/identity/resource defs so the target defs land on + a later page when paginating with ``page_size=1``. + + The target types (``LATE_CTX``, ``late_user``, ``late_balloon``) are + inserted last so a ``page_size=1`` list must loop past earlier pages, + exercising the create-next-page-task branches. + + Arguments + --------- + storage_dict : dict + The backing storage dict shared with the compute's storage module. + """ + storage_dict['context_defs_lut']['EXTRA_CTX'] = { + "context_type": "EXTRA_CTX", + "schema": { + "type": "object", + "additionalProperties": False + } + } + storage_dict['context_defs_lut']['LATE_CTX'] = { + "context_type": "LATE_CTX", + "schema": { + "type": "object", + "additionalProperties": False + } + } + storage_dict['identity_defs_lut']['extra_user'] = { + "identity_type": "extra_user", + "schema": { + "type": "object", + "additionalProperties": True + } + } + storage_dict['identity_defs_lut']['late_user'] = { + "identity_type": "late_user", + "schema": { + "type": "object", + "required": [ + "username" + ], + "additionalProperties": False, + "properties": { + "username": { + "type": "string" + } + } + } + } + storage_dict['resource_defs_lut']['extra_balloon'] = { + "resource_type": "extra_balloon", + "actions": [ + "extra:read" + ], + "schema": { + "type": "object", + "additionalProperties": True + } + } + storage_dict['resource_defs_lut']['late_balloon'] = { + "resource_type": "late_balloon", + "actions": [ + "late:read" + ], + "schema": { + "type": "object", + "additionalProperties": False, + "properties": { + "path": { + "type": "string" + } + } + } + } + + +def _late_request(): + return { + "identities": { + "late_user": [ + { + "username": "someone" + } + ] + }, + "action": "late:read", + "resource_type": "late_balloon", + "resource": { + "path": "/tmp" + }, + "context_type": "LATE_CTX", + "context": {} + } + + +def test_base_validate_request_list_pagination_multi_page( + seeded_compute, + storage_dict +): + _seed_extra_defs(storage_dict) + result = asyncio.run( + seeded_compute.validate_request( + request=_late_request(), + config=vr_config(use_list=True, page_size=1) + ) + ) + assert result['error'] is None + + +def test_base_validate_batch_request_list_pagination_multi_page( + seeded_compute, + storage_dict +): + _seed_extra_defs(storage_dict) + batch_request = _late_request() + batch_request['batch'] = [ + { + "resource": { + "path": "/other" + } + } + ] + result = asyncio.run( + seeded_compute.validate_batch_request( + batch_request=batch_request, + config=vr_config(use_list=True, page_size=1) + ) + ) + assert result['error'] is None + assert result['batch'][0]['error'] is None + + +def test_base_validate_batch_request_item_context_type_only(seeded_compute): + batch_request = _base_batch_request( + batch=[ + { + "context_type": "NONE" + } + ] + ) + result = asyncio.run( + seeded_compute.validate_batch_request( + batch_request=batch_request, + config=vr_config() + ) + ) + assert result['error'] is None + assert result['batch'][0]['error'] is None + + +def test_base_validate_batch_request_item_context_only(seeded_compute): + batch_request = _base_batch_request( + batch=[ + { + "context": {} + } + ] + ) + result = asyncio.run( + seeded_compute.validate_batch_request( + batch_request=batch_request, + config=vr_config() + ) + ) + assert result['error'] is None + assert result['batch'][0]['error'] is None + + +def test_base_validate_batch_request_item_resource_type_only(seeded_compute): + batch_request = _base_batch_request( + batch=[ + { + "resource_type": "balloon" + } + ] + ) + result = asyncio.run( + seeded_compute.validate_batch_request( + batch_request=batch_request, + config=vr_config() + ) + ) + assert result['error'] is None + assert result['batch'][0]['error'] is None + + +def test_base_batch_authorize_multi_deny_skip_complete( + seeded_compute, + storage_dict +): + """Two matching deny grants so an already-complete item is skipped by the + second deny grant, then a matching allow grant is also skipped.""" + from uuid import uuid4 as _uuid4 + storage_dict['grants_lut'][str(_uuid4())] = { + "grant_uuid": str(_uuid4()), + "name": "Deny pop 2", + "description": "", + "tags": {}, + "effect": "deny", + "actions": [ + "balloon:pop" + ], + "query": "length(request.identities.user[?department == 'Intern']) > `0`", + "equality": True, + "applicable_on_failure": False, + "data": {} + } + storage_dict['grants_lut'][str(_uuid4())] = { + "grant_uuid": str(_uuid4()), + "name": "Allow pop", + "description": "", + "tags": {}, + "effect": "allow", + "actions": [ + "balloon:pop" + ], + "query": "`true`", + "equality": True, + "applicable_on_failure": False, + "data": {} + } + batch_request = _base_batch_request( + batch=[ + { + "resource": { + "color": "red", + "is_inflated": True + } + } + ], + department="Intern", + action="balloon:pop" + ) + result = asyncio.run( + seeded_compute.batch_authorize( + batch_request=batch_request, + config=op_config(grants_page_size=1) + ) + ) + assert result['error'] is None + assert result['batch'][0]['is_authorized'] is False + assert "deny grant" in result['batch'][0]['message'] + + +def test_base_batch_authorize_multi_allow_skip_complete( + seeded_compute, + storage_dict +): + """Two matching allow grants so an already-complete (allowed) item is + skipped by the second allow grant.""" + from uuid import uuid4 as _uuid4 + storage_dict['grants_lut'][str(_uuid4())] = { + "grant_uuid": str(_uuid4()), + "name": "Allow inflate 2", + "description": "", + "tags": {}, + "effect": "allow", + "actions": [ + "balloon:inflate" + ], + "query": "`true`", + "equality": True, + "applicable_on_failure": False, + "data": {} + } + batch_request = _base_batch_request( + batch=[ + { + "resource": { + "color": "red", + "is_inflated": True + } + } + ] + ) + result = asyncio.run( + seeded_compute.batch_authorize( + batch_request=batch_request, + config=op_config(grants_page_size=1) + ) + ) + assert result['error'] is None + assert result['batch'][0]['is_authorized'] is True + + +def test_base_validate_request_list_multi_identity_types( + seeded_compute, + storage_dict +): + """validate_request list path with two identity types so the inner + 'still-missing' loop after finding one identity def runs.""" + _seed_extra_defs(storage_dict) + request = _base_request() + request['identities'] = { + "user": [ + { + "username": "balloon_person", + "department": "Balloon Dept" + } + ], + "late_user": [ + { + "username": "someone" + } + ] + } + result = asyncio.run( + seeded_compute.validate_request( + request=request, + config=vr_config(use_list=True, page_size=1) + ) + ) + assert result['error'] is None + + +def test_base_validate_batch_request_item_adds_unregistered_identity( + seeded_compute +): + """A batch item introduces a new identity type absent from the root, so it + is added to the identity lookup. Because the type is unregistered, the base + request validation returns a graceful 'not registered' request error rather + than crashing.""" + batch_request = _base_batch_request( + batch=[ + { + "identities": { + "ghost_id": [ + { + "anything": True + } + ] + } + } + ] + ) + result = asyncio.run( + seeded_compute.validate_batch_request( + batch_request=batch_request, + config=vr_config(use_list=False) + ) + ) + assert result['error'] is not None + assert result['error']['error_type'] == "request" + assert result['batch'] == [] + + +def _multi_type_batch_request(): + """Batch request whose root has two identity types and whose item adds a + new (registered) context type and resource type. + + The root carries both identity types so the base-request validation has + them available, while the item introduces additional context/resource + types to exercise the lookup-collection and multi-type list-path loops. + """ + batch_request = _base_batch_request( + batch=[ + { + "context_type": "EXTRA_CTX", + "context": {}, + "resource_type": "extra_balloon", + "resource": { + "anything": True + } + } + ] + ) + batch_request['identities'] = { + "user": [ + { + "username": "balloon_person", + "department": "Balloon Dept" + } + ], + "late_user": [ + { + "username": "someone" + } + ] + } + + return batch_request + + +def test_base_validate_batch_request_item_added_types_list( + seeded_compute, + storage_dict +): + """Batch root has two identity types and an item references extra + registered context/resource types not present at the root, exercising + lookup collection and the multi-type list-path 'still-missing' loops.""" + _seed_extra_defs(storage_dict) + result = asyncio.run( + seeded_compute.validate_batch_request( + batch_request=_multi_type_batch_request(), + config=vr_config(use_list=True, page_size=1) + ) + ) + assert result['error'] is None + assert len(result['batch']) == 1 + + +def test_base_validate_batch_request_item_added_types_get( + seeded_compute, + storage_dict +): + """Same multi-type batch request but via the get_* def lookup path.""" + _seed_extra_defs(storage_dict) + result = asyncio.run( + seeded_compute.validate_batch_request( + batch_request=_multi_type_batch_request(), + config=vr_config(use_list=False) + ) + ) + assert result['error'] is None + assert len(result['batch']) == 1 + + +def test_base_validate_batch_request_base_from_cache_invalid(seeded_compute): + """Batch base request passes schema and def lookup but fails validation + from cache (registered resource type with invalid resource data) so the + early-return with empty batch fires.""" + batch_request = _base_batch_request( + batch=[ + { + "resource": { + "color": "red", + "is_inflated": True + } + } + ] + ) + batch_request['resource'] = { + "color": 123, + "is_inflated": "bad" + } + result = asyncio.run( + seeded_compute.validate_batch_request( + batch_request=batch_request, + config=vr_config() + ) + ) + assert result['error'] is not None + assert result['error']['error_type'] == "request" + assert result['batch'] == [] diff --git a/tests/unit/test_in_process_compute.py b/tests/unit/test_in_process_compute.py index 44da952..2bdc366 100644 --- a/tests/unit/test_in_process_compute.py +++ b/tests/unit/test_in_process_compute.py @@ -1,18 +1,171 @@ -"""Unit tests for authzee.compute modules (ComputeModule and InProcessCompute).""" +"""Unit tests for authzee.compute InProcessCompute. + +Reuses the shared compute module test suite. Fixtures required by the shared +suite are defined here and bound to InProcessCompute. Base-class +NotImplementedError tests for ``ComputeModule`` stay here since they test the +abstract base rather than a concrete implementation. +""" import asyncio +import os +import sys from uuid import uuid4 import pytest + +sys.path.insert(0, os.path.dirname(__file__)) + +from compute_module_test_base import * + from authzee.compute.compute_module import ComputeModule from authzee.compute.in_process_compute import InProcessCompute -from authzee.exceptions import NotImplementedError as AuthzeeNotImplementedError from authzee.jmespath import jmespath_execute -from authzee.module_locality import ModuleLocality from authzee.storage.dict_storage import DictStorage +class FailingStorage(DictStorage): + """A storage class that always returns an error for list_grants.""" + + + async def list_grants( + self, + effect, + action, + page_ref, + config + ): + return { + "grants": [], + "next_page_ref": None, + "error": { + "error_type": "storage", + "message": "forced failure" + } + } + + +class FailOnAllowStorage(DictStorage): + """A storage class that fails only when listing allow grants.""" + + + async def list_grants( + self, + effect, + action, + page_ref, + config + ): + if effect == "allow": + return { + "grants": [], + "next_page_ref": None, + "error": { + "error_type": "storage", + "message": "forced failure" + } + } + + return await super().list_grants(effect, action, page_ref, config) + + +async def _seed_storage(storage): + await storage.put_context_def( + { + "context_type": "NONE", + "schema": { + "type": "object", + "additionalProperties": False + } + }, + config={} + ) + await storage.put_identity_def( + { + "identity_type": "user", + "schema": { + "type": "object", + "required": [ + "username", + "department" + ], + "additionalProperties": False, + "properties": { + "username": { + "type": "string" + }, + "department": { + "type": "string" + } + } + } + }, + config={} + ) + await storage.put_resource_def( + { + "resource_type": "balloon", + "actions": [ + "balloon:read", + "balloon:inflate", + "balloon:pop" + ], + "schema": { + "type": "object", + "required": [ + "color", + "is_inflated" + ], + "additionalProperties": False, + "properties": { + "color": { + "type": "string" + }, + "is_inflated": { + "type": "boolean" + } + } + } + }, + config={} + ) + await storage.enact( + grant={ + "grant_uuid": str(uuid4()), + "name": "Allow inflate", + "description": "", + "tags": {}, + "effect": "allow", + "actions": [ + "balloon:read", + "balloon:inflate" + ], + "query": "length(request.identities.user[?department == 'Balloon Dept']) > `0`", + "equality": True, + "applicable_on_failure": False, + "data": {} + }, + config={} + ) + await storage.enact( + grant={ + "grant_uuid": str(uuid4()), + "name": "Deny pop for interns", + "description": "", + "tags": {}, + "effect": "deny", + "actions": [ + "balloon:pop" + ], + "query": "length(request.identities.user[?department == 'Intern']) > `0`", + "equality": True, + "applicable_on_failure": False, + "data": {} + }, + config={} + ) + + @pytest.fixture def storage_dict(): d = {} @@ -53,106 +206,47 @@ def seeded_compute(compute, storage_dict): async def seed(): storage = DictStorage(storage_dict=storage_dict) await storage.start(config={}) - await storage.put_context_def( - { - "context_type": "NONE", - "schema": { - "type": "object", - "additionalProperties": False - } - }, - config={} - ) - await storage.put_identity_def( - { - "identity_type": "user", - "schema": { - "type": "object", - "required": [ - "username", - "department" - ], - "additionalProperties": False, - "properties": { - "username": { - "type": "string" - }, - "department": { - "type": "string" - } - } - } - }, - config={} - ) - await storage.put_resource_def( - { - "resource_type": "balloon", - "actions": [ - "balloon:read", - "balloon:inflate", - "balloon:pop" - ], - "schema": { - "type": "object", - "required": [ - "color", - "is_inflated" - ], - "additionalProperties": False, - "properties": { - "color": { - "type": "string" - }, - "is_inflated": { - "type": "boolean" - } - } - } - }, - config={} - ) - await storage.enact( - grant={ - "grant_uuid": str(uuid4()), - "name": "Allow inflate", - "description": "", - "tags": {}, - "effect": "allow", - "actions": [ - "balloon:read", - "balloon:inflate" - ], - "query": "length(request.identities.user[?department == 'Balloon Dept']) > `0`", - "equality": True, - "applicable_on_failure": False, - "data": {} - }, - config={} - ) - await storage.enact( - grant={ - "grant_uuid": str(uuid4()), - "name": "Deny pop for interns", - "description": "", - "tags": {}, - "effect": "deny", - "actions": [ - "balloon:pop" - ], - "query": "length(request.identities.user[?department == 'Intern']) > `0`", - "equality": True, - "applicable_on_failure": False, - "data": {} - }, - config={} - ) + await _seed_storage(storage) asyncio.run(seed()) return compute +@pytest.fixture +def failing_compute(compute, storage_dict): + """InProcessCompute whose storage always fails list_grants.""" + + async def setup(): + storage = DictStorage(storage_dict=storage_dict) + await storage.start(config={}) + await _seed_storage(storage) + failing = FailingStorage(storage_dict=storage_dict) + await failing.start(config={}) + compute._storage = failing + + asyncio.run(setup()) + + return compute + + +@pytest.fixture +def fail_on_allow_compute(compute, storage_dict): + """InProcessCompute whose storage fails list_grants only for effect allow.""" + + async def setup(): + storage = DictStorage(storage_dict=storage_dict) + await storage.start(config={}) + await _seed_storage(storage) + failing = FailOnAllowStorage(storage_dict=storage_dict) + await failing.start(config={}) + compute._storage = failing + + asyncio.run(setup()) + + return compute + + def test_compute_module_shutdown_raises(): cm = ComputeModule() with pytest.raises(TypeError): @@ -249,2119 +343,3 @@ def test_compute_module_batch_authorize_raises(): cm = ComputeModule() with pytest.raises(TypeError): asyncio.run(cm.batch_authorize(batch_request={}, config={})) - - -def test_in_process_compute_start(storage_dict): - c = InProcessCompute() - - async def run(): - storage = DictStorage(storage_dict=storage_dict) - await storage.construct(config={}) - result = await c.start( - execute=jmespath_execute, - storage_type=DictStorage, - storage_kwargs={ - "storage_dict": storage_dict - }, - config={ - "storage": {} - } - ) - - return result - - result = asyncio.run(run()) - assert result['error'] is None - assert c.locality == ModuleLocality.PROCESS - assert c.has_parallel_paging is False - - -def test_in_process_compute_shutdown(compute): - result = asyncio.run( - compute.shutdown( - config={ - "storage": {} - } - ) - ) - assert result['error'] is None - - -def test_in_process_compute_construct(storage_dict): - c = InProcessCompute() - result = asyncio.run(c.construct(config={})) - assert result['error'] is None - - -def test_in_process_compute_destroy(storage_dict): - c = InProcessCompute() - result = asyncio.run(c.destroy(config={})) - assert result['error'] is None - - -def test_in_process_validate_context_def_valid(compute): - result = asyncio.run( - compute.validate_context_def( - context_def={ - "context_type": "NONE", - "schema": { - "type": "object", - "additionalProperties": False - } - }, - config={} - ) - ) - assert result['error'] is None - - -def test_in_process_validate_context_def_invalid(compute): - result = asyncio.run( - compute.validate_context_def( - context_def={ - "bad": "data" - }, - config={} - ) - ) - assert result['error'] is not None - - -def test_in_process_validate_identity_def_valid(compute): - result = asyncio.run( - compute.validate_identity_def( - identity_def={ - "identity_type": "user", - "schema": { - "type": "object" - } - }, - config={} - ) - ) - assert result['error'] is None - - -def test_in_process_validate_identity_def_invalid(compute): - result = asyncio.run( - compute.validate_identity_def( - identity_def={ - "bad": "data" - }, - config={} - ) - ) - assert result['error'] is not None - - -def test_in_process_validate_resource_def_valid(compute): - result = asyncio.run( - compute.validate_resource_def( - resource_def={ - "resource_type": "file", - "actions": [ - "read" - ], - "schema": { - "type": "object" - } - }, - config={} - ) - ) - assert result['error'] is None - - -def test_in_process_validate_resource_def_invalid(compute): - result = asyncio.run( - compute.validate_resource_def( - resource_def={ - "bad": "data" - }, - config={} - ) - ) - assert result['error'] is not None - - -def test_in_process_validate_grant_valid(compute): - result = asyncio.run( - compute.validate_grant( - grant={ - "grant_uuid": str(uuid4()), - "name": "Test", - "description": "", - "tags": {}, - "effect": "allow", - "actions": [ - "read" - ], - "query": "`true`", - "equality": True, - "applicable_on_failure": False, - "data": {} - }, - config={} - ) - ) - assert result['error'] is None - - -def test_in_process_validate_grant_invalid(compute): - result = asyncio.run( - compute.validate_grant( - grant={ - "bad": "data" - }, - config={} - ) - ) - assert result['error'] is not None - - -def test_in_process_validate_request_valid(seeded_compute): - request = { - "identities": { - "user": [ - { - "username": "balloon_person", - "department": "Balloon Dept" - } - ] - }, - "action": "balloon:inflate", - "resource_type": "balloon", - "resource": { - "color": "blue", - "is_inflated": False - }, - "context_type": "NONE", - "context": {} - } - config = { - "get_context_def": {}, - "use_list_context_defs": False, - "list_context_defs": { - "page_size": 1000, - "use_cache": True - }, - "get_identity_def": {}, - "use_list_identity_defs": True, - "list_identity_defs": { - "page_size": 1000, - "use_cache": True - }, - "get_resource_def": {}, - "use_list_resource_defs": False, - "list_resource_defs": { - "page_size": 1000, - "use_cache": True - } - } - result = asyncio.run( - seeded_compute.validate_request( - request=request, - config=config - ) - ) - assert result['error'] is None - - -def test_in_process_validate_request_invalid_schema(seeded_compute): - result = asyncio.run( - seeded_compute.validate_request( - request={ - "bad": "data" - }, - config={} - ) - ) - assert result['error'] is not None - - -def test_in_process_validate_request_unknown_context_type(seeded_compute): - request = { - "identities": { - "user": [ - { - "username": "a", - "department": "b" - } - ] - }, - "action": "balloon:inflate", - "resource_type": "balloon", - "resource": { - "color": "blue", - "is_inflated": False - }, - "context_type": "UNKNOWN", - "context": {} - } - config = { - "get_context_def": {}, - "use_list_context_defs": False, - "list_context_defs": { - "page_size": 1000, - "use_cache": True - }, - "get_identity_def": {}, - "use_list_identity_defs": True, - "list_identity_defs": { - "page_size": 1000, - "use_cache": True - }, - "get_resource_def": {}, - "use_list_resource_defs": False, - "list_resource_defs": { - "page_size": 1000, - "use_cache": True - } - } - result = asyncio.run( - seeded_compute.validate_request( - request=request, - config=config - ) - ) - assert result['error'] is not None - assert result['error'] is not None - - -def test_in_process_validate_request_invalid_context_data(seeded_compute): - request = { - "identities": { - "user": [ - { - "username": "a", - "department": "b" - } - ] - }, - "action": "balloon:inflate", - "resource_type": "balloon", - "resource": { - "color": "blue", - "is_inflated": False - }, - "context_type": "NONE", - "context": { - "extra_field": "not allowed" - } - } - config = { - "get_context_def": {}, - "use_list_context_defs": False, - "list_context_defs": { - "page_size": 1000, - "use_cache": True - }, - "get_identity_def": {}, - "use_list_identity_defs": True, - "list_identity_defs": { - "page_size": 1000, - "use_cache": True - }, - "get_resource_def": {}, - "use_list_resource_defs": False, - "list_resource_defs": { - "page_size": 1000, - "use_cache": True - } - } - result = asyncio.run( - seeded_compute.validate_request( - request=request, - config=config - ) - ) - assert result['error'] is not None - - -def test_in_process_validate_request_unknown_resource_type(seeded_compute): - request = { - "identities": { - "user": [ - { - "username": "a", - "department": "b" - } - ] - }, - "action": "balloon:inflate", - "resource_type": "UNKNOWN", - "resource": { - "color": "blue", - "is_inflated": False - }, - "context_type": "NONE", - "context": {} - } - config = { - "get_context_def": {}, - "use_list_context_defs": False, - "list_context_defs": { - "page_size": 1000, - "use_cache": True - }, - "get_identity_def": {}, - "use_list_identity_defs": True, - "list_identity_defs": { - "page_size": 1000, - "use_cache": True - }, - "get_resource_def": {}, - "use_list_resource_defs": False, - "list_resource_defs": { - "page_size": 1000, - "use_cache": True - } - } - result = asyncio.run( - seeded_compute.validate_request( - request=request, - config=config - ) - ) - assert result['error'] is not None - - -def test_in_process_validate_request_invalid_resource_data(seeded_compute): - request = { - "identities": { - "user": [ - { - "username": "a", - "department": "b" - } - ] - }, - "action": "balloon:inflate", - "resource_type": "balloon", - "resource": { - "color": 123, - "is_inflated": "not_bool" - }, - "context_type": "NONE", - "context": {} - } - config = { - "get_context_def": {}, - "use_list_context_defs": False, - "list_context_defs": { - "page_size": 1000, - "use_cache": True - }, - "get_identity_def": {}, - "use_list_identity_defs": True, - "list_identity_defs": { - "page_size": 1000, - "use_cache": True - }, - "get_resource_def": {}, - "use_list_resource_defs": False, - "list_resource_defs": { - "page_size": 1000, - "use_cache": True - } - } - result = asyncio.run( - seeded_compute.validate_request( - request=request, - config=config - ) - ) - assert result['error'] is not None - - -def test_in_process_validate_request_invalid_action(seeded_compute): - request = { - "identities": { - "user": [ - { - "username": "a", - "department": "b" - } - ] - }, - "action": "balloon:NONEXISTENT", - "resource_type": "balloon", - "resource": { - "color": "blue", - "is_inflated": False - }, - "context_type": "NONE", - "context": {} - } - config = { - "get_context_def": {}, - "use_list_context_defs": False, - "list_context_defs": { - "page_size": 1000, - "use_cache": True - }, - "get_identity_def": {}, - "use_list_identity_defs": True, - "list_identity_defs": { - "page_size": 1000, - "use_cache": True - }, - "get_resource_def": {}, - "use_list_resource_defs": False, - "list_resource_defs": { - "page_size": 1000, - "use_cache": True - } - } - result = asyncio.run( - seeded_compute.validate_request( - request=request, - config=config - ) - ) - assert result['error'] is not None - - -def test_in_process_validate_request_unknown_identity_type(seeded_compute): - request = { - "identities": { - "unknown_id": [ - { - "username": "a", - "department": "b" - } - ] - }, - "action": "balloon:inflate", - "resource_type": "balloon", - "resource": { - "color": "blue", - "is_inflated": False - }, - "context_type": "NONE", - "context": {} - } - config = { - "get_context_def": {}, - "use_list_context_defs": False, - "list_context_defs": { - "page_size": 1000, - "use_cache": True - }, - "get_identity_def": {}, - "use_list_identity_defs": True, - "list_identity_defs": { - "page_size": 1000, - "use_cache": True - }, - "get_resource_def": {}, - "use_list_resource_defs": False, - "list_resource_defs": { - "page_size": 1000, - "use_cache": True - } - } - result = asyncio.run( - seeded_compute.validate_request( - request=request, - config=config - ) - ) - assert result['error'] is not None - - -def test_in_process_validate_request_invalid_identity_data(seeded_compute): - request = { - "identities": { - "user": [ - { - "username": 123, - "department": 456 - } - ] - }, - "action": "balloon:inflate", - "resource_type": "balloon", - "resource": { - "color": "blue", - "is_inflated": False - }, - "context_type": "NONE", - "context": {} - } - config = { - "get_context_def": {}, - "use_list_context_defs": False, - "list_context_defs": { - "page_size": 1000, - "use_cache": True - }, - "get_identity_def": {}, - "use_list_identity_defs": True, - "list_identity_defs": { - "page_size": 1000, - "use_cache": True - }, - "get_resource_def": {}, - "use_list_resource_defs": False, - "list_resource_defs": { - "page_size": 1000, - "use_cache": True - } - } - result = asyncio.run( - seeded_compute.validate_request( - request=request, - config=config - ) - ) - assert result['error'] is not None - - -def test_in_process_validate_batch_request_valid(seeded_compute): - batch_request = { - "identities": { - "user": [ - { - "username": "balloon_person", - "department": "Balloon Dept" - } - ] - }, - "action": "balloon:inflate", - "resource_type": "balloon", - "resource": { - "color": "blue", - "is_inflated": False - }, - "context_type": "NONE", - "context": {}, - "batch": [ - { - "resource": { - "color": "red", - "is_inflated": True - } - } - ] - } - config = { - "get_context_def": {}, - "use_list_context_defs": False, - "list_context_defs": { - "page_size": 1000, - "use_cache": True - }, - "get_identity_def": {}, - "use_list_identity_defs": True, - "list_identity_defs": { - "page_size": 1000, - "use_cache": True - }, - "get_resource_def": {}, - "use_list_resource_defs": False, - "list_resource_defs": { - "page_size": 1000, - "use_cache": True - } - } - result = asyncio.run( - seeded_compute.validate_batch_request( - batch_request=batch_request, - config=config - ) - ) - assert result['error'] is None - - -def test_in_process_validate_batch_request_invalid_schema(seeded_compute): - result = asyncio.run( - seeded_compute.validate_batch_request( - batch_request={ - "bad": "data" - }, - config={} - ) - ) - assert result['error'] is not None - - -def test_in_process_validate_batch_request_invalid_batch_item(seeded_compute): - batch_request = { - "identities": { - "user": [ - { - "username": "balloon_person", - "department": "Balloon Dept" - } - ] - }, - "action": "balloon:inflate", - "resource_type": "balloon", - "resource": { - "color": "blue", - "is_inflated": False - }, - "context_type": "NONE", - "context": {}, - "batch": [ - { - "resource": { - "color": 123, - "is_inflated": "bad" - } - } - ] - } - config = { - "get_context_def": {}, - "use_list_context_defs": False, - "list_context_defs": { - "page_size": 1000, - "use_cache": True - }, - "get_identity_def": {}, - "use_list_identity_defs": True, - "list_identity_defs": { - "page_size": 1000, - "use_cache": True - }, - "get_resource_def": {}, - "use_list_resource_defs": False, - "list_resource_defs": { - "page_size": 1000, - "use_cache": True - } - } - result = asyncio.run( - seeded_compute.validate_batch_request( - batch_request=batch_request, - config=config - ) - ) - assert result['error'] is None - assert result['batch'][0] is not None - - -def test_in_process_audit(seeded_compute): - request = { - "identities": { - "user": [ - { - "username": "balloon_person", - "department": "Balloon Dept" - } - ] - }, - "action": "balloon:inflate", - "resource_type": "balloon", - "resource": { - "color": "blue", - "is_inflated": False - }, - "context_type": "NONE", - "context": {} - } - config = { - "validate_request": { - "get_context_def": {}, - "use_list_context_defs": False, - "list_context_defs": { - "page_size": 1000, - "use_cache": True - }, - "get_identity_def": {}, - "use_list_identity_defs": True, - "list_identity_defs": { - "page_size": 1000, - "use_cache": True - }, - "get_resource_def": {}, - "use_list_resource_defs": False, - "list_resource_defs": { - "page_size": 1000, - "use_cache": True - } - }, - "list_grants": { - "page_size": 100, - "use_cache": False - } - } - result = asyncio.run( - seeded_compute.audit( - request=request, - page_ref=None, - config=config - ) - ) - assert result['error'] is None - assert len(result['results']) > 0 - assert result['results'][0]['grant'] is not None - - -def test_in_process_authorize_allowed(seeded_compute): - request = { - "identities": { - "user": [ - { - "username": "balloon_person", - "department": "Balloon Dept" - } - ] - }, - "action": "balloon:inflate", - "resource_type": "balloon", - "resource": { - "color": "blue", - "is_inflated": False - }, - "context_type": "NONE", - "context": {} - } - config = { - "validate_request": { - "get_context_def": {}, - "use_list_context_defs": False, - "list_context_defs": { - "page_size": 1000, - "use_cache": True - }, - "get_identity_def": {}, - "use_list_identity_defs": True, - "list_identity_defs": { - "page_size": 1000, - "use_cache": True - }, - "get_resource_def": {}, - "use_list_resource_defs": False, - "list_resource_defs": { - "page_size": 1000, - "use_cache": True - } - }, - "list_grants": { - "page_size": 100, - "use_cache": False - } - } - result = asyncio.run( - seeded_compute.authorize(request=request, config=config) - ) - assert result['is_authorized'] is True - assert result['error'] is None - - -def test_in_process_authorize_denied(seeded_compute): - request = { - "identities": { - "user": [ - { - "username": "intern_person", - "department": "Intern" - } - ] - }, - "action": "balloon:pop", - "resource_type": "balloon", - "resource": { - "color": "blue", - "is_inflated": True - }, - "context_type": "NONE", - "context": {} - } - config = { - "validate_request": { - "get_context_def": {}, - "use_list_context_defs": False, - "list_context_defs": { - "page_size": 1000, - "use_cache": True - }, - "get_identity_def": {}, - "use_list_identity_defs": True, - "list_identity_defs": { - "page_size": 1000, - "use_cache": True - }, - "get_resource_def": {}, - "use_list_resource_defs": False, - "list_resource_defs": { - "page_size": 1000, - "use_cache": True - } - }, - "list_grants": { - "page_size": 100, - "use_cache": False - } - } - result = asyncio.run( - seeded_compute.authorize(request=request, config=config) - ) - assert result['is_authorized'] is False - assert result['error'] is None - - -def test_in_process_authorize_implicit_deny(seeded_compute): - """No matching grants -> implicit deny.""" - request = { - "identities": { - "user": [ - { - "username": "nobody", - "department": "None" - } - ] - }, - "action": "balloon:inflate", - "resource_type": "balloon", - "resource": { - "color": "blue", - "is_inflated": False - }, - "context_type": "NONE", - "context": {} - } - config = { - "validate_request": { - "get_context_def": {}, - "use_list_context_defs": False, - "list_context_defs": { - "page_size": 1000, - "use_cache": True - }, - "get_identity_def": {}, - "use_list_identity_defs": True, - "list_identity_defs": { - "page_size": 1000, - "use_cache": True - }, - "get_resource_def": {}, - "use_list_resource_defs": False, - "list_resource_defs": { - "page_size": 1000, - "use_cache": True - } - }, - "list_grants": { - "page_size": 100, - "use_cache": False - } - } - result = asyncio.run( - seeded_compute.authorize(request=request, config=config) - ) - assert result['is_authorized'] is False - assert result['error'] is None - assert "implicitly denied" in result['message'] - - -def test_in_process_batch_audit(seeded_compute): - batch_request = { - "identities": { - "user": [ - { - "username": "balloon_person", - "department": "Balloon Dept" - } - ] - }, - "action": "balloon:inflate", - "resource_type": "balloon", - "resource": { - "color": "blue", - "is_inflated": False - }, - "context_type": "NONE", - "context": {}, - "batch": [ - { - "resource": { - "color": "red", - "is_inflated": True - } - }, - { - "resource": { - "color": "green", - "is_inflated": False - } - } - ] - } - config = { - "validate_batch_request": { - "get_context_def": {}, - "use_list_context_defs": False, - "list_context_defs": { - "page_size": 1000, - "use_cache": True - }, - "get_identity_def": {}, - "use_list_identity_defs": True, - "list_identity_defs": { - "page_size": 1000, - "use_cache": True - }, - "get_resource_def": {}, - "use_list_resource_defs": False, - "list_resource_defs": { - "page_size": 1000, - "use_cache": True - } - }, - "list_grants": { - "page_size": 100, - "use_cache": False - } - } - result = asyncio.run( - seeded_compute.batch_audit( - batch_request=batch_request, - page_ref=None, - config=config - ) - ) - assert result['error'] is None - assert len(result['batch']) == 2 - - -def test_in_process_batch_authorize_mixed(seeded_compute): - batch_request = { - "identities": { - "user": [ - { - "username": "balloon_person", - "department": "Balloon Dept" - } - ] - }, - "action": "balloon:inflate", - "resource_type": "balloon", - "resource": { - "color": "blue", - "is_inflated": False - }, - "context_type": "NONE", - "context": {}, - "batch": [ - { - "resource": { - "color": "red", - "is_inflated": True - } - }, - { - "resource": { - "color": "green", - "is_inflated": False - } - } - ] - } - config = { - "validate_batch_request": { - "get_context_def": {}, - "use_list_context_defs": False, - "list_context_defs": { - "page_size": 1000, - "use_cache": True - }, - "get_identity_def": {}, - "use_list_identity_defs": True, - "list_identity_defs": { - "page_size": 1000, - "use_cache": True - }, - "get_resource_def": {}, - "use_list_resource_defs": False, - "list_resource_defs": { - "page_size": 1000, - "use_cache": True - } - }, - "list_grants": { - "page_size": 100, - "use_cache": False - } - } - result = asyncio.run( - seeded_compute.batch_authorize( - batch_request=batch_request, - config=config - ) - ) - assert result['error'] is None - assert len(result['batch']) == 2 - for br in result['batch']: - assert br['is_authorized'] is True - - -def test_in_process_batch_authorize_deny(seeded_compute): - """Batch authorize where a deny grant applies.""" - batch_request = { - "identities": { - "user": [ - { - "username": "intern_person", - "department": "Intern" - } - ] - }, - "action": "balloon:pop", - "resource_type": "balloon", - "resource": { - "color": "blue", - "is_inflated": True - }, - "context_type": "NONE", - "context": {}, - "batch": [ - { - "resource": { - "color": "red", - "is_inflated": True - } - } - ] - } - config = { - "validate_batch_request": { - "get_context_def": {}, - "use_list_context_defs": False, - "list_context_defs": { - "page_size": 1000, - "use_cache": True - }, - "get_identity_def": {}, - "use_list_identity_defs": True, - "list_identity_defs": { - "page_size": 1000, - "use_cache": True - }, - "get_resource_def": {}, - "use_list_resource_defs": False, - "list_resource_defs": { - "page_size": 1000, - "use_cache": True - } - }, - "list_grants": { - "page_size": 100, - "use_cache": False - } - } - result = asyncio.run( - seeded_compute.batch_authorize( - batch_request=batch_request, - config=config - ) - ) - assert result['error'] is None - for br in result['batch']: - assert br['is_authorized'] is False - - -def test_in_process_batch_authorize_implicit_deny(seeded_compute): - """Batch authorize where no grants match -> implicit deny.""" - batch_request = { - "identities": { - "user": [ - { - "username": "nobody", - "department": "None" - } - ] - }, - "action": "balloon:inflate", - "resource_type": "balloon", - "resource": { - "color": "blue", - "is_inflated": False - }, - "context_type": "NONE", - "context": {}, - "batch": [ - { - "resource": { - "color": "red", - "is_inflated": True - } - } - ] - } - config = { - "validate_batch_request": { - "get_context_def": {}, - "use_list_context_defs": False, - "list_context_defs": { - "page_size": 1000, - "use_cache": True - }, - "get_identity_def": {}, - "use_list_identity_defs": True, - "list_identity_defs": { - "page_size": 1000, - "use_cache": True - }, - "get_resource_def": {}, - "use_list_resource_defs": False, - "list_resource_defs": { - "page_size": 1000, - "use_cache": True - } - }, - "list_grants": { - "page_size": 100, - "use_cache": False - } - } - result = asyncio.run( - seeded_compute.batch_authorize( - batch_request=batch_request, - config=config - ) - ) - assert result['error'] is None - for br in result['batch']: - assert br['is_authorized'] is False - assert "implicitly denied" in br['message'] - - -class FailingStorage(DictStorage): - """A storage class that always returns an error for list_grants.""" - - - async def list_grants( - self, - effect, - action, - page_ref, - config - ): - return { - "grants": [], - "next_page_ref": None, - "error": { - "error_type": "storage", - "message": "forced failure" - } - } - - -class FailOnAllowStorage(DictStorage): - """A storage class that fails only when listing allow grants.""" - - - async def list_grants( - self, - effect, - action, - page_ref, - config - ): - if effect == "allow": - return { - "grants": [], - "next_page_ref": None, - "error": { - "error_type": "storage", - "message": "forced failure" - } - } - - return await super().list_grants(effect, action, page_ref, config) - - -@pytest.fixture -def failing_compute(storage_dict): - """InProcessCompute with a failing storage module.""" - c = InProcessCompute() - - async def setup(): - storage = DictStorage(storage_dict=storage_dict) - await storage.construct(config={}) - await c.start( - execute=jmespath_execute, - storage_type=DictStorage, - storage_kwargs={ - "storage_dict": storage_dict - }, - config={ - "storage": {} - } - ) - failing = FailingStorage(storage_dict=storage_dict) - await failing.start(config={}) - c._storage = failing - - return c - - asyncio.run(setup()) - - return c - - -@pytest.fixture -def seeded_failing_compute(failing_compute, storage_dict): - """Failing compute with definitions stored.""" - - async def seed(): - storage = DictStorage(storage_dict=storage_dict) - await storage.start(config={}) - await storage.put_context_def( - { - "context_type": "NONE", - "schema": { - "type": "object", - "additionalProperties": False - } - }, - config={} - ) - await storage.put_identity_def( - { - "identity_type": "user", - "schema": { - "type": "object", - "required": [ - "username" - ], - "additionalProperties": False, - "properties": { - "username": { - "type": "string" - } - } - } - }, - config={} - ) - await storage.put_resource_def( - { - "resource_type": "file", - "actions": [ - "read" - ], - "schema": { - "type": "object", - "required": [ - "path" - ], - "additionalProperties": False, - "properties": { - "path": { - "type": "string" - } - } - } - }, - config={} - ) - - asyncio.run(seed()) - - return failing_compute - - -def test_in_process_audit_storage_failure(seeded_failing_compute): - """Audit when storage.list_grants fails.""" - request = { - "identities": { - "user": [ - { - "username": "test" - } - ] - }, - "action": "read", - "resource_type": "file", - "resource": { - "path": "/tmp" - }, - "context_type": "NONE", - "context": {} - } - config = { - "validate_request": { - "get_context_def": {}, - "use_list_context_defs": False, - "list_context_defs": { - "page_size": 1000, - "use_cache": True - }, - "get_identity_def": {}, - "use_list_identity_defs": True, - "list_identity_defs": { - "page_size": 1000, - "use_cache": True - }, - "get_resource_def": {}, - "use_list_resource_defs": False, - "list_resource_defs": { - "page_size": 1000, - "use_cache": True - } - }, - "list_grants": { - "page_size": 100, - "use_cache": False - } - } - result = asyncio.run( - seeded_failing_compute.audit( - request=request, - page_ref=None, - config=config - ) - ) - assert result['error'] is not None - - -def test_in_process_authorize_storage_failure(seeded_failing_compute): - """Authorize when storage.list_grants fails.""" - request = { - "identities": { - "user": [ - { - "username": "test" - } - ] - }, - "action": "read", - "resource_type": "file", - "resource": { - "path": "/tmp" - }, - "context_type": "NONE", - "context": {} - } - config = { - "validate_request": { - "get_context_def": {}, - "use_list_context_defs": False, - "list_context_defs": { - "page_size": 1000, - "use_cache": True - }, - "get_identity_def": {}, - "use_list_identity_defs": True, - "list_identity_defs": { - "page_size": 1000, - "use_cache": True - }, - "get_resource_def": {}, - "use_list_resource_defs": False, - "list_resource_defs": { - "page_size": 1000, - "use_cache": True - } - }, - "list_grants": { - "page_size": 100, - "use_cache": False - } - } - result = asyncio.run( - seeded_failing_compute.authorize( - request=request, - config=config - ) - ) - assert result['error'] is not None - - -def test_in_process_batch_audit_storage_failure(seeded_failing_compute): - """Batch audit when storage.list_grants fails.""" - batch_request = { - "identities": { - "user": [ - { - "username": "test" - } - ] - }, - "action": "read", - "resource_type": "file", - "resource": { - "path": "/tmp" - }, - "context_type": "NONE", - "context": {}, - "batch": [ - { - "resource": { - "path": "/other" - } - } - ] - } - config = { - "validate_batch_request": { - "get_context_def": {}, - "use_list_context_defs": False, - "list_context_defs": { - "page_size": 1000, - "use_cache": True - }, - "get_identity_def": {}, - "use_list_identity_defs": True, - "list_identity_defs": { - "page_size": 1000, - "use_cache": True - }, - "get_resource_def": {}, - "use_list_resource_defs": False, - "list_resource_defs": { - "page_size": 1000, - "use_cache": True - } - }, - "list_grants": { - "page_size": 100, - "use_cache": False - } - } - result = asyncio.run( - seeded_failing_compute.batch_audit( - batch_request=batch_request, - page_ref=None, - config=config - ) - ) - assert result['error'] is not None - - -def test_in_process_batch_authorize_storage_failure(seeded_failing_compute): - """Batch authorize when storage.list_grants fails in deny phase.""" - batch_request = { - "identities": { - "user": [ - { - "username": "test" - } - ] - }, - "action": "read", - "resource_type": "file", - "resource": { - "path": "/tmp" - }, - "context_type": "NONE", - "context": {}, - "batch": [ - { - "resource": { - "path": "/other" - } - } - ] - } - config = { - "validate_batch_request": { - "get_context_def": {}, - "use_list_context_defs": False, - "list_context_defs": { - "page_size": 1000, - "use_cache": True - }, - "get_identity_def": {}, - "use_list_identity_defs": True, - "list_identity_defs": { - "page_size": 1000, - "use_cache": True - }, - "get_resource_def": {}, - "use_list_resource_defs": False, - "list_resource_defs": { - "page_size": 1000, - "use_cache": True - } - }, - "list_grants": { - "page_size": 100, - "use_cache": False - } - } - result = asyncio.run( - seeded_failing_compute.batch_authorize( - batch_request=batch_request, - config=config - ) - ) - assert result['error'] is not None - assert result['error'] is not None - - -def test_in_process_batch_authorize_allow_phase_storage_failure(storage_dict): - """Batch authorize storage failure in allow phase.""" - c = InProcessCompute() - - async def setup_and_run(): - storage = DictStorage(storage_dict=storage_dict) - await storage.construct(config={}) - await c.start( - execute=jmespath_execute, - storage_type=DictStorage, - storage_kwargs={ - "storage_dict": storage_dict - }, - config={ - "storage": {} - } - ) - await storage.start(config={}) - await storage.put_context_def( - { - "context_type": "NONE", - "schema": { - "type": "object", - "additionalProperties": False - } - }, - config={} - ) - await storage.put_identity_def( - { - "identity_type": "user", - "schema": { - "type": "object", - "required": [ - "username" - ], - "additionalProperties": False, - "properties": { - "username": { - "type": "string" - } - } - } - }, - config={} - ) - await storage.put_resource_def( - { - "resource_type": "file", - "actions": [ - "read" - ], - "schema": { - "type": "object", - "required": [ - "path" - ], - "additionalProperties": False, - "properties": { - "path": { - "type": "string" - } - } - } - }, - config={} - ) - failing = FailOnAllowStorage(storage_dict=storage_dict) - await failing.start(config={}) - c._storage = failing - - batch_request = { - "identities": { - "user": [ - { - "username": "test" - } - ] - }, - "action": "read", - "resource_type": "file", - "resource": { - "path": "/tmp" - }, - "context_type": "NONE", - "context": {}, - "batch": [ - { - "resource": { - "path": "/other" - } - } - ] - } - config_val = { - "validate_batch_request": { - "get_context_def": {}, - "get_identity_def": {}, - "get_resource_def": {} - }, - "list_grants": { - "page_size": 100, - "use_cache": False - } - } - - return await c.batch_authorize( - batch_request=batch_request, - config=config_val - ) - - result = asyncio.run(setup_and_run()) - assert result['error'] is not None - assert result['error'] is not None - - -def test_in_process_authorize_allow_phase_storage_failure(storage_dict): - """Authorize storage failure in allow grants phase.""" - c = InProcessCompute() - - async def setup_and_run(): - storage = DictStorage(storage_dict=storage_dict) - await storage.construct(config={}) - await c.start( - execute=jmespath_execute, - storage_type=DictStorage, - storage_kwargs={ - "storage_dict": storage_dict - }, - config={ - "storage": {} - } - ) - await storage.start(config={}) - await storage.put_context_def( - { - "context_type": "NONE", - "schema": { - "type": "object", - "additionalProperties": False - } - }, - config={} - ) - await storage.put_identity_def( - { - "identity_type": "user", - "schema": { - "type": "object", - "required": [ - "username" - ], - "additionalProperties": False, - "properties": { - "username": { - "type": "string" - } - } - } - }, - config={} - ) - await storage.put_resource_def( - { - "resource_type": "file", - "actions": [ - "read" - ], - "schema": { - "type": "object", - "required": [ - "path" - ], - "additionalProperties": False, - "properties": { - "path": { - "type": "string" - } - } - } - }, - config={} - ) - failing = FailOnAllowStorage(storage_dict=storage_dict) - await failing.start(config={}) - c._storage = failing - - request = { - "identities": { - "user": [ - { - "username": "test" - } - ] - }, - "action": "read", - "resource_type": "file", - "resource": { - "path": "/tmp" - }, - "context_type": "NONE", - "context": {} - } - config_val = { - "validate_request": { - "get_context_def": {}, - "get_identity_def": {}, - "get_resource_def": {} - }, - "list_grants": { - "page_size": 100, - "use_cache": False - } - } - - return await c.authorize(request=request, config=config_val) - - result = asyncio.run(setup_and_run()) - assert result['error'] is not None - - -def test_in_process_validate_batch_request_base_request_invalid(seeded_compute): - """validate_batch_request where the batch schema passes but base request is invalid.""" - batch_request = { - "identities": { - "user": [ - { - "username": "balloon_person", - "department": "Balloon Dept" - } - ] - }, - "action": "balloon:inflate", - "resource_type": "balloon", - "resource": { - "color": "blue", - "is_inflated": False - }, - "context_type": "NONEXISTENT", - "context": {}, - "batch": [ - { - "resource": { - "color": "red", - "is_inflated": True - } - } - ] - } - config = { - "get_context_def": {}, - "use_list_context_defs": False, - "list_context_defs": { - "page_size": 1000, - "use_cache": True - }, - "get_identity_def": {}, - "use_list_identity_defs": True, - "list_identity_defs": { - "page_size": 1000, - "use_cache": True - }, - "get_resource_def": {}, - "use_list_resource_defs": False, - "list_resource_defs": { - "page_size": 1000, - "use_cache": True - } - } - result = asyncio.run( - seeded_compute.validate_batch_request( - batch_request=batch_request, - config=config - ) - ) - assert result['error'] is not None - - -def test_in_process_batch_authorize_deny_applicable_continue(storage_dict): - """Test batch_authorize where deny grant is applicable.""" - c = InProcessCompute() - - async def setup_and_run(): - storage = DictStorage(storage_dict=storage_dict) - await storage.construct(config={}) - await c.start( - execute=jmespath_execute, - storage_type=DictStorage, - storage_kwargs={ - "storage_dict": storage_dict - }, - config={ - "storage": {} - } - ) - await storage.start(config={}) - await storage.put_context_def( - { - "context_type": "NONE", - "schema": { - "type": "object", - "additionalProperties": False - } - }, - config={} - ) - await storage.put_identity_def( - { - "identity_type": "user", - "schema": { - "type": "object", - "required": [ - "username" - ], - "additionalProperties": False, - "properties": { - "username": { - "type": "string" - } - } - } - }, - config={} - ) - await storage.put_resource_def( - { - "resource_type": "file", - "actions": [ - "read" - ], - "schema": { - "type": "object", - "required": [ - "path" - ], - "additionalProperties": False, - "properties": { - "path": { - "type": "string" - } - } - } - }, - config={} - ) - await storage.enact( - grant={ - "grant_uuid": str(uuid4()), - "name": "Deny All", - "description": "", - "tags": {}, - "effect": "deny", - "actions": [ - "read" - ], - "query": "`true`", - "equality": True, - "applicable_on_failure": False, - "data": {} - }, - config={} - ) - await storage.enact( - grant={ - "grant_uuid": str(uuid4()), - "name": "Allow All", - "description": "", - "tags": {}, - "effect": "allow", - "actions": [ - "read" - ], - "query": "`true`", - "equality": True, - "applicable_on_failure": False, - "data": {} - }, - config={} - ) - - batch_request = { - "identities": { - "user": [ - { - "username": "test" - } - ] - }, - "action": "read", - "resource_type": "file", - "resource": { - "path": "/tmp" - }, - "context_type": "NONE", - "context": {}, - "batch": [ - { - "resource": { - "path": "/other" - } - } - ] - } - config_val = { - "validate_batch_request": { - "get_context_def": {}, - "get_identity_def": {}, - "get_resource_def": {} - }, - "list_grants": { - "page_size": 100, - "use_cache": False - } - } - - return await c.batch_authorize( - batch_request=batch_request, - config=config_val - ) - - result = asyncio.run(setup_and_run()) - assert result['batch'][0]['is_authorized'] is False - assert "deny grant" in result['batch'][0]['message'] - - -def test_in_process_batch_authorize_deny_phase_skip_complete(storage_dict): - """Test that once an item is marked complete by a deny grant, - subsequent deny grants skip it.""" - c = InProcessCompute() - - async def setup_and_run(): - storage = DictStorage(storage_dict=storage_dict) - await storage.construct(config={}) - await c.start( - execute=jmespath_execute, - storage_type=DictStorage, - storage_kwargs={ - "storage_dict": storage_dict - }, - config={ - "storage": {} - } - ) - await storage.start(config={}) - await storage.put_context_def( - { - "context_type": "NONE", - "schema": { - "type": "object", - "additionalProperties": False - } - }, - config={} - ) - await storage.put_identity_def( - { - "identity_type": "user", - "schema": { - "type": "object", - "required": [ - "username" - ], - "additionalProperties": False, - "properties": { - "username": { - "type": "string" - } - } - } - }, - config={} - ) - await storage.put_resource_def( - { - "resource_type": "file", - "actions": [ - "read" - ], - "schema": { - "type": "object", - "required": [ - "path" - ], - "additionalProperties": False, - "properties": { - "path": { - "type": "string" - } - } - } - }, - config={} - ) - await storage.enact( - grant={ - "grant_uuid": str(uuid4()), - "name": "Deny All 1", - "description": "", - "tags": {}, - "effect": "deny", - "actions": [ - "read" - ], - "query": "`true`", - "equality": True, - "applicable_on_failure": False, - "data": {} - }, - config={} - ) - await storage.enact( - grant={ - "grant_uuid": str(uuid4()), - "name": "Deny All 2", - "description": "", - "tags": {}, - "effect": "deny", - "actions": [ - "read" - ], - "query": "`true`", - "equality": True, - "applicable_on_failure": False, - "data": {} - }, - config={} - ) - - batch_request = { - "identities": { - "user": [ - { - "username": "test" - } - ] - }, - "action": "read", - "resource_type": "file", - "resource": { - "path": "/tmp" - }, - "context_type": "NONE", - "context": {}, - "batch": [ - { - "resource": { - "path": "/other" - } - } - ] - } - config_val = { - "validate_batch_request": { - "get_context_def": {}, - "get_identity_def": {}, - "get_resource_def": {} - }, - "list_grants": { - "page_size": 100, - "use_cache": False - } - } - - return await c.batch_authorize( - batch_request=batch_request, - config=config_val - ) - - result = asyncio.run(setup_and_run()) - assert result['batch'][0]['is_authorized'] is False From 7118cdbed7e02756c13be09a76ef87d16a161673 Mon Sep 17 00:00:00 2001 From: btemplep Date: Thu, 27 Aug 2026 23:18:19 -0400 Subject: [PATCH 8/9] fixed up examples for default configs --- CHANGELOG.md | 5 +- src/authzee/compute/in_process_compute.py | 39 +- src/authzee/config.py | 4 +- src/authzee/storage/dict_storage.py | 36 ++ src/authzee/types/config.py | 212 +++---- tests/unit/storage_module_test_base.py | 714 ++++++++++++++++++++++ tests/unit/test_dict_storage.py | 691 +-------------------- 7 files changed, 920 insertions(+), 781 deletions(-) create mode 100644 tests/unit/storage_module_test_base.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a88215..58d008c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,7 +22,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Security --> -## [0.1.0a6] - 2026-08-25 +## [0.1.0a6] - TBD Support for Authzee spec 0.5.0. @@ -42,6 +42,9 @@ Support for Authzee spec 0.5.0. - `validate_request` in `InProcessCompute` now respects the full `ValidateRequestConfig` - Uses `use_list_context_defs`, `use_list_identity_defs`, `use_list_resource_defs` config options - `validate_batch_request` in `InProcessCompute` now returns per-item errors in `batch` instead of failing fast +- `InProcessCompute` + - Updated `validate_request` and `validate_batch_request` to be much more efficient, and fully support all config options for `get_*` vs `list_*`. +- Default config - parallel paging set to false by default. ### Removed diff --git a/src/authzee/compute/in_process_compute.py b/src/authzee/compute/in_process_compute.py index dd4b7bf..7a70844 100644 --- a/src/authzee/compute/in_process_compute.py +++ b/src/authzee/compute/in_process_compute.py @@ -7,7 +7,7 @@ "InProcessCompute" ] -from asyncio import as_completed, create_task, gather, Task +from asyncio import create_task, gather, Task import copy from typing import Any, Callable, Type @@ -46,6 +46,43 @@ class InProcessCompute(ComputeModule): + """Compute module that processes authorization requests in the local process. + + All compute is performed within the same process and `asyncio` event loop as the + caller. It uses the given execute function to evaluate grant queries and a + [](authzee.storage.storage_module.StorageModule) instance to retrieve definitions + and grants. Request and batch-request validation caching is self contained per + request. + + This module takes no constructor arguments. It is not meant to be instantiated or + started directly. Instead, pass the class to the [](authzee.authzee.Authzee) (or + [](authzee.authzee_async.AuthzeeAsync)) app as `compute_type`, and the app manages + its lifecycle. + + Parameters + ---------- + None + + Examples + -------- + + ```python + from authzee import Authzee, DictStorage, InProcessCompute, jmespath_execute + + storage_dict = {} + authz = Authzee( + execute=jmespath_execute, + compute_type=InProcessCompute, + compute_kwargs={}, + storage_type=DictStorage, + storage_kwargs={ + "storage_dict": storage_dict + } + ) + authz.construct() + authz.start() + ``` + """ async def start( diff --git a/src/authzee/config.py b/src/authzee/config.py index 5f61085..cbb93ac 100644 --- a/src/authzee/config.py +++ b/src/authzee/config.py @@ -225,7 +225,7 @@ "page_size": 1000, "use_cache": True }, - "parallel_paging": True, + "parallel_paging": False, "list_grant_refs": { "page_size": 10, "use_cache": True @@ -262,7 +262,7 @@ "page_size": 1000, "use_cache": True }, - "parallel_paging": True, + "parallel_paging": False, "list_grant_refs": { "page_size": 10, "use_cache": True diff --git a/src/authzee/storage/dict_storage.py b/src/authzee/storage/dict_storage.py index 52ea93c..1fa5c05 100644 --- a/src/authzee/storage/dict_storage.py +++ b/src/authzee/storage/dict_storage.py @@ -44,6 +44,42 @@ class DictStorage(StorageModule): + """Storage module that keeps all Authzee data in a Python dict in main memory. + + Context, identity, and resource definitions, grants, and storage latches are all + stored within the given `storage_dict`. Because the data lives in a plain dict, it + is only shared by objects that reference the same dict and does not persist beyond + the lifetime of that dict. + + This storage module supports parallel pagination. + There is not really any penalty for using parallel pagination because it is just a python dict. + + Parameters + ---------- + storage_dict : dict + The dict used to hold all storage data. The same dict must be passed to every + `DictStorage` instance that should share state. + + Examples + -------- + + ```python + from authzee import Authzee, DictStorage, InProcessCompute, jmespath_execute + + storage_dict = {} + authz = Authzee( + execute=jmespath_execute, + compute_type=InProcessCompute, + compute_kwargs={}, + storage_type=DictStorage, + storage_kwargs={ + "storage_dict": storage_dict + } + ) + authz.construct() + authz.start() + ``` + """ def __init__(self, storage_dict: dict): diff --git a/src/authzee/types/config.py b/src/authzee/types/config.py index e02eef8..7f5c9b0 100644 --- a/src/authzee/types/config.py +++ b/src/authzee/types/config.py @@ -60,6 +60,7 @@ class AuthzeeBaseConfig(TypedDict): Examples -------- + Example showing the default values: ```python { "raise_errors": True @@ -82,6 +83,7 @@ class StorageStartConfig(TypedDict): Examples -------- + Example (the default is an empty dict): ```python {} ``` @@ -97,6 +99,7 @@ class ComputeStartConfig(TypedDict): Examples -------- + Example showing the default values: ```python { "storage": {} @@ -119,6 +122,7 @@ class StartConfig(TypedDict): Examples -------- + Example showing the default values: ```python { "compute_start": { @@ -147,6 +151,7 @@ class StorageShutdownConfig(TypedDict): Examples -------- + Example (the default is an empty dict): ```python {} ``` @@ -162,6 +167,7 @@ class ComputeShutdownConfig(TypedDict): Examples -------- + Example showing the default values: ```python { "storage": {} @@ -184,6 +190,7 @@ class ShutdownConfig(TypedDict): Examples -------- + Example showing the default values: ```python { "compute_shutdown": { @@ -212,6 +219,7 @@ class ComputeConstructConfig(TypedDict): Examples -------- + Example (the default is an empty dict): ```python {} ``` @@ -227,6 +235,7 @@ class StorageConstructConfig(TypedDict): Examples -------- + Example (the default is an empty dict): ```python {} ``` @@ -242,6 +251,7 @@ class ConstructConfig(TypedDict): Examples -------- + Example showing the default values: ```python { "compute_construct": {}, @@ -268,6 +278,7 @@ class ComputeDestroyConfig(TypedDict): Examples -------- + Example (the default is an empty dict): ```python {} ``` @@ -283,6 +294,7 @@ class StorageDestroyConfig(TypedDict): Examples -------- + Example (the default is an empty dict): ```python {} ``` @@ -298,6 +310,7 @@ class DestroyConfig(TypedDict): Examples -------- + Example showing the default values: ```python { "compute_destroy": {}, @@ -324,6 +337,7 @@ class ListContextDefsConfig(TypedDict): Examples -------- + Example showing the default values: ```python { "page_size": 100, @@ -350,6 +364,7 @@ class ListIdentityDefsConfig(TypedDict): Examples -------- + Example showing the default values: ```python { "page_size": 100, @@ -376,6 +391,7 @@ class ListResourceDefsConfig(TypedDict): Examples -------- + Example showing the default values: ```python { "page_size": 100, @@ -402,6 +418,7 @@ class ListGrantsConfig(TypedDict): Examples -------- + Example showing the default values: ```python { "page_size": 100, @@ -428,6 +445,7 @@ class ValidateContextDefConfig(TypedDict): Examples -------- + Example (the default is an empty dict): ```python {} ``` @@ -443,6 +461,7 @@ class GetContextDefConfig(TypedDict): Examples -------- + Example showing the default values: ```python { "use_cache": False @@ -465,6 +484,7 @@ class PutContextDefConfig(TypedDict): Examples -------- + Example (the default is an empty dict): ```python {} ``` @@ -480,6 +500,7 @@ class DeleteContextDefConfig(TypedDict): Examples -------- + Example (the default is an empty dict): ```python {} ``` @@ -495,6 +516,7 @@ class ValidateIdentityDefConfig(TypedDict): Examples -------- + Example (the default is an empty dict): ```python {} ``` @@ -510,6 +532,7 @@ class GetIdentityDefConfig(TypedDict): Examples -------- + Example showing the default values: ```python { "use_cache": False @@ -532,6 +555,7 @@ class PutIdentityDefConfig(TypedDict): Examples -------- + Example (the default is an empty dict): ```python {} ``` @@ -547,6 +571,7 @@ class DeleteIdentityDefConfig(TypedDict): Examples -------- + Example (the default is an empty dict): ```python {} ``` @@ -562,6 +587,7 @@ class ValidateResourceDefConfig(TypedDict): Examples -------- + Example (the default is an empty dict): ```python {} ``` @@ -577,6 +603,7 @@ class GetResourceDefConfig(TypedDict): Examples -------- + Example showing the default values: ```python { "use_cache": False @@ -599,6 +626,7 @@ class PutResourceDefConfig(TypedDict): Examples -------- + Example (the default is an empty dict): ```python {} ``` @@ -614,6 +642,7 @@ class DeleteResourceDefConfig(TypedDict): Examples -------- + Example (the default is an empty dict): ```python {} ``` @@ -629,6 +658,7 @@ class ValidateGrantConfig(TypedDict): Examples -------- + Example (the default is an empty dict): ```python {} ``` @@ -644,6 +674,7 @@ class GetGrantConfig(TypedDict): Examples -------- + Example showing the default values: ```python { "use_cache": False @@ -666,6 +697,7 @@ class EnactConfig(TypedDict): Examples -------- + Example (the default is an empty dict): ```python {} ``` @@ -681,6 +713,7 @@ class RepealConfig(TypedDict): Examples -------- + Example (the default is an empty dict): ```python {} ``` @@ -696,6 +729,7 @@ class CreateLatchConfig(TypedDict): Examples -------- + Example (the default is an empty dict): ```python {} ``` @@ -711,6 +745,7 @@ class GetLatchConfig(TypedDict): Examples -------- + Example (the default is an empty dict): ```python {} ``` @@ -726,6 +761,7 @@ class SetLatchConfig(TypedDict): Examples -------- + Example (the default is an empty dict): ```python {} ``` @@ -741,6 +777,7 @@ class DeleteLatchConfig(TypedDict): Examples -------- + Example (the default is an empty dict): ```python {} ``` @@ -756,6 +793,7 @@ class CleanupLatchesConfig(TypedDict): Examples -------- + Example (the default is an empty dict): ```python {} ``` @@ -771,6 +809,7 @@ class ListGrantRefsConfig(TypedDict): Examples -------- + Example showing the default values: ```python { "page_size": 10, @@ -797,14 +836,15 @@ class ValidateRequestConfig(TypedDict): Examples -------- + Example showing the default values: ```python { "get_context_def": { "use_cache": True }, - "use_list_context_defs": True, + "use_list_context_defs": False, "list_context_defs": { - "page_size": 100, + "page_size": 1000, "use_cache": True }, "get_identity_def": { @@ -812,15 +852,15 @@ class ValidateRequestConfig(TypedDict): }, "use_list_identity_defs": True, "list_identity_defs": { - "page_size": 100, + "page_size": 1000, "use_cache": True }, "get_resource_def": { "use_cache": True }, - "use_list_resource_defs": True, + "use_list_resource_defs": False, "list_resource_defs": { - "page_size": 100, + "page_size": 1000, "use_cache": True } } @@ -866,6 +906,7 @@ class ValidateBatchRequestConfig(TypedDict): Examples -------- + Example showing the default values: ```python { "get_context_def": { @@ -873,7 +914,7 @@ class ValidateBatchRequestConfig(TypedDict): }, "use_list_context_defs": True, "list_context_defs": { - "page_size": 100, + "page_size": 1000, "use_cache": True }, "get_identity_def": { @@ -881,7 +922,7 @@ class ValidateBatchRequestConfig(TypedDict): }, "use_list_identity_defs": True, "list_identity_defs": { - "page_size": 100, + "page_size": 1000, "use_cache": True }, "get_resource_def": { @@ -889,7 +930,7 @@ class ValidateBatchRequestConfig(TypedDict): }, "use_list_resource_defs": True, "list_resource_defs": { - "page_size": 100, + "page_size": 1000, "use_cache": True } } @@ -935,15 +976,16 @@ class AuditConfig(TypedDict): Examples -------- + Example showing the default values: ```python { "validate_request": { "get_context_def": { "use_cache": True }, - "use_list_context_defs": True, + "use_list_context_defs": False, "list_context_defs": { - "page_size": 100, + "page_size": 1000, "use_cache": True }, "get_identity_def": { @@ -951,15 +993,15 @@ class AuditConfig(TypedDict): }, "use_list_identity_defs": True, "list_identity_defs": { - "page_size": 100, + "page_size": 1000, "use_cache": True }, "get_resource_def": { "use_cache": True }, - "use_list_resource_defs": True, + "use_list_resource_defs": False, "list_resource_defs": { - "page_size": 100, + "page_size": 1000, "use_cache": True } }, @@ -989,6 +1031,7 @@ class BatchAuditConfig(TypedDict): Examples -------- + Example showing the default values: ```python { "validate_batch_request": { @@ -997,7 +1040,7 @@ class BatchAuditConfig(TypedDict): }, "use_list_context_defs": True, "list_context_defs": { - "page_size": 100, + "page_size": 1000, "use_cache": True }, "get_identity_def": { @@ -1005,7 +1048,7 @@ class BatchAuditConfig(TypedDict): }, "use_list_identity_defs": True, "list_identity_defs": { - "page_size": 100, + "page_size": 1000, "use_cache": True }, "get_resource_def": { @@ -1013,7 +1056,7 @@ class BatchAuditConfig(TypedDict): }, "use_list_resource_defs": True, "list_resource_defs": { - "page_size": 100, + "page_size": 1000, "use_cache": True } }, @@ -1043,15 +1086,16 @@ class AuthorizeConfig(TypedDict): Examples -------- + Example showing the default values: ```python { "validate_request": { "get_context_def": { "use_cache": True }, - "use_list_context_defs": True, + "use_list_context_defs": False, "list_context_defs": { - "page_size": 100, + "page_size": 1000, "use_cache": True }, "get_identity_def": { @@ -1059,23 +1103,23 @@ class AuthorizeConfig(TypedDict): }, "use_list_identity_defs": True, "list_identity_defs": { - "page_size": 100, + "page_size": 1000, "use_cache": True }, "get_resource_def": { "use_cache": True }, - "use_list_resource_defs": True, + "use_list_resource_defs": False, "list_resource_defs": { - "page_size": 100, + "page_size": 1000, "use_cache": True } }, "list_grants": { - "page_size": 100, + "page_size": 1000, "use_cache": True }, - "parallel_paging": True, + "parallel_paging": False, "list_grant_refs": { "page_size": 10, "use_cache": True @@ -1108,6 +1152,7 @@ class BatchAuthorizeConfig(TypedDict): Examples -------- + Example showing the default values: ```python { "validate_batch_request": { @@ -1116,33 +1161,7 @@ class BatchAuthorizeConfig(TypedDict): }, "use_list_context_defs": True, "list_context_defs": { - "page_size": 100, - "use_cache": True - }, - "get_identity_def": { - "use_cache": True - }, - "use_list_identity_defs": True, - "list_identity_defs": { - "page_size": 100, - "use_cache": True - }, - "get_resource_def": { - "use_cache": True - }, - "use_list_resource_defs": True, - "list_resource_defs": { - "page_size": 100, - "use_cache": True - } - }, - "validate_request": { - "get_context_def": { - "use_cache": True - }, - "use_list_context_defs": True, - "list_context_defs": { - "page_size": 100, + "page_size": 1000, "use_cache": True }, "get_identity_def": { @@ -1150,7 +1169,7 @@ class BatchAuthorizeConfig(TypedDict): }, "use_list_identity_defs": True, "list_identity_defs": { - "page_size": 100, + "page_size": 1000, "use_cache": True }, "get_resource_def": { @@ -1158,15 +1177,15 @@ class BatchAuthorizeConfig(TypedDict): }, "use_list_resource_defs": True, "list_resource_defs": { - "page_size": 100, + "page_size": 1000, "use_cache": True } }, "list_grants": { - "page_size": 100, + "page_size": 1000, "use_cache": True }, - "parallel_paging": True, + "parallel_paging": False, "list_grant_refs": { "page_size": 10, "use_cache": True @@ -1178,8 +1197,6 @@ class BatchAuthorizeConfig(TypedDict): ---------- validate_batch_request : ValidateBatchRequestConfig Config for validating batch requests during authorization. - validate_request : ValidateRequestConfig - Config for validating requests during authorization. list_grants : ListGrantsConfig Config for listing grants during authorization. parallel_paging : bool @@ -1188,7 +1205,6 @@ class BatchAuthorizeConfig(TypedDict): Config for listing grant references during authorization. """ validate_batch_request: ValidateBatchRequestConfig - validate_request: ValidateRequestConfig list_grants: ListGrantsConfig parallel_paging: bool list_grant_refs: ListGrantRefsConfig @@ -1289,9 +1305,9 @@ class AuthzeeConfig(TypedDict): "get_context_def": { "use_cache": True }, - "use_list_context_defs": True, + "use_list_context_defs": False, "list_context_defs": { - "page_size": 100, + "page_size": 1000, "use_cache": True }, "get_identity_def": { @@ -1299,15 +1315,15 @@ class AuthzeeConfig(TypedDict): }, "use_list_identity_defs": True, "list_identity_defs": { - "page_size": 100, + "page_size": 1000, "use_cache": True }, "get_resource_def": { "use_cache": True }, - "use_list_resource_defs": True, + "use_list_resource_defs": False, "list_resource_defs": { - "page_size": 100, + "page_size": 1000, "use_cache": True } }, @@ -1317,7 +1333,7 @@ class AuthzeeConfig(TypedDict): }, "use_list_context_defs": True, "list_context_defs": { - "page_size": 100, + "page_size": 1000, "use_cache": True }, "get_identity_def": { @@ -1325,7 +1341,7 @@ class AuthzeeConfig(TypedDict): }, "use_list_identity_defs": True, "list_identity_defs": { - "page_size": 100, + "page_size": 1000, "use_cache": True }, "get_resource_def": { @@ -1333,7 +1349,7 @@ class AuthzeeConfig(TypedDict): }, "use_list_resource_defs": True, "list_resource_defs": { - "page_size": 100, + "page_size": 1000, "use_cache": True } }, @@ -1342,9 +1358,9 @@ class AuthzeeConfig(TypedDict): "get_context_def": { "use_cache": True }, - "use_list_context_defs": True, + "use_list_context_defs": False, "list_context_defs": { - "page_size": 100, + "page_size": 1000, "use_cache": True }, "get_identity_def": { @@ -1352,15 +1368,15 @@ class AuthzeeConfig(TypedDict): }, "use_list_identity_defs": True, "list_identity_defs": { - "page_size": 100, + "page_size": 1000, "use_cache": True }, "get_resource_def": { "use_cache": True }, - "use_list_resource_defs": True, + "use_list_resource_defs": False, "list_resource_defs": { - "page_size": 100, + "page_size": 1000, "use_cache": True } }, @@ -1376,7 +1392,7 @@ class AuthzeeConfig(TypedDict): }, "use_list_context_defs": True, "list_context_defs": { - "page_size": 100, + "page_size": 1000, "use_cache": True }, "get_identity_def": { @@ -1384,7 +1400,7 @@ class AuthzeeConfig(TypedDict): }, "use_list_identity_defs": True, "list_identity_defs": { - "page_size": 100, + "page_size": 1000, "use_cache": True }, "get_resource_def": { @@ -1392,7 +1408,7 @@ class AuthzeeConfig(TypedDict): }, "use_list_resource_defs": True, "list_resource_defs": { - "page_size": 100, + "page_size": 1000, "use_cache": True } }, @@ -1406,9 +1422,9 @@ class AuthzeeConfig(TypedDict): "get_context_def": { "use_cache": True }, - "use_list_context_defs": True, + "use_list_context_defs": False, "list_context_defs": { - "page_size": 100, + "page_size": 1000, "use_cache": True }, "get_identity_def": { @@ -1416,23 +1432,23 @@ class AuthzeeConfig(TypedDict): }, "use_list_identity_defs": True, "list_identity_defs": { - "page_size": 100, + "page_size": 1000, "use_cache": True }, "get_resource_def": { "use_cache": True }, - "use_list_resource_defs": True, + "use_list_resource_defs": False, "list_resource_defs": { - "page_size": 100, + "page_size": 1000, "use_cache": True } }, "list_grants": { - "page_size": 100, + "page_size": 1000, "use_cache": True }, - "parallel_paging": True, + "parallel_paging": False, "list_grant_refs": { "page_size": 10, "use_cache": True @@ -1445,33 +1461,7 @@ class AuthzeeConfig(TypedDict): }, "use_list_context_defs": True, "list_context_defs": { - "page_size": 100, - "use_cache": True - }, - "get_identity_def": { - "use_cache": True - }, - "use_list_identity_defs": True, - "list_identity_defs": { - "page_size": 100, - "use_cache": True - }, - "get_resource_def": { - "use_cache": True - }, - "use_list_resource_defs": True, - "list_resource_defs": { - "page_size": 100, - "use_cache": True - } - }, - "validate_request": { - "get_context_def": { - "use_cache": True - }, - "use_list_context_defs": True, - "list_context_defs": { - "page_size": 100, + "page_size": 1000, "use_cache": True }, "get_identity_def": { @@ -1479,7 +1469,7 @@ class AuthzeeConfig(TypedDict): }, "use_list_identity_defs": True, "list_identity_defs": { - "page_size": 100, + "page_size": 1000, "use_cache": True }, "get_resource_def": { @@ -1487,15 +1477,15 @@ class AuthzeeConfig(TypedDict): }, "use_list_resource_defs": True, "list_resource_defs": { - "page_size": 100, + "page_size": 1000, "use_cache": True } }, "list_grants": { - "page_size": 100, + "page_size": 1000, "use_cache": True }, - "parallel_paging": True, + "parallel_paging": False, "list_grant_refs": { "page_size": 10, "use_cache": True diff --git a/tests/unit/storage_module_test_base.py b/tests/unit/storage_module_test_base.py new file mode 100644 index 0000000..b76aab5 --- /dev/null +++ b/tests/unit/storage_module_test_base.py @@ -0,0 +1,714 @@ +"""Reusable base test suite for Authzee storage modules. + +Any concrete storage module test file can reuse this suite by importing all of +its test functions via ``from storage_module_test_base import *`` and supplying +the required pytest fixtures: + +- ``storage_dict`` +- ``storage`` + +The shared test functions reference these fixtures by name so pytest resolves +them against whatever the concrete test module defines. Only strictly generic +``StorageModule`` behavior is asserted here; implementation-specific internals +(e.g. backing-dict layout, ``has_parallel_paging`` values) belong in the +concrete test module. +""" + +import asyncio +import datetime +from uuid import uuid4 + +from authzee.module_locality import ModuleLocality + + +def _grant( + effect="allow", + actions=None, + name="Test" +): + """Build a valid grant dict. + + Arguments + --------- + effect : str, default="allow" + The grant effect, ``"allow"`` or ``"deny"``. + actions : list | None, default=["read"] + The actions the grant matches. By default ``["read"]``. + name : str, default="Test" + The grant name. + + Returns + ------- + dict + A valid grant object with a fresh ``grant_uuid``. + """ + if actions is None: + actions = ["read"] + + return { + "grant_uuid": str(uuid4()), + "name": name, + "description": "", + "tags": {}, + "effect": effect, + "actions": actions, + "query": "`true`", + "equality": True, + "applicable_on_failure": False, + "data": {} + } + + +def test_base_start(storage_dict): + s = _new_storage(storage_dict) + asyncio.run(s.construct(config={})) + result = asyncio.run(s.start(config={})) + assert result['error'] is None + assert s.locality == ModuleLocality.PROCESS + + +def test_base_shutdown(storage): + result = asyncio.run(storage.shutdown(config={})) + assert result['error'] is None + + +def test_base_construct(storage_dict): + s = _new_storage(storage_dict) + result = asyncio.run(s.construct(config={})) + assert result['error'] is None + asyncio.run(s.start(config={})) + context_def = { + "context_type": "NONE", + "schema": { + "type": "object" + } + } + asyncio.run(s.put_context_def(context_def, config={})) + get_result = asyncio.run(s.get_context_def("NONE", config={})) + assert get_result['error'] is None + assert get_result['context_def'] == context_def + + +def test_base_destroy(storage): + result = asyncio.run(storage.destroy(config={})) + assert result['error'] is None + + +def test_base_put_and_get_context_def(storage): + context_def = { + "context_type": "NONE", + "schema": { + "type": "object" + } + } + asyncio.run(storage.put_context_def(context_def, config={})) + result = asyncio.run(storage.get_context_def("NONE", config={})) + assert result['error'] is None + assert result['context_def'] == context_def + + +def test_base_get_context_def_not_found(storage): + result = asyncio.run(storage.get_context_def("MISSING", config={})) + assert result['error'] is not None + assert result['error']['error_type'] == "resource_not_found" + assert result['context_def'] is None + + +def test_base_list_context_defs(storage): + asyncio.run( + storage.put_context_def( + { + "context_type": "A", + "schema": { + "type": "object" + } + }, + config={} + ) + ) + asyncio.run( + storage.put_context_def( + { + "context_type": "B", + "schema": { + "type": "object" + } + }, + config={} + ) + ) + result = asyncio.run( + storage.list_context_defs( + page_ref=None, + config={ + "page_size": 10 + } + ) + ) + assert result['error'] is None + assert len(result['context_defs']) == 2 + assert result['next_page_ref'] is None + + +def test_base_list_context_defs_pagination(storage): + for i in range(5): + asyncio.run( + storage.put_context_def( + { + "context_type": f"T{i}", + "schema": { + "type": "object" + } + }, + config={} + ) + ) + + result = asyncio.run( + storage.list_context_defs( + page_ref=None, + config={ + "page_size": 2 + } + ) + ) + assert len(result['context_defs']) == 2 + assert result['next_page_ref'] is not None + result2 = asyncio.run( + storage.list_context_defs( + page_ref=result['next_page_ref'], + config={ + "page_size": 2 + } + ) + ) + assert len(result2['context_defs']) == 2 + + +def test_base_delete_context_def(storage): + asyncio.run( + storage.put_context_def( + { + "context_type": "DEL", + "schema": { + "type": "object" + } + }, + config={} + ) + ) + result = asyncio.run(storage.delete_context_def("DEL", config={})) + assert result['error'] is None + get_result = asyncio.run(storage.get_context_def("DEL", config={})) + assert get_result['context_def'] is None + + +def test_base_put_and_get_identity_def(storage): + identity_def = { + "identity_type": "user", + "schema": { + "type": "object" + } + } + asyncio.run( + storage.put_identity_def(identity_def, config={}) + ) + result = asyncio.run(storage.get_identity_def("user", config={})) + assert result['error'] is None + assert result['identity_def'] == identity_def + + +def test_base_get_identity_def_not_found(storage): + result = asyncio.run(storage.get_identity_def("MISSING", config={})) + assert result['error'] is not None + assert result['error']['error_type'] == "resource_not_found" + assert result['identity_def'] is None + + +def test_base_list_identity_defs(storage): + asyncio.run( + storage.put_identity_def( + { + "identity_type": "A", + "schema": { + "type": "object" + } + }, + config={} + ) + ) + result = asyncio.run( + storage.list_identity_defs( + page_ref=None, + config={ + "page_size": 10 + } + ) + ) + assert len(result['identity_defs']) == 1 + + +def test_base_list_identity_defs_pagination(storage): + for i in range(5): + asyncio.run( + storage.put_identity_def( + { + "identity_type": f"T{i}", + "schema": { + "type": "object" + } + }, + config={} + ) + ) + + result = asyncio.run( + storage.list_identity_defs( + page_ref=None, + config={ + "page_size": 2 + } + ) + ) + assert len(result['identity_defs']) == 2 + assert result['next_page_ref'] is not None + result2 = asyncio.run( + storage.list_identity_defs( + page_ref=result['next_page_ref'], + config={ + "page_size": 2 + } + ) + ) + assert len(result2['identity_defs']) == 2 + + +def test_base_delete_identity_def(storage): + asyncio.run( + storage.put_identity_def( + { + "identity_type": "DEL", + "schema": { + "type": "object" + } + }, + config={} + ) + ) + asyncio.run(storage.delete_identity_def("DEL", config={})) + result = asyncio.run(storage.get_identity_def("DEL", config={})) + assert result['identity_def'] is None + + +def test_base_put_and_get_resource_def(storage): + resource_def = { + "resource_type": "file", + "actions": [ + "read" + ], + "schema": { + "type": "object" + } + } + asyncio.run( + storage.put_resource_def(resource_def, config={}) + ) + result = asyncio.run(storage.get_resource_def("file", config={})) + assert result['error'] is None + assert result['resource_def'] == resource_def + + +def test_base_get_resource_def_not_found(storage): + result = asyncio.run(storage.get_resource_def("MISSING", config={})) + assert result['error'] is not None + assert result['error']['error_type'] == "resource_not_found" + assert result['resource_def'] is None + + +def test_base_list_resource_defs(storage): + asyncio.run( + storage.put_resource_def( + { + "resource_type": "A", + "actions": [ + "x" + ], + "schema": { + "type": "object" + } + }, + config={} + ) + ) + result = asyncio.run( + storage.list_resource_defs( + page_ref=None, + config={ + "page_size": 10 + } + ) + ) + assert len(result['resource_defs']) == 1 + + +def test_base_list_resource_defs_pagination(storage): + for i in range(5): + asyncio.run( + storage.put_resource_def( + { + "resource_type": f"T{i}", + "actions": [ + "x" + ], + "schema": { + "type": "object" + } + }, + config={} + ) + ) + + result = asyncio.run( + storage.list_resource_defs( + page_ref=None, + config={ + "page_size": 2 + } + ) + ) + assert len(result['resource_defs']) == 2 + assert result['next_page_ref'] is not None + result2 = asyncio.run( + storage.list_resource_defs( + page_ref=result['next_page_ref'], + config={ + "page_size": 2 + } + ) + ) + assert len(result2['resource_defs']) == 2 + + +def test_base_delete_resource_def(storage): + asyncio.run( + storage.put_resource_def( + { + "resource_type": "DEL", + "actions": [ + "x" + ], + "schema": { + "type": "object" + } + }, + config={} + ) + ) + asyncio.run(storage.delete_resource_def("DEL", config={})) + result = asyncio.run(storage.get_resource_def("DEL", config={})) + assert result['resource_def'] is None + + +def test_base_enact_and_get_grant(storage): + grant = _grant() + asyncio.run(storage.enact(grant, config={})) + result = asyncio.run( + storage.get_grant(grant['grant_uuid'], config={}) + ) + assert result['error'] is None + assert result['grant'] == grant + + +def test_base_get_grant_not_found(storage): + result = asyncio.run( + storage.get_grant("nonexistent-uuid", config={}) + ) + assert result['error'] is not None + assert result['error']['error_type'] == "resource_not_found" + assert result['grant'] is None + + +def test_base_repeal(storage): + grant = _grant() + asyncio.run(storage.enact(grant, config={})) + result = asyncio.run( + storage.repeal( + grant['grant_uuid'], + purge=True, + config={} + ) + ) + assert result['error'] is None + get_result = asyncio.run( + storage.get_grant(grant['grant_uuid'], config={}) + ) + assert get_result['grant'] is None + + +def test_base_list_grants(storage): + grant = _grant() + asyncio.run(storage.enact(grant, config={})) + result = asyncio.run( + storage.list_grants( + effect=None, + action=None, + page_ref=None, + config={ + "page_size": 10 + } + ) + ) + assert len(result['grants']) == 1 + + +def test_base_list_grants_filter_effect(storage): + asyncio.run( + storage.enact(_grant(effect="allow", name="A"), config={}) + ) + asyncio.run( + storage.enact(_grant(effect="deny", name="D"), config={}) + ) + result = asyncio.run( + storage.list_grants( + effect="allow", + action=None, + page_ref=None, + config={ + "page_size": 10 + } + ) + ) + assert len(result['grants']) == 1 + assert result['grants'][0]['effect'] == "allow" + + +def test_base_list_grants_filter_action(storage): + grant1 = _grant(actions=["read", "write"], name="G1") + grant2 = _grant(actions=["delete"], name="G2") + asyncio.run(storage.enact(grant1, config={})) + asyncio.run(storage.enact(grant2, config={})) + result = asyncio.run( + storage.list_grants( + effect=None, + action="write", + page_ref=None, + config={ + "page_size": 10 + } + ) + ) + assert len(result['grants']) == 1 + assert result['grants'][0]['name'] == "G1" + + +def test_base_list_grants_pagination(storage): + for i in range(5): + asyncio.run(storage.enact(_grant(name=f"G{i}"), config={})) + + result = asyncio.run( + storage.list_grants( + effect=None, + action=None, + page_ref=None, + config={ + "page_size": 2 + } + ) + ) + assert len(result['grants']) == 2 + assert result['next_page_ref'] is not None + result2 = asyncio.run( + storage.list_grants( + effect=None, + action=None, + page_ref=result['next_page_ref'], + config={ + "page_size": 2 + } + ) + ) + assert len(result2['grants']) == 2 + + +def test_base_list_grant_refs(storage): + for i in range(5): + asyncio.run(storage.enact(_grant(name=f"G{i}"), config={})) + + result = asyncio.run( + storage.list_grant_refs( + effect=None, + action=None, + page_ref=None, + config={ + "page_size": 2 + } + ) + ) + assert result['error'] is None + assert len(result['page_refs']) > 0 + if result['next_page_ref'] is not None: + result2 = asyncio.run( + storage.list_grant_refs( + effect=None, + action=None, + page_ref=str(result['next_page_ref']), + config={ + "page_size": 2 + } + ) + ) + assert result2['error'] is None + + +def test_base_list_grant_refs_filter_effect(storage): + asyncio.run( + storage.enact(_grant(effect="allow", name="A"), config={}) + ) + result = asyncio.run( + storage.list_grant_refs( + effect="deny", + action=None, + page_ref=None, + config={ + "page_size": 2 + } + ) + ) + assert result['error'] is None + assert result['page_refs'] == [0] + + +def test_base_list_grant_refs_filter_action(storage): + asyncio.run( + storage.enact( + _grant(actions=["write"], name="G"), + config={} + ) + ) + result = asyncio.run( + storage.list_grant_refs( + effect=None, + action="write", + page_ref=None, + config={ + "page_size": 10 + } + ) + ) + assert result['error'] is None + + +def test_base_create_and_get_latch(storage): + create_result = asyncio.run(storage.create_latch(config={})) + assert create_result['error'] is None + latch = create_result['storage_latch'] + assert latch['is_set'] is False + + get_result = asyncio.run( + storage.get_latch(latch['storage_latch_uuid'], config={}) + ) + assert get_result['error'] is None + assert get_result['storage_latch'] == latch + + +def test_base_get_latch_not_found(storage): + result = asyncio.run(storage.get_latch("nonexistent", config={})) + assert result['error'] is not None + assert result['error']['error_type'] == "resource_not_found" + + +def test_base_set_latch(storage): + create_result = asyncio.run(storage.create_latch(config={})) + latch_uuid = create_result['storage_latch']['storage_latch_uuid'] + set_result = asyncio.run(storage.set_latch(latch_uuid, config={})) + assert set_result['error'] is None + assert set_result['storage_latch']['is_set'] is True + + +def test_base_set_latch_not_found(storage): + result = asyncio.run(storage.set_latch("nonexistent", config={})) + assert result['error'] is not None + assert result['error']['error_type'] == "resource_not_found" + + +def test_base_delete_latch(storage): + create_result = asyncio.run(storage.create_latch(config={})) + latch_uuid = create_result['storage_latch']['storage_latch_uuid'] + del_result = asyncio.run(storage.delete_latch(latch_uuid, config={})) + assert del_result['error'] is None + get_result = asyncio.run(storage.get_latch(latch_uuid, config={})) + assert get_result['error'] is not None + + +def test_base_cleanup_latches(storage): + create_a = asyncio.run(storage.create_latch(config={})) + create_b = asyncio.run(storage.create_latch(config={})) + uuid_a = create_a['storage_latch']['storage_latch_uuid'] + uuid_b = create_b['storage_latch']['storage_latch_uuid'] + future = ( + datetime.datetime.now(tz=datetime.timezone.utc) + + datetime.timedelta(seconds=1) + ) + result = asyncio.run( + storage.cleanup_latches(before=future, config={}) + ) + assert result['error'] is None + get_a = asyncio.run(storage.get_latch(uuid_a, config={})) + get_b = asyncio.run(storage.get_latch(uuid_b, config={})) + assert get_a['error'] is not None + assert get_b['error'] is not None + + +def test_base_cleanup_latches_keeps_recent(storage): + create_result = asyncio.run(storage.create_latch(config={})) + latch_uuid = create_result['storage_latch']['storage_latch_uuid'] + past = ( + datetime.datetime.now(tz=datetime.timezone.utc) + - datetime.timedelta(seconds=10) + ) + result = asyncio.run(storage.cleanup_latches(before=past, config={})) + assert result['error'] is None + get_result = asyncio.run(storage.get_latch(latch_uuid, config={})) + assert get_result['error'] is None + + +def _new_storage(storage_dict): + """Construct a fresh storage instance of the same concrete type used by the + ``storage`` fixture, backed by ``storage_dict``. + + The concrete test module registers its storage type via + ``register_storage_type`` so the base tests can build additional instances + for construct/start lifecycle checks without knowing the class. + + Arguments + --------- + storage_dict : dict + The backing storage dict to pass to the new instance. + + Returns + ------- + StorageModule + A new, unconstructed storage instance. + """ + return _STORAGE_TYPE_HOLDER['storage_type']( + storage_dict=storage_dict + ) + + +_STORAGE_TYPE_HOLDER = { + "storage_type": None +} + + +def register_storage_type(storage_type): + """Register the concrete storage type used by the reusable base tests. + + Concrete test modules must call this at import time so lifecycle tests can + build fresh instances. + + Arguments + --------- + storage_type : type + The concrete ``StorageModule`` subclass to instantiate. + """ + _STORAGE_TYPE_HOLDER['storage_type'] = storage_type diff --git a/tests/unit/test_dict_storage.py b/tests/unit/test_dict_storage.py index 675b5d0..c92c949 100644 --- a/tests/unit/test_dict_storage.py +++ b/tests/unit/test_dict_storage.py @@ -1,17 +1,35 @@ -"""Unit tests for authzee.storage modules (StorageModule and DictStorage).""" +"""Unit tests for authzee.storage modules (StorageModule and DictStorage). + +Reuses the shared storage module test suite. Fixtures required by the shared +suite (``storage`` and ``storage_dict``) are defined here and bound to +DictStorage. Base-class ``TypeError`` tests for ``StorageModule`` stay here +since they test the abstract base rather than a concrete implementation, along +with DictStorage-specific tests that assert internal ``storage_dict`` structure +and ``has_parallel_paging``. +""" import asyncio import datetime +import os +import sys from uuid import uuid4 import pytest -from authzee.exceptions import NotImplementedError as AuthzeeNotImplementedError + +sys.path.insert(0, os.path.dirname(__file__)) + +from storage_module_test_base import * +from storage_module_test_base import register_storage_type + from authzee.module_locality import ModuleLocality from authzee.storage.dict_storage import DictStorage from authzee.storage.storage_module import StorageModule +register_storage_type(DictStorage) + + @pytest.fixture def storage_dict(): return {} @@ -221,7 +239,7 @@ def test_storage_module_cleanup_latches_raises(): ) -def test_dict_storage_start(storage_dict): +def test_dict_storage_start_parallel_paging(storage_dict): s = DictStorage(storage_dict=storage_dict) asyncio.run(s.construct(config={})) result = asyncio.run(s.start(config={})) @@ -230,12 +248,7 @@ def test_dict_storage_start(storage_dict): assert s.has_parallel_paging is True -def test_dict_storage_shutdown(storage): - result = asyncio.run(storage.shutdown(config={})) - assert result['error'] is None - - -def test_dict_storage_construct(storage_dict): +def test_dict_storage_construct_creates_luts(storage_dict): s = DictStorage(storage_dict=storage_dict) result = asyncio.run(s.construct(config={})) assert result['error'] is None @@ -246,667 +259,13 @@ def test_dict_storage_construct(storage_dict): assert "latches_lut" in storage_dict -def test_dict_storage_destroy(storage, storage_dict): +def test_dict_storage_destroy_removes_luts(storage, storage_dict): result = asyncio.run(storage.destroy(config={})) assert result['error'] is None assert "context_defs_lut" not in storage_dict -def test_dict_storage_put_and_get_context_def(storage): - context_def = { - "context_type": "NONE", - "schema": { - "type": "object" - } - } - asyncio.run(storage.put_context_def(context_def, config={})) - result = asyncio.run(storage.get_context_def("NONE", config={})) - assert result['error'] is None - assert result['context_def'] == context_def - - -def test_dict_storage_get_context_def_not_found(storage): - result = asyncio.run(storage.get_context_def("MISSING", config={})) - assert result['error'] is not None - assert result['context_def'] is None - - -def test_dict_storage_list_context_defs(storage): - asyncio.run( - storage.put_context_def( - { - "context_type": "A", - "schema": { - "type": "object" - } - }, - config={} - ) - ) - asyncio.run( - storage.put_context_def( - { - "context_type": "B", - "schema": { - "type": "object" - } - }, - config={} - ) - ) - result = asyncio.run( - storage.list_context_defs( - page_ref=None, - config={ - "page_size": 10 - } - ) - ) - assert result['error'] is None - assert len(result['context_defs']) == 2 - assert result['next_page_ref'] is None - - -def test_dict_storage_list_context_defs_pagination(storage): - for i in range(5): - asyncio.run( - storage.put_context_def( - { - "context_type": f"T{i}", - "schema": { - "type": "object" - } - }, - config={} - ) - ) - - result = asyncio.run( - storage.list_context_defs( - page_ref=None, - config={ - "page_size": 2 - } - ) - ) - assert len(result['context_defs']) == 2 - assert result['next_page_ref'] is not None - result2 = asyncio.run( - storage.list_context_defs( - page_ref=result['next_page_ref'], - config={ - "page_size": 2 - } - ) - ) - assert len(result2['context_defs']) == 2 - - -def test_dict_storage_delete_context_def(storage): - asyncio.run( - storage.put_context_def( - { - "context_type": "DEL", - "schema": { - "type": "object" - } - }, - config={} - ) - ) - result = asyncio.run(storage.delete_context_def("DEL", config={})) - assert result['error'] is None - get_result = asyncio.run(storage.get_context_def("DEL", config={})) - assert get_result['context_def'] is None - - -def test_dict_storage_put_and_get_identity_def(storage): - identity_def = { - "identity_type": "user", - "schema": { - "type": "object" - } - } - asyncio.run( - storage.put_identity_def(identity_def, config={}) - ) - result = asyncio.run(storage.get_identity_def("user", config={})) - assert result['error'] is None - assert result['identity_def'] == identity_def - - -def test_dict_storage_get_identity_def_not_found(storage): - result = asyncio.run(storage.get_identity_def("MISSING", config={})) - assert result['error'] is not None - assert result['identity_def'] is None - - -def test_dict_storage_list_identity_defs(storage): - asyncio.run( - storage.put_identity_def( - { - "identity_type": "A", - "schema": { - "type": "object" - } - }, - config={} - ) - ) - result = asyncio.run( - storage.list_identity_defs( - page_ref=None, - config={ - "page_size": 10 - } - ) - ) - assert len(result['identity_defs']) == 1 - - -def test_dict_storage_list_identity_defs_pagination(storage): - for i in range(5): - asyncio.run( - storage.put_identity_def( - { - "identity_type": f"T{i}", - "schema": { - "type": "object" - } - }, - config={} - ) - ) - - result = asyncio.run( - storage.list_identity_defs( - page_ref=None, - config={ - "page_size": 2 - } - ) - ) - assert len(result['identity_defs']) == 2 - assert result['next_page_ref'] is not None - result2 = asyncio.run( - storage.list_identity_defs( - page_ref=result['next_page_ref'], - config={ - "page_size": 2 - } - ) - ) - assert len(result2['identity_defs']) == 2 - - -def test_dict_storage_delete_identity_def(storage): - asyncio.run( - storage.put_identity_def( - { - "identity_type": "DEL", - "schema": { - "type": "object" - } - }, - config={} - ) - ) - asyncio.run(storage.delete_identity_def("DEL", config={})) - result = asyncio.run(storage.get_identity_def("DEL", config={})) - assert result['identity_def'] is None - - -def test_dict_storage_put_and_get_resource_def(storage): - resource_def = { - "resource_type": "file", - "actions": [ - "read" - ], - "schema": { - "type": "object" - } - } - asyncio.run( - storage.put_resource_def(resource_def, config={}) - ) - result = asyncio.run(storage.get_resource_def("file", config={})) - assert result['error'] is None - assert result['resource_def'] == resource_def - - -def test_dict_storage_get_resource_def_not_found(storage): - result = asyncio.run(storage.get_resource_def("MISSING", config={})) - assert result['error'] is not None - assert result['resource_def'] is None - - -def test_dict_storage_list_resource_defs(storage): - asyncio.run( - storage.put_resource_def( - { - "resource_type": "A", - "actions": [ - "x" - ], - "schema": { - "type": "object" - } - }, - config={} - ) - ) - result = asyncio.run( - storage.list_resource_defs( - page_ref=None, - config={ - "page_size": 10 - } - ) - ) - assert len(result['resource_defs']) == 1 - - -def test_dict_storage_list_resource_defs_pagination(storage): - for i in range(5): - asyncio.run( - storage.put_resource_def( - { - "resource_type": f"T{i}", - "actions": [ - "x" - ], - "schema": { - "type": "object" - } - }, - config={} - ) - ) - - result = asyncio.run( - storage.list_resource_defs( - page_ref=None, - config={ - "page_size": 2 - } - ) - ) - assert len(result['resource_defs']) == 2 - assert result['next_page_ref'] is not None - result2 = asyncio.run( - storage.list_resource_defs( - page_ref=result['next_page_ref'], - config={ - "page_size": 2 - } - ) - ) - assert len(result2['resource_defs']) == 2 - - -def test_dict_storage_delete_resource_def(storage): - asyncio.run( - storage.put_resource_def( - { - "resource_type": "DEL", - "actions": [ - "x" - ], - "schema": { - "type": "object" - } - }, - config={} - ) - ) - asyncio.run(storage.delete_resource_def("DEL", config={})) - result = asyncio.run(storage.get_resource_def("DEL", config={})) - assert result['resource_def'] is None - - -@pytest.fixture -def sample_grant(): - return { - "grant_uuid": str(uuid4()), - "name": "Test", - "description": "", - "tags": {}, - "effect": "allow", - "actions": [ - "read" - ], - "query": "`true`", - "equality": True, - "applicable_on_failure": False, - "data": {} - } - - -def test_dict_storage_enact_and_get_grant(storage, sample_grant): - asyncio.run(storage.enact(sample_grant, config={})) - result = asyncio.run( - storage.get_grant(sample_grant['grant_uuid'], config={}) - ) - assert result['error'] is None - assert result['grant'] == sample_grant - - -def test_dict_storage_get_grant_not_found(storage): - result = asyncio.run( - storage.get_grant("nonexistent-uuid", config={}) - ) - assert result['error'] is not None - assert result['grant'] is None - - -def test_dict_storage_repeal(storage, sample_grant): - asyncio.run(storage.enact(sample_grant, config={})) - result = asyncio.run( - storage.repeal( - sample_grant['grant_uuid'], - purge=True, - config={} - ) - ) - assert result['error'] is None - get_result = asyncio.run( - storage.get_grant(sample_grant['grant_uuid'], config={}) - ) - assert get_result['grant'] is None - - -def test_dict_storage_list_grants(storage, sample_grant): - asyncio.run(storage.enact(sample_grant, config={})) - result = asyncio.run( - storage.list_grants( - effect=None, - action=None, - page_ref=None, - config={ - "page_size": 10 - } - ) - ) - assert len(result['grants']) == 1 - - -def test_dict_storage_list_grants_filter_effect(storage): - allow_grant = { - "grant_uuid": str(uuid4()), - "name": "A", - "description": "", - "tags": {}, - "effect": "allow", - "actions": [ - "read" - ], - "query": "`true`", - "equality": True, - "applicable_on_failure": False, - "data": {} - } - deny_grant = { - "grant_uuid": str(uuid4()), - "name": "D", - "description": "", - "tags": {}, - "effect": "deny", - "actions": [ - "read" - ], - "query": "`true`", - "equality": True, - "applicable_on_failure": False, - "data": {} - } - asyncio.run(storage.enact(allow_grant, config={})) - asyncio.run(storage.enact(deny_grant, config={})) - result = asyncio.run( - storage.list_grants( - effect="allow", - action=None, - page_ref=None, - config={ - "page_size": 10 - } - ) - ) - assert len(result['grants']) == 1 - assert result['grants'][0]['effect'] == "allow" - - -def test_dict_storage_list_grants_filter_action(storage): - grant1 = { - "grant_uuid": str(uuid4()), - "name": "G1", - "description": "", - "tags": {}, - "effect": "allow", - "actions": [ - "read", - "write" - ], - "query": "`true`", - "equality": True, - "applicable_on_failure": False, - "data": {} - } - grant2 = { - "grant_uuid": str(uuid4()), - "name": "G2", - "description": "", - "tags": {}, - "effect": "allow", - "actions": [ - "delete" - ], - "query": "`true`", - "equality": True, - "applicable_on_failure": False, - "data": {} - } - asyncio.run(storage.enact(grant1, config={})) - asyncio.run(storage.enact(grant2, config={})) - result = asyncio.run( - storage.list_grants( - effect=None, - action="write", - page_ref=None, - config={ - "page_size": 10 - } - ) - ) - assert len(result['grants']) == 1 - assert result['grants'][0]['name'] == "G1" - - -def test_dict_storage_list_grants_pagination(storage): - for i in range(5): - g = { - "grant_uuid": str(uuid4()), - "name": f"G{i}", - "description": "", - "tags": {}, - "effect": "allow", - "actions": [ - "read" - ], - "query": "`true`", - "equality": True, - "applicable_on_failure": False, - "data": {} - } - asyncio.run(storage.enact(g, config={})) - - result = asyncio.run( - storage.list_grants( - effect=None, - action=None, - page_ref=None, - config={ - "page_size": 2 - } - ) - ) - assert len(result['grants']) == 2 - assert result['next_page_ref'] is not None - result2 = asyncio.run( - storage.list_grants( - effect=None, - action=None, - page_ref=result['next_page_ref'], - config={ - "page_size": 2 - } - ) - ) - assert len(result2['grants']) == 2 - - -def test_dict_storage_list_grant_refs(storage): - for i in range(5): - g = { - "grant_uuid": str(uuid4()), - "name": f"G{i}", - "description": "", - "tags": {}, - "effect": "allow", - "actions": [ - "read" - ], - "query": "`true`", - "equality": True, - "applicable_on_failure": False, - "data": {} - } - asyncio.run(storage.enact(g, config={})) - - result = asyncio.run( - storage.list_grant_refs( - effect=None, - action=None, - page_ref=None, - config={ - "page_size": 2 - } - ) - ) - assert result['error'] is None - assert len(result['page_refs']) > 0 - if result['next_page_ref'] is not None: - result2 = asyncio.run( - storage.list_grant_refs( - effect=None, - action=None, - page_ref=str(result['next_page_ref']), - config={ - "page_size": 2 - } - ) - ) - assert result2['error'] is None - - -def test_dict_storage_list_grant_refs_filter_effect(storage): - allow_grant = { - "grant_uuid": str(uuid4()), - "name": "A", - "description": "", - "tags": {}, - "effect": "allow", - "actions": [ - "read" - ], - "query": "`true`", - "equality": True, - "applicable_on_failure": False, - "data": {} - } - asyncio.run(storage.enact(allow_grant, config={})) - result = asyncio.run( - storage.list_grant_refs( - effect="deny", - action=None, - page_ref=None, - config={ - "page_size": 2 - } - ) - ) - assert result['page_refs'] == [0] - - -def test_dict_storage_list_grant_refs_filter_action(storage): - g = { - "grant_uuid": str(uuid4()), - "name": "G", - "description": "", - "tags": {}, - "effect": "allow", - "actions": [ - "write" - ], - "query": "`true`", - "equality": True, - "applicable_on_failure": False, - "data": {} - } - asyncio.run(storage.enact(g, config={})) - result = asyncio.run( - storage.list_grant_refs( - effect=None, - action="write", - page_ref=None, - config={ - "page_size": 10 - } - ) - ) - assert result['error'] is None - - -def test_dict_storage_create_and_get_latch(storage): - create_result = asyncio.run(storage.create_latch(config={})) - assert create_result['error'] is None - latch = create_result['storage_latch'] - assert latch['is_set'] is False - - get_result = asyncio.run( - storage.get_latch(latch['storage_latch_uuid'], config={}) - ) - assert get_result['error'] is None - assert get_result['storage_latch'] == latch - - -def test_dict_storage_get_latch_not_found(storage): - result = asyncio.run(storage.get_latch("nonexistent", config={})) - assert result['error'] is not None - - -def test_dict_storage_set_latch(storage): - create_result = asyncio.run(storage.create_latch(config={})) - latch_uuid = create_result['storage_latch']['storage_latch_uuid'] - set_result = asyncio.run(storage.set_latch(latch_uuid, config={})) - assert set_result['error'] is None - assert set_result['storage_latch']['is_set'] is True - - -def test_dict_storage_set_latch_not_found(storage): - result = asyncio.run(storage.set_latch("nonexistent", config={})) - assert result['error'] is not None - - -def test_dict_storage_delete_latch(storage): - create_result = asyncio.run(storage.create_latch(config={})) - latch_uuid = create_result['storage_latch']['storage_latch_uuid'] - del_result = asyncio.run(storage.delete_latch(latch_uuid, config={})) - assert del_result['error'] is None - get_result = asyncio.run(storage.get_latch(latch_uuid, config={})) - assert get_result['error'] is not None - - -def test_dict_storage_cleanup_latches(storage): +def test_dict_storage_cleanup_latches_removes_from_dict(storage): asyncio.run(storage.create_latch(config={})) asyncio.run(storage.create_latch(config={})) future = ( @@ -920,7 +279,7 @@ def test_dict_storage_cleanup_latches(storage): assert len(storage._storage_dict['latches_lut']) == 0 -def test_dict_storage_cleanup_latches_keeps_recent(storage): +def test_dict_storage_cleanup_latches_keeps_recent_in_dict(storage): asyncio.run(storage.create_latch(config={})) past = ( datetime.datetime.now(tz=datetime.timezone.utc) From 3e01f5f0284fd3d70b1f2cce6c94a96e4bfc20a8 Mon Sep 17 00:00:00 2001 From: btemplep Date: Thu, 27 Aug 2026 23:46:23 -0400 Subject: [PATCH 9/9] new release --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 58d008c..5b7c1d5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,7 +22,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Security --> -## [0.1.0a6] - TBD +## [0.1.0a6] - 2026-08-27 Support for Authzee spec 0.5.0.