@@ -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
3343class 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
131236class 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