diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 19b9cd7..6df966d 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 2.4.2 +current_version = 2.5.0 commit = False tag = False parse = (?P\d+)\.(?P\d+)\.(?P\d+)(\-(?P[a-z]+)(?P\d+))? diff --git a/.github/workflows/test-package.yml b/.github/workflows/test-package.yml index 508a9ff..851eae0 100644 --- a/.github/workflows/test-package.yml +++ b/.github/workflows/test-package.yml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ['3.11', '3.12', '3.13'] + python-version: ['3.11', '3.12', '3.13', '3.14'] fail-fast: false steps: - uses: actions/checkout@v2 diff --git a/oauth2_lib/__init__.py b/oauth2_lib/__init__.py index 4543c08..1f1d83e 100644 --- a/oauth2_lib/__init__.py +++ b/oauth2_lib/__init__.py @@ -13,4 +13,4 @@ """This is the SURF Oauth2 module that interfaces with the oauth2 setup.""" -__version__ = "2.4.2" +__version__ = "2.5.0" diff --git a/oauth2_lib/fastapi.py b/oauth2_lib/fastapi.py index 9170cbb..41a038c 100644 --- a/oauth2_lib/fastapi.py +++ b/oauth2_lib/fastapi.py @@ -260,7 +260,7 @@ class GraphqlAuthorization(ABC): """ @abstractmethod - async def authorize(self, request: RequestPath, user: OIDCUserModel) -> bool | None: + async def authorize(self, request: RequestPath, method: str, user: OIDCUserModel) -> bool | None: pass @@ -366,7 +366,7 @@ def __init__(self, opa_url: str, auto_error: bool = False, opa_kwargs: Mapping[s # By default don't raise HTTP 403 because partial results are preferred super().__init__(opa_url, auto_error, opa_kwargs) - async def authorize(self, request: RequestPath, user_info: OIDCUserModel) -> bool | None: + async def authorize(self, request: RequestPath, method: str, user_info: OIDCUserModel) -> bool | None: if not (oauth2lib_settings.OAUTH2_ACTIVE and oauth2lib_settings.OAUTH2_AUTHORIZATION_ACTIVE): return None @@ -375,7 +375,7 @@ async def authorize(self, request: RequestPath, user_info: OIDCUserModel) -> boo **(self.opa_kwargs or {}), **(user_info or {}), "resource": request, - "method": "POST", + "method": method, } } diff --git a/oauth2_lib/strawberry.py b/oauth2_lib/strawberry.py index dd346c4..df0691e 100644 --- a/oauth2_lib/strawberry.py +++ b/oauth2_lib/strawberry.py @@ -105,14 +105,14 @@ async def is_authenticated(info: OauthInfo) -> bool: return current_user is not None -async def is_authorized(info: OauthInfo, path: str) -> bool: +async def is_authorized(info: OauthInfo, path: str, method: str) -> bool: """Check that the user is allowed to query/mutate this path.""" context = info.context current_user = await context.get_current_user if not current_user: return False - authorization_decision = await context.auth_manager.graphql_authorization.authorize(path, current_user) + authorization_decision = await context.auth_manager.graphql_authorization.authorize(path, method, current_user) authorized = bool(authorization_decision) logger.debug( "Received graphql authorization decision", @@ -172,7 +172,7 @@ async def has_permission(self, source: Any, info: OauthInfo, **kwargs) -> bool: return True path = get_query_path(info) - if await is_authorized(info, path): + if await is_authorized(info, path, "QUERY"): return True self.message = f"User is not authorized to query `{path}`" @@ -192,7 +192,7 @@ async def has_permission(self, source: Any, info: OauthInfo, **kwargs) -> bool: return skip_mutation_auth_checks() path = get_mutation_path(info) - if await is_authorized(info, path): + if await is_authorized(info, path, "POST"): return True self.message = f"User is not authorized to execute mutation `{path}`" diff --git a/pyproject.toml b/pyproject.toml index 033e50b..ffe4daf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,6 +25,7 @@ classifiers = [ "Intended Audience :: Telecommunications Industry", "License :: OSI Approved :: Apache Software License", "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.14", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.11", @@ -41,7 +42,7 @@ requires = [ "asyncstdlib", ] description-file = "README.md" -requires-python = ">=3.11,<3.14" +requires-python = ">=3.11,<3.15" [tool.flit.metadata.urls] Documentation = "https://workfloworchestrator.org/" diff --git a/tests/test_opa_graphql_decision.py b/tests/test_opa_graphql_decision.py index e986069..1185e69 100644 --- a/tests/test_opa_graphql_decision.py +++ b/tests/test_opa_graphql_decision.py @@ -15,7 +15,7 @@ async def test_opa_graphql_decision_auto_error(): oauth2lib_settings.OAUTH2_ACTIVE = False authorization = GraphQLOPAAuthorization(opa_url="https://opa_url.test") - assert await authorization.authorize("", cast(OIDCUserModel, {})) is None + assert await authorization.authorize("", "QUERY", cast(OIDCUserModel, {})) is None oauth2lib_settings.OAUTH2_ACTIVE = True @@ -28,13 +28,13 @@ async def test_opa_graphql_decision_user_not_allowed_autoerror_true(make_mock_as with patch("oauth2_lib.fastapi.AsyncClient", return_value=mock_async_client): authorization = GraphQLOPAAuthorization(opa_url="https://opa_url.test", auto_error=True) with pytest.raises(HTTPException) as exception_info: - await authorization.authorize("/test/path", user_info=user_info_matching) + await authorization.authorize("/test/path", "QUERY", user_info=user_info_matching) assert exception_info.value.status_code == HTTPStatus.FORBIDDEN expected_detail = f"User is not allowed to access resource: /test/path Decision was taken with id: {'8ef9daf0-1a23-4a6b-8433-c64ef028bee8'}" assert exception_info.value.detail == expected_detail - opa_input = {"input": {**user_info_matching, "resource": "/test/path", "method": "POST"}} + opa_input = {"input": {**user_info_matching, "resource": "/test/path", "method": "QUERY"}} mock_async_client.client.post.assert_called_with("https://opa_url.test", json=opa_input) @@ -46,14 +46,14 @@ async def test_opa_graphql_decision_user_not_allowed_autoerror_false(make_mock_a with patch("oauth2_lib.fastapi.AsyncClient", return_value=mock_async_client): authorization = GraphQLOPAAuthorization(opa_url="https://opa_url.test", auto_error=False) - result = await authorization.authorize("/test/path", user_info_matching) + result = await authorization.authorize("/test/path", "QUERY", user_info_matching) assert result is False opa_input = { "input": { **user_info_matching, "resource": "/test/path", - "method": "POST", + "method": "QUERY", } } mock_async_client.client.post.assert_called_with("https://opa_url.test", json=opa_input) @@ -67,14 +67,14 @@ async def test_opa_graphql_decision_user_allowed(make_mock_async_client): with patch("oauth2_lib.fastapi.AsyncClient", return_value=mock_async_client): authorization = GraphQLOPAAuthorization(opa_url="https://opa_url.test", auto_error=False) - result = await authorization.authorize("/test/path", user_info_matching) + result = await authorization.authorize("/test/path", "QUERY", user_info_matching) assert result is True opa_input = { "input": { **user_info_matching, "resource": "/test/path", - "method": "POST", + "method": "QUERY", } } mock_async_client.client.post.assert_called_with("https://opa_url.test", json=opa_input) @@ -88,7 +88,7 @@ async def test_opa_graphql_decision_network_or_type_error(make_mock_async_client authorization = GraphQLOPAAuthorization(opa_url="https://opa_url.test") with pytest.raises(HTTPException) as exception: - await authorization.authorize("/test/path", user_info_matching) + await authorization.authorize("/test/path", "QUERY", user_info_matching) assert exception.value.status_code == 503 assert exception.value.detail == "Policy agent is unavailable" @@ -105,7 +105,7 @@ async def test_opa_graphql_decision_kwargs(make_mock_async_client): opa_url="https://opa_url.test", auto_error=False, opa_kwargs={"extra": 3} ) - result = await authorization.authorize("/test/path", user_info_matching) + result = await authorization.authorize("/test/path", "QUERY", user_info_matching) assert result is True @@ -114,7 +114,7 @@ async def test_opa_graphql_decision_kwargs(make_mock_async_client): "extra": 3, **user_info_matching, "resource": "/test/path", - "method": "POST", + "method": "QUERY", } } mock_async_client.client.post.assert_called_with("https://opa_url.test", json=opa_input) @@ -131,7 +131,7 @@ async def test_opa_decision_auto_error_not_allowed(make_mock_async_client): opa_url="https://opa_url.test", opa_kwargs={"extra": 3}, auto_error=False ) - result = await authorization.authorize("/test/path", user_info_matching) + result = await authorization.authorize("/test/path", "QUERY", user_info_matching) assert result is False opa_input = { @@ -139,7 +139,7 @@ async def test_opa_decision_auto_error_not_allowed(make_mock_async_client): "extra": 3, **user_info_matching, "resource": "/test/path", - "method": "POST", + "method": "QUERY", } } mock_async_client.client.post.assert_called_with("https://opa_url.test", json=opa_input) @@ -156,7 +156,7 @@ async def test_opa_graphql_decision_auto_error_allowed(make_mock_async_client): opa_url="https://opa_url.test", opa_kwargs={"extra": 3}, auto_error=False ) - result = await authorization.authorize("/test/path", user_info_matching) + result = await authorization.authorize("/test/path", "QUERY", user_info_matching) assert result is True opa_input = { @@ -164,7 +164,7 @@ async def test_opa_graphql_decision_auto_error_allowed(make_mock_async_client): "extra": 3, **user_info_matching, "resource": "/test/path", - "method": "POST", + "method": "QUERY", } } mock_async_client.client.post.assert_called_with("https://opa_url.test", json=opa_input)