Skip to content

Commit 626d49e

Browse files
authored
Merge pull request #1015 from tisnik/lcore-1142-final-docstrings-improvements
LCORE-1142: Final docstrings improvements
2 parents 3303303 + 26f5dd8 commit 626d49e

3 files changed

Lines changed: 334 additions & 17 deletions

File tree

src/authorization/middleware.py

Lines changed: 61 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,21 @@
2828

2929
@lru_cache(maxsize=1)
3030
def get_authorization_resolvers() -> Tuple[RolesResolver, AccessResolver]:
31-
"""Get authorization resolvers from configuration (cached)."""
31+
"""Get authorization resolvers from configuration (cached).
32+
33+
Return the configured RolesResolver and AccessResolver based on
34+
authentication and authorization settings.
35+
36+
The selection mirrors configuration: returns noop resolvers for
37+
NOOP/K8S/NOOP_WITH_TOKEN or when JWT role rules or authorization access
38+
rules are not set; returns JwtRolesResolver and GenericAccessResolver when
39+
JWK_TOKEN configuration provides role and access rules. The result is
40+
cached to avoid recomputing resolvers.
41+
42+
Returns:
43+
tuple[RolesResolver, AccessResolver]: (roles_resolver, access_resolver)
44+
appropriate for the current configuration.
45+
"""
3246
authorization_cfg = configuration.authorization_configuration
3347
authentication_config = configuration.authentication_configuration
3448

@@ -83,7 +97,29 @@ def get_authorization_resolvers() -> Tuple[RolesResolver, AccessResolver]:
8397
async def _perform_authorization_check(
8498
action: Action, args: tuple[Any, ...], kwargs: dict[str, Any]
8599
) -> None:
86-
"""Perform authorization check - common logic for all decorators."""
100+
"""Perform authorization check - common logic for all decorators.
101+
102+
Performs role resolution and access verification for the supplied `action`
103+
using configured resolvers. Expects `kwargs` to contain an `auth` value
104+
from the authentication dependency; if a Request is present in `args` or
105+
`kwargs` its `state.authorized_actions` will be set to the set of actions
106+
the resolved roles are authorized to perform.
107+
108+
Parameters:
109+
action (Action): The action to authorize.
110+
args (tuple[Any, ...]): Positional arguments passed to the endpoint;
111+
used to locate a Request instance if present.
112+
kwargs (dict[str, Any]): Keyword arguments passed to the endpoint; must
113+
include `auth` (authentication info) and may include `request`.
114+
115+
Returns:
116+
none
117+
118+
Raises:
119+
HTTPException: with 500 Internal Server Error if `auth` is missing from `kwargs`.
120+
HTTPException: with 403 Forbidden if the resolved roles are not
121+
permitted to perform `action`.
122+
"""
87123
role_resolver, access_resolver = get_authorization_resolvers()
88124

89125
try:
@@ -120,9 +156,31 @@ async def _perform_authorization_check(
120156

121157

122158
def authorize(action: Action) -> Callable:
123-
"""Check authorization for an endpoint (async version)."""
159+
"""Check authorization for an endpoint (async version).
160+
161+
Create a decorator that enforces the specified authorization action on an endpoint.
162+
163+
Parameters:
164+
action (Action): The action that the decorated endpoint must be
165+
authorized to perform.
166+
167+
Returns:
168+
Callable: A decorator which, when applied to an endpoint function,
169+
performs the authorization check for the given action before invoking
170+
the function.
171+
"""
124172

125173
def decorator(func: Callable) -> Callable:
174+
"""
175+
Wrap an endpoint function to perform an authorization check before invoking original one.
176+
177+
Parameters:
178+
func (Callable): The function to wrap.
179+
180+
Returns:
181+
Callable: A wrapper that performs authorization then calls `func`.
182+
"""
183+
126184
@wraps(func)
127185
async def wrapper(*args: Any, **kwargs: Any) -> Any:
128186
await _perform_authorization_check(action, args, kwargs)

src/authorization/resolvers.py

Lines changed: 169 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,32 @@ class RolesResolver(ABC): # pylint: disable=too-few-public-methods
2727

2828
@abstractmethod
2929
async def resolve_roles(self, auth: AuthTuple) -> UserRoles:
30-
"""Given an auth tuple, return the list of user roles."""
30+
"""Given an auth tuple, return the list of user roles.
31+
32+
Resolve and return the set of user roles extracted from the provided authentication tuple.
33+
34+
Parameters:
35+
auth (AuthTuple): Authentication tuple (for example, a token and
36+
associated metadata) used to determine roles.
37+
38+
Returns:
39+
UserRoles: A set of role names associated with the authenticated subject.
40+
"""
3141

3242

3343
class NoopRolesResolver(RolesResolver): # pylint: disable=too-few-public-methods
3444
"""No-op roles resolver that does not perform any role resolution."""
3545

3646
async def resolve_roles(self, auth: AuthTuple) -> UserRoles:
37-
"""Return an empty list of roles."""
47+
"""Return an empty list of roles.
48+
49+
Produce an empty set of user roles; no role resolution is performed.
50+
51+
The provided `auth` tuple is accepted but ignored.
52+
53+
Returns:
54+
An empty set of role names.
55+
"""
3856
_ = auth # Unused
3957
return set()
4058

@@ -44,6 +62,9 @@ def unsafe_get_claims(token: str) -> dict[str, Any]:
4462
4563
A somewhat hacky way to get JWT claims without verifying the signature.
4664
We assume verification has already been done during authentication.
65+
66+
Returns:
67+
dict[str, Any]: Claims dictionary parsed from the JWT payload.
4768
"""
4869
payload = token.split(".")[1]
4970
padded = payload + "=" * (-len(payload) % 4)
@@ -54,11 +75,28 @@ class JwtRolesResolver(RolesResolver): # pylint: disable=too-few-public-methods
5475
"""Processes JWT claims with the given JSONPath rules to get roles."""
5576

5677
def __init__(self, role_rules: list[JwtRoleRule]):
57-
"""Initialize the resolver with rules."""
78+
"""Initialize the resolver with rules.
79+
80+
Create a JwtRolesResolver configured with JWT-to-role extraction rules.
81+
82+
Parameters:
83+
role_rules (list[JwtRoleRule]): Ordered list of rules that map JWT
84+
claim matches to roles. Each rule specifies a JSONPath to evaluate,
85+
an operator to apply to matches, the roles to grant when the rule
86+
matches, and optional negation or regex matching.
87+
"""
5888
self.role_rules = role_rules
5989

6090
async def resolve_roles(self, auth: AuthTuple) -> UserRoles:
61-
"""Extract roles from JWT claims using configured rules."""
91+
"""Extract roles from JWT claims using configured rules.
92+
93+
Determine user roles by evaluating configured JwtRoleRule objects
94+
against JWT claims extracted from the provided AuthTuple.
95+
96+
Returns:
97+
roles (UserRoles): Set of role names derived from all configured
98+
rules that match the token's claims.
99+
"""
62100
jwt_claims = self._get_claims(auth)
63101
return {
64102
role
@@ -68,7 +106,20 @@ async def resolve_roles(self, auth: AuthTuple) -> UserRoles:
68106

69107
@staticmethod
70108
def evaluate_role_rules(rule: JwtRoleRule, jwt_claims: dict[str, Any]) -> UserRoles:
71-
"""Get roles from a JWT role rule if it matches the claims."""
109+
"""Get roles from a JWT role rule if it matches the claims.
110+
111+
Determine which roles from a JwtRoleRule apply to the provided JWT claims.
112+
113+
Parameters:
114+
rule (JwtRoleRule): Rule containing a JSONPath expression,
115+
operator, and associated roles to grant when matched.
116+
jwt_claims (dict[str, Any]): Decoded JWT claims to evaluate against
117+
the rule's JSONPath.
118+
119+
Returns:
120+
roles (set[str]): The set of roles from `rule.roles` if the rule
121+
matches `jwt_claims`, otherwise an empty set.
122+
"""
72123
return (
73124
set(rule.roles)
74125
if JwtRolesResolver._evaluate_operator(
@@ -80,7 +131,18 @@ def evaluate_role_rules(rule: JwtRoleRule, jwt_claims: dict[str, Any]) -> UserRo
80131

81132
@staticmethod
82133
def _get_claims(auth: AuthTuple) -> dict[str, Any]:
83-
"""Get the JWT claims from the auth tuple."""
134+
"""Get the JWT claims from the auth tuple.
135+
136+
Extract JWT claims from an AuthTuple.
137+
138+
Parameters:
139+
auth (AuthTuple): Authentication tuple where the fourth element is the JWT token.
140+
141+
Returns:
142+
dict[str, Any]: Decoded JWT claims as a dictionary. Returns an
143+
empty dict when the token equals constants.NO_USER_TOKEN (guest).
144+
The token payload is decoded without validating the JWT signature.
145+
"""
84146
_, _, _, token = auth
85147
if token == constants.NO_USER_TOKEN:
86148
# No claims for guests
@@ -93,7 +155,28 @@ def _get_claims(auth: AuthTuple) -> dict[str, Any]:
93155
def _evaluate_operator(
94156
rule: JwtRoleRule, match: Any
95157
) -> bool: # pylint: disable=too-many-branches
96-
"""Evaluate an operator against a match and rule."""
158+
"""Evaluate an operator against a match and rule.
159+
160+
Determine whether a single JSONPath rule condition matches the provided value.
161+
162+
Evaluates the rule's operator against the given match and applies rule.negate if set.
163+
Supported operators:
164+
- EQUALS: match equals rule.value.
165+
- CONTAINS: rule.value is contained in match.
166+
- IN: match is contained in rule.value.
167+
- MATCH: any string item in match matches rule.compiled_regex (if
168+
compiled_regex is provided).
169+
170+
Parameters:
171+
rule (JwtRoleRule): The role rule containing operator, value,
172+
negate flag, and optionally compiled_regex.
173+
match (Any): The value(s) produced by evaluating the JSONPath; may
174+
be a single value or an iterable of values for MATCH.
175+
176+
Returns:
177+
bool: `true` if the operator evaluation (after applying negation
178+
when set) succeeds, `false` otherwise.
179+
"""
97180
result = False
98181
match rule.operator:
99182
case JsonPathOperator.EQUALS:
@@ -121,24 +204,62 @@ class AccessResolver(ABC): # pylint: disable=too-few-public-methods
121204

122205
@abstractmethod
123206
def check_access(self, action: Action, user_roles: UserRoles) -> bool:
124-
"""Check if the user has access to the specified action based on their roles."""
207+
"""Check if the user has access to the specified action based on their roles.
208+
209+
Determine whether any of the given user roles permit performing the specified action.
210+
211+
Parameters:
212+
action (Action): The action to authorize.
213+
user_roles (UserRoles): Set of role names assigned to the user.
214+
215+
Returns:
216+
bool: `true` if at least one role in `user_roles` grants the
217+
requested `action`, `false` otherwise.
218+
"""
125219

126220
@abstractmethod
127221
def get_actions(self, user_roles: UserRoles) -> set[Action]:
128-
"""Get the actions that the user can perform based on their roles."""
222+
"""Get the actions that the user can perform based on their roles.
223+
224+
Compute the set of actions permitted for the provided user roles.
225+
226+
Parameters:
227+
user_roles (UserRoles): Set of role names to evaluate.
228+
229+
Returns:
230+
set[Action]: The aggregated set of allowed actions for the given
231+
roles. If `ADMIN` is included in the aggregated actions, returns
232+
all available non-`ADMIN` actions.
233+
"""
129234

130235

131236
class NoopAccessResolver(AccessResolver): # pylint: disable=too-few-public-methods
132237
"""No-op access resolver that does not perform any access checks."""
133238

134239
def check_access(self, action: Action, user_roles: UserRoles) -> bool:
135-
"""Return True always, indicating access is granted."""
240+
"""Return True always, indicating access is granted.
241+
242+
Grant all access unconditionally.
243+
244+
Parameters:
245+
action (Action): Ignored.
246+
user_roles (UserRoles): Ignored.
247+
248+
Returns:
249+
`true` always (access is always granted).
250+
"""
136251
_ = action # We're noop, it doesn't matter, everyone is allowed
137252
_ = user_roles # We're noop, it doesn't matter, everyone is allowed
138253
return True
139254

140255
def get_actions(self, user_roles: UserRoles) -> set[Action]:
141-
"""Return an empty set of actions, indicating no specific actions are allowed."""
256+
"""Return an empty set of actions, indicating no specific actions are allowed.
257+
258+
Determine the set of actions permitted for any user under the noop access resolver.
259+
260+
Returns:
261+
allowed_actions (set[Action]): All defined `Action` values except `Action.ADMIN`.
262+
"""
142263
_ = user_roles # We're noop, it doesn't matter, everyone is allowed
143264
return set(Action) - {Action.ADMIN}
144265

@@ -151,7 +272,19 @@ class GenericAccessResolver(AccessResolver): # pylint: disable=too-few-public-m
151272
"""
152273

153274
def __init__(self, access_rules: list[AccessRule]):
154-
"""Initialize the access resolver with access rules."""
275+
"""Initialize the access resolver with access rules.
276+
277+
Create a GenericAccessResolver and build an internal mapping of roles to allowed actions.
278+
279+
Parameters:
280+
access_rules (list[AccessRule]): List of access rules used to
281+
populate the resolver. Each rule's `role` is mapped to the union of
282+
its `actions`.
283+
284+
Raises:
285+
ValueError: If any rule contains the `Action.ADMIN` action together
286+
with other actions.
287+
"""
155288
for rule in access_rules:
156289
# Since this is nonsensical, it might be a mistake, so hard fail
157290
if Action.ADMIN in rule.actions and len(rule.actions) > 1:
@@ -169,7 +302,21 @@ def __init__(self, access_rules: list[AccessRule]):
169302
self._access_lookup[rule.role].update(rule.actions)
170303

171304
def check_access(self, action: Action, user_roles: UserRoles) -> bool:
172-
"""Check if the user has access to the specified action based on their roles."""
305+
"""Check if the user has access to the specified action based on their roles.
306+
307+
Determine whether the provided roles permit performing the specified action.
308+
309+
If any role grants the ADMIN action, that role permits all non-ADMIN
310+
actions (ADMIN acts as a full override).
311+
312+
Parameters:
313+
action (Action): The action to check.
314+
user_roles (UserRoles): The set of roles assigned to the user.
315+
316+
Returns:
317+
true if at least one role permits the action or ADMIN override
318+
applies, false otherwise.
319+
"""
173320
if action != Action.ADMIN and self.check_access(Action.ADMIN, user_roles):
174321
# Recurse to check if the roles allow the user to perform the admin action,
175322
# if they do, then we allow any action
@@ -188,7 +335,15 @@ def check_access(self, action: Action, user_roles: UserRoles) -> bool:
188335
return False
189336

190337
def get_actions(self, user_roles: UserRoles) -> set[Action]:
191-
"""Get the actions that the user can perform based on their roles."""
338+
"""Get the actions that the user can perform based on their roles.
339+
340+
Determine which actions are permitted for the given user roles.
341+
342+
Returns:
343+
allowed_actions (set[Action]): Set of actions the user may perform.
344+
If any role grants Action.ADMIN, returns every Action except
345+
Action.ADMIN.
346+
"""
192347
actions = {
193348
action
194349
for role in user_roles

0 commit comments

Comments
 (0)