Skip to content

Commit 5cd517f

Browse files
Prepare for v1.0.3 🔱
1 parent 8fdf2e6 commit 5cd517f

File tree

12 files changed

+633
-84
lines changed

12 files changed

+633
-84
lines changed

.github/workflows/build.yml

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,10 @@ jobs:
2020
strategy:
2121
fail-fast: false
2222
matrix:
23-
python-version: [3.8, 3.9, "3.10", "3.11"]
23+
python-version: [3.9, "3.10", "3.11", "3.12", "3.13"]
2424

2525
steps:
26-
- uses: actions/checkout@v1
26+
- uses: actions/checkout@v4
2727
with:
2828
fetch-depth: 9
2929
submodules: false
@@ -33,7 +33,7 @@ jobs:
3333
with:
3434
python-version: ${{ matrix.python-version }}
3535

36-
- uses: actions/cache@v1
36+
- uses: actions/cache@v4
3737
id: depcache
3838
with:
3939
path: deps
@@ -69,7 +69,7 @@ jobs:
6969
for f in ./examples/*.py; do echo "Processing $f file..." && python $f; done
7070
7171
- name: Upload pytest test results
72-
uses: actions/upload-artifact@master
72+
uses: actions/upload-artifact@v4
7373
with:
7474
name: pytest-results-${{ matrix.python-version }}
7575
path: junit/pytest-results-${{ matrix.python-version }}.xml
@@ -81,34 +81,34 @@ jobs:
8181
8282
- name: Install distribution dependencies
8383
run: pip install --upgrade build
84-
if: matrix.python-version == 3.10
84+
if: matrix.python-version == 3.12
8585

8686
- name: Create distribution package
8787
run: python -m build
88-
if: matrix.python-version == 3.10
88+
if: matrix.python-version == 3.12
8989

9090
- name: Upload distribution package
91-
uses: actions/upload-artifact@master
91+
uses: actions/upload-artifact@v4
9292
with:
9393
name: dist
9494
path: dist
95-
if: matrix.python-version == 3.10
95+
if: matrix.python-version == 3.12
9696

9797
publish:
9898
runs-on: ubuntu-latest
9999
needs: build
100100
if: github.event_name == 'release'
101101
steps:
102102
- name: Download a distribution artifact
103-
uses: actions/download-artifact@v2
103+
uses: actions/download-artifact@v4
104104
with:
105105
name: dist
106106
path: dist
107107

108-
- name: Use Python 3.11
109-
uses: actions/setup-python@v1
108+
- name: Use Python 3.12
109+
uses: actions/setup-python@v3
110110
with:
111-
python-version: '3.11'
111+
python-version: "3.12"
112112

113113
- name: Install dependencies
114114
run: |

CHANGELOG.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,20 +5,37 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [1.0.3] - 2025-10-04 :trident:
9+
10+
- Add a `roles` property to the `Identity` object.
11+
- Add a `RolesRequirement` class to authorize by **sufficient roles**
12+
(any one is enough).
13+
- Add support for validating JWTs signed using symmetric encryption
14+
(`SymmetricJWTValidator` and `AsymmetricJWTValidator`).
15+
- Add support to call the `authorize` method with an optional set of roles,
16+
treated as sufficient roles to succeed authorization.
17+
- Add Python `3.12` and `3.13` to the build matrix.
18+
- Remove Python `3.8` from the build matrix.
19+
- Improve `pyproject.toml`.
20+
- Workflow maintenance.
21+
822
## [1.0.2] - 2023-06-16 :corn:
23+
924
- Raises a more specific exception `ForbiddenError` when the user of an
1025
operation is authenticated properly, but authorization fails.
1126
This enables better handling of authorization error, differentiating when the
1227
user context is missing or invalid, and when the context is valid but the
1328
user has no rights to do a certain operation. See [#371](https://github.com/Neoteroi/BlackSheep/issues/371).
1429

1530
## [1.0.1] - 2023-03-20 :sun_with_face:
31+
1632
- Improves the automatic rotation of `JWKS`: when validating `JWTs`, `JWKS` are
1733
refreshed automatically if an unknown `kid` is encountered, and `JWKS` were
1834
last fetched more than `refresh_time` seconds ago (by default 120 seconds).
1935
- Corrects an inconsistency in how `claims` are read in the `User` class.
2036

2137
## [1.0.0] - 2023-01-07 :star:
38+
2239
- Adds built-in support for dependency injection, using the new `ContainerProtocol`
2340
in `rodi` v2.
2441
- Removes the synchronous code API, maintaining only the asynchronous code API
@@ -29,24 +46,29 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2946
- Corrects `Identity.__getitem__` to raise `KeyError` if a claim is missing.
3047

3148
## [0.1.0] - 2022-11-06 :snake:
49+
3250
- Workflow maintenance.
3351

3452
## [0.0.9] - 2021-11-14 :swan:
53+
3554
- Adds `sub`, `access_token`, and `refresh_token` properties to the `Identity`.
3655
class
3756
- Adds `py.typed` file.
3857

3958
## [0.0.8] - 2021-10-31 :shield:
59+
4060
- Adds classes to handle `JWT`s validation, but only for `RSA` keys.
4161
- Fixes issue (wrong arrangement in test) #5.
4262
- Includes `Python 3.10` in the CI/CD matrix.
4363
- Enforces `black` and `isort` in the CI pipeline.
4464

4565
## [0.0.7] - 2021-01-31 :grapes:
66+
4667
- Corrects a bug in the `Policy` class (#2).
4768
- Changes the type annotation of `Identity` claims (#3).
4869

4970
## [0.0.6] - 2020-12-12 :octocat:
71+
5072
- Completely migrates to GitHub Workflows.
5173
- Improves build to test Python 3.6 and 3.9.
5274
- Adds a changelog.

LICENSE

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
MIT License
22

3-
Copyright (c) 2019 Roberto Prevato
3+
Copyright (c) 2019-present Roberto Prevato [email protected]
44

55
Permission is hereby granted, free of charge, to any person obtaining a copy
66
of this software and associated documentation files (the "Software"), to deal

guardpost/__about__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "1.0.2"
1+
__version__ = "1.0.3"

guardpost/authentication.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ def __init__(
2828
def sub(self) -> Optional[str]:
2929
return self.get("sub")
3030

31+
@property
32+
def roles(self) -> Optional[str]:
33+
return self.get("roles")
34+
3135
def is_authenticated(self) -> bool:
3236
return bool(self.authentication_mode)
3337

@@ -43,6 +47,11 @@ def has_claim(self, name: str) -> bool:
4347
def has_claim_value(self, name: str, value: str) -> bool:
4448
return self.claims.get(name) == value
4549

50+
def has_role(self, name: str) -> bool:
51+
if not self.roles:
52+
return False
53+
return name in self.roles
54+
4655

4756
class User(Identity):
4857
@property

guardpost/authorization.py

Lines changed: 73 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,29 @@ async def handle(self, context: "AuthorizationContext"):
3333
"""Handles this requirement for a given context."""
3434

3535

36+
class RolesRequirement(Requirement):
37+
"""
38+
Requires an identity with certain roles.
39+
Supports defining sufficient roles (any one is enough).
40+
"""
41+
42+
__slots__ = ("_roles",)
43+
44+
def __init__(self, roles: Optional[Sequence[str]] = None):
45+
self._roles = list(roles) if roles else None
46+
47+
def handle(self, context: "AuthorizationContext"):
48+
identity = context.identity
49+
50+
if not identity:
51+
context.fail("Missing identity")
52+
return
53+
54+
if self._roles:
55+
if any(identity.has_role(name) for name in self._roles):
56+
context.succeed(self)
57+
58+
3659
RequirementConfType = Union[Requirement, Type[Requirement]]
3760

3861

@@ -208,46 +231,78 @@ def with_default_policy(self, policy: Policy) -> "AuthorizationStrategy":
208231
return self
209232

210233
async def authorize(
211-
self, policy_name: Optional[str], identity: Identity, scope: Any = None
234+
self,
235+
policy_name: Optional[str],
236+
identity: Identity,
237+
scope: Any = None,
238+
roles: Optional[Sequence[str]] = None,
212239
):
213240
if policy_name:
214241
policy = self.get_policy(policy_name)
215242

216243
if not policy:
217244
raise PolicyNotFoundError(policy_name)
218245

219-
await self._handle_with_policy(policy, identity, scope)
246+
await self._handle_with_policy(policy, identity, scope, roles)
220247
else:
221248
if self.default_policy:
222-
await self._handle_with_policy(self.default_policy, identity, scope)
249+
await self._handle_with_policy(
250+
self.default_policy, identity, scope, roles
251+
)
252+
return
253+
254+
if roles:
255+
# This code is only executed if the user specified roles without
256+
# specifying an authorization policy.
257+
await self._handle_with_roles(identity, roles)
223258
return
224259

225260
if not identity:
226261
raise UnauthorizedError("Missing identity", [])
227262
if not identity.is_authenticated():
228263
raise UnauthorizedError("The resource requires authentication", [])
229264

230-
def _get_requirements(self, policy: Policy, scope: Any) -> Iterable[Requirement]:
265+
def _get_requirements(
266+
self, policy: Policy, scope: Any, roles: Optional[Sequence[str]] = None
267+
) -> Iterable[Requirement]:
268+
if roles:
269+
yield RolesRequirement(roles=roles)
231270
yield from self._get_instances(policy.requirements, scope)
232271

233-
async def _handle_with_policy(self, policy: Policy, identity: Identity, scope: Any):
272+
async def _handle_with_policy(
273+
self,
274+
policy: Policy,
275+
identity: Identity,
276+
scope: Any,
277+
roles: Optional[Sequence[str]] = None,
278+
):
234279
with AuthorizationContext(
235-
identity, list(self._get_requirements(policy, scope))
280+
identity, list(self._get_requirements(policy, scope, roles))
236281
) as context:
237-
for requirement in context.requirements:
238-
if _is_async_handler(type(requirement)): # type: ignore
239-
await requirement.handle(context)
240-
else:
241-
requirement.handle(context) # type: ignore
242-
243-
if not context.has_succeeded:
244-
if identity and identity.is_authenticated():
245-
raise ForbiddenError(
246-
context.forced_failure, context.pending_requirements
247-
)
248-
raise UnauthorizedError(
282+
await self._handle_context(identity, context)
283+
284+
async def _handle_with_roles(
285+
self, identity: Identity, roles: Optional[Sequence[str]] = None
286+
):
287+
# This method is to be used only when the user specified roles without a policy
288+
with AuthorizationContext(identity, [RolesRequirement(roles=roles)]) as context:
289+
await self._handle_context(identity, context)
290+
291+
async def _handle_context(self, identity: Identity, context: AuthorizationContext):
292+
for requirement in context.requirements:
293+
if _is_async_handler(type(requirement)): # type: ignore
294+
await requirement.handle(context)
295+
else:
296+
requirement.handle(context) # type: ignore
297+
298+
if not context.has_succeeded:
299+
if identity and identity.is_authenticated():
300+
raise ForbiddenError(
249301
context.forced_failure, context.pending_requirements
250302
)
303+
raise UnauthorizedError(
304+
context.forced_failure, context.pending_requirements
305+
)
251306

252307
async def _handle_with_identity_getter(
253308
self, policy_name: Optional[str], *args, **kwargs

0 commit comments

Comments
 (0)