Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .bumpversion.cfg
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[bumpversion]
current_version = 2.4.2
current_version = 2.5.0
commit = False
tag = False
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(\-(?P<release>[a-z]+)(?P<build>\d+))?
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/test-package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion oauth2_lib/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,4 @@

"""This is the SURF Oauth2 module that interfaces with the oauth2 setup."""

__version__ = "2.4.2"
__version__ = "2.5.0"
6 changes: 3 additions & 3 deletions oauth2_lib/fastapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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

Expand All @@ -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,
}
}

Expand Down
8 changes: 4 additions & 4 deletions oauth2_lib/strawberry.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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}`"
Expand All @@ -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}`"
Expand Down
3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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/"
Expand Down
28 changes: 14 additions & 14 deletions tests/test_opa_graphql_decision.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand All @@ -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)


Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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"
Expand All @@ -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

Expand All @@ -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)
Expand All @@ -131,15 +131,15 @@ 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 = {
"input": {
"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)
Expand All @@ -156,15 +156,15 @@ 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 = {
"input": {
"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)