Skip to content

Commit 4faa4b1

Browse files
Add support for roles in authorize calls
1 parent 51a1931 commit 4faa4b1

3 files changed

Lines changed: 69 additions & 31 deletions

File tree

CHANGELOG.md

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,14 @@ 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-02
9-
10-
- Add a `roles` property to the `Identity` object and built-in classes for
11-
role based authorization.
12-
- Add support for validating JWTs signed using symmetric encryption.
8+
## [1.0.3] - 2025-10-03
9+
10+
- Add a `roles` property to the `Identity` object and a `RolesRequirement`
11+
class to require roles.
12+
- Add support for validating JWTs signed using symmetric encryption
13+
(`SymmetricJWTValidator` and `AsymmetricJWTValidator`).
14+
- Add support to call the `authorize` method with an optional set of roles,
15+
treated as sufficient roles to succeed authorization.
1316

1417
## [1.0.2] - 2023-06-16 :corn:
1518

guardpost/authorization.py

Lines changed: 51 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
from guardpost.abc import BaseStrategy
99
from guardpost.authentication import Identity
10+
from guardpost.common import RolesRequirement
1011

1112

1213
class AuthorizationError(Exception):
@@ -208,46 +209,78 @@ def with_default_policy(self, policy: Policy) -> "AuthorizationStrategy":
208209
return self
209210

210211
async def authorize(
211-
self, policy_name: Optional[str], identity: Identity, scope: Any = None
212+
self,
213+
policy_name: Optional[str],
214+
identity: Identity,
215+
scope: Any = None,
216+
roles: Optional[Sequence[str]] = None,
212217
):
213218
if policy_name:
214219
policy = self.get_policy(policy_name)
215220

216221
if not policy:
217222
raise PolicyNotFoundError(policy_name)
218223

219-
await self._handle_with_policy(policy, identity, scope)
224+
await self._handle_with_policy(policy, identity, scope, roles)
220225
else:
221226
if self.default_policy:
222-
await self._handle_with_policy(self.default_policy, identity, scope)
227+
await self._handle_with_policy(
228+
self.default_policy, identity, scope, roles
229+
)
230+
return
231+
232+
if roles:
233+
# This code is only executed if the user specified roles without
234+
# specifying an authorization policy.
235+
await self._handle_with_roles(identity, roles)
223236
return
224237

225238
if not identity:
226239
raise UnauthorizedError("Missing identity", [])
227240
if not identity.is_authenticated():
228241
raise UnauthorizedError("The resource requires authentication", [])
229242

230-
def _get_requirements(self, policy: Policy, scope: Any) -> Iterable[Requirement]:
243+
def _get_requirements(
244+
self, policy: Policy, scope: Any, roles: Optional[Sequence[str]] = None
245+
) -> Iterable[Requirement]:
246+
if roles:
247+
yield RolesRequirement(roles=roles)
231248
yield from self._get_instances(policy.requirements, scope)
232249

233-
async def _handle_with_policy(self, policy: Policy, identity: Identity, scope: Any):
250+
async def _handle_with_policy(
251+
self,
252+
policy: Policy,
253+
identity: Identity,
254+
scope: Any,
255+
roles: Optional[Sequence[str]] = None,
256+
):
234257
with AuthorizationContext(
235-
identity, list(self._get_requirements(policy, scope))
258+
identity, list(self._get_requirements(policy, scope, roles))
236259
) 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(
260+
await self._handle_context(identity, context)
261+
262+
async def _handle_with_roles(
263+
self, identity: Identity, roles: Optional[Sequence[str]] = None
264+
):
265+
# This method is to be used only when the user specified roles without a policy
266+
with AuthorizationContext(identity, [RolesRequirement(roles=roles)]) as context:
267+
await self._handle_context(identity, context)
268+
269+
async def _handle_context(self, identity: Identity, context: AuthorizationContext):
270+
for requirement in context.requirements:
271+
if _is_async_handler(type(requirement)): # type: ignore
272+
await requirement.handle(context)
273+
else:
274+
requirement.handle(context) # type: ignore
275+
276+
if not context.has_succeeded:
277+
if identity and identity.is_authenticated():
278+
raise ForbiddenError(
249279
context.forced_failure, context.pending_requirements
250280
)
281+
raise UnauthorizedError(
282+
context.forced_failure, context.pending_requirements
283+
)
251284

252285
async def _handle_with_identity_getter(
253286
self, policy_name: Optional[str], *args, **kwargs

guardpost/common.py

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -69,17 +69,19 @@ def handle(self, context: AuthorizationContext):
6969
class RolesRequirement(Requirement):
7070
"""
7171
Requires an identity with certain roles.
72+
Supports defining sufficient roles (any one is enough), and required roles (all
73+
must be present).
7274
"""
7375

74-
__slots__ = ("_required_roles", "_sufficient_roles")
76+
__slots__ = ("_roles", "_required_roles")
7577

7678
def __init__(
7779
self,
80+
roles: Optional[Sequence[str]] = None,
7881
required_roles: Optional[Sequence[str]] = None,
79-
sufficient_roles: Optional[Sequence[str]] = None,
8082
):
81-
self._required_roles = list(required_roles or [])
82-
self._sufficient_roles = list(sufficient_roles or [])
83+
self._required_roles = list(required_roles) if required_roles else None
84+
self._roles = list(roles) if roles else None
8385

8486
def handle(self, context: AuthorizationContext):
8587
identity = context.identity
@@ -88,10 +90,10 @@ def handle(self, context: AuthorizationContext):
8890
context.fail("Missing identity")
8991
return
9092

91-
if self._required_roles:
92-
if all(identity.has_role(name) for name in self._required_roles):
93+
if self._roles:
94+
if any(identity.has_role(name) for name in self._roles):
9395
context.succeed(self)
9496

95-
if self._sufficient_roles:
96-
if any(identity.has_role(name) for name in self._required_roles):
97+
if self._required_roles:
98+
if all(identity.has_role(name) for name in self._required_roles):
9799
context.succeed(self)

0 commit comments

Comments
 (0)