From ba0c7cc4c436e6264a900e84ee7121e1ef23aad5 Mon Sep 17 00:00:00 2001 From: Aaron Tainter Date: Thu, 8 May 2025 10:56:00 -0700 Subject: [PATCH 1/3] Add warnings to FGA check / query response --- tests/test_fga.py | 83 ++++++++++++++++++++++++++++++++++++ workos/fga.py | 11 +++-- workos/types/fga/check.py | 2 + workos/types/fga/warnings.py | 21 +++++++++ 4 files changed, 113 insertions(+), 4 deletions(-) create mode 100644 workos/types/fga/warnings.py diff --git a/tests/test_fga.py b/tests/test_fga.py index 5bf3e046..4f935790 100644 --- a/tests/test_fga.py +++ b/tests/test_fga.py @@ -101,6 +101,89 @@ def test_get_resource_401(self, mock_http_client_with_response): self.fga.get_resource(resource_type="test", resource_id="test") +class TestWarnings: + @pytest.fixture(autouse=True) + def setup(self, sync_http_client_for_test): + self.http_client = sync_http_client_for_test + self.fga = FGA(http_client=self.http_client) + + @pytest.fixture + def mock_check_warning_response(self): + return { + "result": "authorized", + "is_implicit": True, + "warnings": [ + { + "code": "missing_context_keys", + "message": "Missing context keys", + "keys": ["key1", "key2"], + } + ], + } + + @pytest.fixture + def mock_query_warning_response(self): + return { + "object": "list", + "data": [ + { + "resource_type": "user", + "resource_id": "richard", + "relation": "member", + "warrant": { + "resource_type": "role", + "resource_id": "developer", + "relation": "member", + "subject": {"resource_type": "user", "resource_id": "richard"}, + }, + "is_implicit": True, + } + ], + "list_metadata": {}, + "warnings": [ + { + "code": "missing_context_keys", + "message": "Missing context keys", + "keys": ["key1", "key2"], + } + ], + } + + def test_check_with_warning( + self, mock_check_warning_response, mock_http_client_with_response + ): + mock_http_client_with_response( + self.http_client, mock_check_warning_response, 200 + ) + + response = self.fga.check( + op="any_of", + checks=[ + WarrantCheckInput( + resource_type="schedule", + resource_id="schedule-A1", + relation="viewer", + subject=SubjectInput(resource_type="user", resource_id="user-A"), + ) + ], + ) + assert response.dict(exclude_none=True) == mock_check_warning_response + + def test_query_with_warning( + self, mock_query_warning_response, mock_http_client_with_response + ): + mock_http_client_with_response( + self.http_client, mock_query_warning_response, 200 + ) + + response = self.fga.query( + q="select member of type user for permission:view-docs", + order="asc", + warrant_token="warrant_token", + ) + assert response.dict(exclude_none=True) == mock_query_warning_response + + class TestFGA: @pytest.fixture(autouse=True) def setup(self, sync_http_client_for_test): diff --git a/workos/fga.py b/workos/fga.py index 5b143b28..66481c92 100644 --- a/workos/fga.py +++ b/workos/fga.py @@ -10,7 +10,7 @@ WarrantWrite, WarrantWriteOperation, WriteWarrantResponse, - WarrantQueryResult, + WarrantQueryResult, FGAWarning, ) from workos.types.fga.list_filters import ( AuthorizationResourceListFilters, @@ -45,9 +45,11 @@ WarrantListResource = WorkOSListResource[Warrant, WarrantListFilters, ListMetadata] -WarrantQueryListResource = WorkOSListResource[ - WarrantQueryResult, WarrantQueryListFilters, ListMetadata -] + +class WarrantQueryListResource(WorkOSListResource[ + WarrantQueryResult, WarrantQueryListFilters, ListMetadata + ]): + warnings: Optional[Sequence[FGAWarning]] = None class FGAModule(Protocol): @@ -641,5 +643,6 @@ def query( return WarrantQueryListResource( list_method=self.query, list_args=list_params, + warnings=response.get("warnings"), **ListPage[WarrantQueryResult](**response).model_dump(), ) diff --git a/workos/types/fga/check.py b/workos/types/fga/check.py index 74f263ee..cac38520 100644 --- a/workos/types/fga/check.py +++ b/workos/types/fga/check.py @@ -3,6 +3,7 @@ from workos.types.workos_model import WorkOSModel from workos.typing.literals import LiteralOrUntyped +from .warnings import FGAWarning from .warrant import Subject, SubjectInput CheckOperation = Literal["any_of", "all_of", "batch"] @@ -44,6 +45,7 @@ class CheckResponse(WorkOSModel): result: LiteralOrUntyped[CheckResult] is_implicit: bool debug_info: Optional[DebugInfo] = None + warnings: Optional[Sequence[FGAWarning]] = None def authorized(self) -> bool: return self.result == "authorized" diff --git a/workos/types/fga/warnings.py b/workos/types/fga/warnings.py new file mode 100644 index 00000000..64f95757 --- /dev/null +++ b/workos/types/fga/warnings.py @@ -0,0 +1,21 @@ +from typing import Sequence, Union, Literal, Annotated + +from pydantic import Field + +from workos.types.workos_model import WorkOSModel + + +class FGABaseWarning(WorkOSModel): + code: str + message: str + + +class MissingContextKeysWarning(FGABaseWarning): + code: Literal["missing_context_keys"] + keys: Sequence[str] + + +FGAWarning = Annotated[ + Union[MissingContextKeysWarning, FGABaseWarning], + Field(discriminator='type') +] From 4fb808be9ede3ac44164c9403e66754347dff00d Mon Sep 17 00:00:00 2001 From: Aaron Tainter Date: Thu, 8 May 2025 12:18:45 -0700 Subject: [PATCH 2/3] Fix typing issues with Pydantic --- tests/test_fga.py | 66 ++++++++++++++++++++++-------------- workos/fga.py | 9 ++--- workos/types/fga/__init__.py | 1 + workos/types/fga/warnings.py | 16 ++++++--- 4 files changed, 58 insertions(+), 34 deletions(-) diff --git a/tests/test_fga.py b/tests/test_fga.py index 4f935790..c0f2337b 100644 --- a/tests/test_fga.py +++ b/tests/test_fga.py @@ -107,9 +107,8 @@ def setup(self, sync_http_client_for_test): self.http_client = sync_http_client_for_test self.fga = FGA(http_client=self.http_client) - @pytest.fixture - def mock_check_warning_response(self): - return { + def test_check_with_warning(self, mock_http_client_with_response): + mock_response = { "result": "authorized", "is_implicit": True, "warnings": [ @@ -120,10 +119,23 @@ def mock_check_warning_response(self): } ], } + mock_http_client_with_response(self.http_client, mock_response, 200) - @pytest.fixture - def mock_query_warning_response(self): - return { + response = self.fga.check( + op="any_of", + checks=[ + WarrantCheckInput( + resource_type="schedule", + resource_id="schedule-A1", + relation="viewer", + subject=SubjectInput(resource_type="user", resource_id="user-A"), + ) + ], + ) + assert response.dict(exclude_none=True) == mock_response + + def test_query_with_warning(self, mock_http_client_with_response): + mock_response = { "object": "list", "data": [ { @@ -149,12 +161,28 @@ def mock_query_warning_response(self): ], } - def test_check_with_warning( - self, mock_check_warning_response, mock_http_client_with_response - ): - mock_http_client_with_response( - self.http_client, mock_check_warning_response, 200 + mock_http_client_with_response(self.http_client, mock_response, 200) + + response = self.fga.query( + q="select member of type user for permission:view-docs", + order="asc", + warrant_token="warrant_token", ) + assert response.dict(exclude_none=True) == mock_response + + def test_check_with_generic_warning(self, mock_http_client_with_response): + mock_response = { + "result": "authorized", + "is_implicit": True, + "warnings": [ + { + "code": "generic", + "message": "Generic warning", + } + ], + } + + mock_http_client_with_response(self.http_client, mock_response, 200) response = self.fga.check( op="any_of", @@ -167,21 +195,7 @@ def test_check_with_warning( ) ], ) - assert response.dict(exclude_none=True) == mock_check_warning_response - - def test_query_with_warning( - self, mock_query_warning_response, mock_http_client_with_response - ): - mock_http_client_with_response( - self.http_client, mock_query_warning_response, 200 - ) - - response = self.fga.query( - q="select member of type user for permission:view-docs", - order="asc", - warrant_token="warrant_token", - ) - assert response.dict(exclude_none=True) == mock_query_warning_response + assert response.dict(exclude_none=True) == mock_response class TestFGA: diff --git a/workos/fga.py b/workos/fga.py index 66481c92..2c76a320 100644 --- a/workos/fga.py +++ b/workos/fga.py @@ -10,7 +10,8 @@ WarrantWrite, WarrantWriteOperation, WriteWarrantResponse, - WarrantQueryResult, FGAWarning, + WarrantQueryResult, + FGAWarning, ) from workos.types.fga.list_filters import ( AuthorizationResourceListFilters, @@ -46,9 +47,9 @@ WarrantListResource = WorkOSListResource[Warrant, WarrantListFilters, ListMetadata] -class WarrantQueryListResource(WorkOSListResource[ - WarrantQueryResult, WarrantQueryListFilters, ListMetadata - ]): +class WarrantQueryListResource( + WorkOSListResource[WarrantQueryResult, WarrantQueryListFilters, ListMetadata] +): warnings: Optional[Sequence[FGAWarning]] = None diff --git a/workos/types/fga/__init__.py b/workos/types/fga/__init__.py index 0558f426..7d8a640c 100644 --- a/workos/types/fga/__init__.py +++ b/workos/types/fga/__init__.py @@ -2,3 +2,4 @@ from .authorization_resource_types import * from .authorization_resources import * from .warrant import * +from .warnings import * diff --git a/workos/types/fga/warnings.py b/workos/types/fga/warnings.py index 64f95757..af979f52 100644 --- a/workos/types/fga/warnings.py +++ b/workos/types/fga/warnings.py @@ -1,6 +1,7 @@ -from typing import Sequence, Union, Literal, Annotated +from typing import Sequence, Annotated, Union, Any, Dict -from pydantic import Field +from pydantic import BeforeValidator +from pydantic_core.core_schema import ValidationInfo from workos.types.workos_model import WorkOSModel @@ -11,11 +12,18 @@ class FGABaseWarning(WorkOSModel): class MissingContextKeysWarning(FGABaseWarning): - code: Literal["missing_context_keys"] keys: Sequence[str] +def fga_warning_dispatch_validator( + value: Dict[str, Any], info: ValidationInfo +) -> FGABaseWarning: + if value.get("code") == "missing_context_keys": + return MissingContextKeysWarning.model_validate(value) + return FGABaseWarning.model_validate(value) + + FGAWarning = Annotated[ Union[MissingContextKeysWarning, FGABaseWarning], - Field(discriminator='type') + BeforeValidator(fga_warning_dispatch_validator), ] From 8afcf010c6c04d0c636b9d67bdcaea80d89d6e89 Mon Sep 17 00:00:00 2001 From: Aaron Tainter Date: Thu, 8 May 2025 12:24:21 -0700 Subject: [PATCH 3/3] Fix python 3.8 issue --- workos/types/fga/warnings.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/workos/types/fga/warnings.py b/workos/types/fga/warnings.py index af979f52..d7c7c986 100644 --- a/workos/types/fga/warnings.py +++ b/workos/types/fga/warnings.py @@ -1,4 +1,5 @@ -from typing import Sequence, Annotated, Union, Any, Dict +from typing import Sequence, Union, Any, Dict, Literal +from typing_extensions import Annotated from pydantic import BeforeValidator from pydantic_core.core_schema import ValidationInfo @@ -12,6 +13,7 @@ class FGABaseWarning(WorkOSModel): class MissingContextKeysWarning(FGABaseWarning): + code: Literal["missing_context_keys"] keys: Sequence[str] @@ -20,6 +22,8 @@ def fga_warning_dispatch_validator( ) -> FGABaseWarning: if value.get("code") == "missing_context_keys": return MissingContextKeysWarning.model_validate(value) + + # Fallback to the base warning model return FGABaseWarning.model_validate(value)