Skip to content

Commit 7e452cd

Browse files
authored
Merge pull request lightspeed-core#1011 from tisnik/lcore-1142-docstrings-for-authentication
LCORE-1142: docstrings for authentication module
2 parents ab07691 + b769bee commit 7e452cd

7 files changed

Lines changed: 180 additions & 26 deletions

File tree

src/authentication/__init__.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,19 @@
2121
def get_auth_dependency(
2222
virtual_path: str = constants.DEFAULT_VIRTUAL_PATH,
2323
) -> AuthInterface:
24-
"""Select the configured authentication dependency interface."""
24+
"""Select the configured authentication dependency interface.
25+
26+
Parameters:
27+
virtual_path (str): Virtual path passed to the authentication
28+
dependency when it is constructed.
29+
30+
Returns:
31+
AuthInterface: An instance implementing AuthInterface for the
32+
configured authentication module.
33+
34+
Raises:
35+
ValueError: If the configured authentication module is not supported.
36+
"""
2537
try:
2638
module = configuration.authentication_configuration.module
2739
except LogicError:

src/authentication/interface.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,4 +36,13 @@ class AuthInterface(ABC): # pylint: disable=too-few-public-methods
3636

3737
@abstractmethod
3838
async def __call__(self, request: Request) -> AuthTuple:
39-
"""Validate FastAPI Requests for authentication and authorization."""
39+
"""Validate FastAPI Requests for authentication and authorization.
40+
41+
Returns:
42+
AuthTuple: A 4-tuple (user_id, user_name, skip_user_id_check, token) where
43+
user_id (str): authenticated user's unique identifier,
44+
user_name (str): authenticated user's display name,
45+
skip_user_id_check (bool): whether downstream handlers should
46+
skip user-id verification,
47+
token (str): authentication token or NO_USER_TOKEN when no token is present.
48+
"""

src/authentication/jwk_token.py

Lines changed: 62 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,13 @@
3535

3636

3737
async def get_jwk_set(url: str) -> KeySet:
38-
"""Fetch the JWK set from the cache, or fetch it from the URL if not cached."""
38+
"""Fetch the JWK set from the cache, or fetch it from the URL if not cached.
39+
40+
Retrieve a JWK KeySet from the in-memory cache or fetch and cache it from the provided URL.
41+
42+
Returns:
43+
KeySet: The JWK `KeySet` corresponding to the URL.
44+
"""
3945
async with _jwk_cache_lock:
4046
if url not in _jwk_cache:
4147
async with aiohttp.ClientSession() as session:
@@ -54,10 +60,21 @@ def key_resolver_func(
5460
jwk_set: KeySet,
5561
) -> Callable[[dict[str, Any], dict[str, Any]], Key]:
5662
"""
57-
Create a key resolver function.
63+
Create a JWK key resolver used to locate the verification key from a KeySet.
5864
59-
Return a function to find a key in the given jwk_set. The function matches the
60-
signature expected by the jwt.decode key kwarg.
65+
The returned callable accepts a JWT header and payload and selects a key from the provided
66+
jwk_set. If the header contains a `kid`, the resolver returns the single key with that `kid`
67+
and verifies the key's `alg` matches the header `alg`. If `kid` is absent, the resolver
68+
returns the first key that matches the header `alg`. The resolver raises `KeyNotFoundError`
69+
if no suitable key is found or if multiple keys are found for the same `kid`.
70+
71+
Parameters:
72+
jwk_set (KeySet): JWK set to search for verification keys.
73+
74+
Returns:
75+
callable: A function `header, payload -> Key` that resolves and returns
76+
the verification key.
77+
Raises `KeyNotFoundError` when resolution fails.
6178
"""
6279

6380
def _internal(header: dict[str, Any], _payload: dict[str, Any]) -> Key:
@@ -67,8 +84,16 @@ def _internal(header: dict[str, Any], _payload: dict[str, Any]) -> Key:
6784
match the algorithm to make sure the algorithm stated by the user
6885
is the same algorithm the key itself expects.
6986
70-
# We intentionally do not use find_by_kid because it's a bad function
71-
# that doesn't take the alg into account
87+
We intentionally do not use find_by_kid because it's a bad function
88+
that doesn't take the alg into account
89+
90+
Returns:
91+
key (Key): The JWK matching the header's selection criteria.
92+
93+
Raises:
94+
KeyNotFoundError: If the header is missing `alg`, no matching key
95+
is found, multiple keys match a `kid`, or a found key's algorithm
96+
does not match the header's `alg`.
7297
"""
7398
if "alg" not in header:
7499
raise KeyNotFoundError("Token header missing 'alg' field")
@@ -117,13 +142,42 @@ class JwkTokenAuthDependency(AuthInterface): # pylint: disable=too-few-public-m
117142
def __init__(
118143
self, config: JwkConfiguration, virtual_path: str = DEFAULT_VIRTUAL_PATH
119144
) -> None:
120-
"""Initialize the required allowed paths for authorization checks."""
145+
"""Initialize the required allowed paths for authorization checks.
146+
147+
Create a JWK-based token authentication dependency configured for a specific virtual path.
148+
149+
Parameters:
150+
config (JwkConfiguration): Configuration containing the JWK URL,
151+
claim names, and validation settings.
152+
virtual_path (str): Virtual authorization scope path used when
153+
resolving authorization rules; defaults to DEFAULT_VIRTUAL_PATH.
154+
155+
Notes:
156+
Initializes the instance and sets `skip_userid_check` to False.
157+
"""
121158
self.virtual_path: str = virtual_path
122159
self.config: JwkConfiguration = config
123160
self.skip_userid_check = False
124161

125162
async def __call__(self, request: Request) -> AuthTuple:
126-
"""Authenticate the JWT in the headers against the keys from the JWK url."""
163+
"""Authenticate the JWT in the headers against the keys from the JWK url.
164+
165+
If the Authorization header is missing, returns NO_AUTH_TUPLE. On token
166+
verification or validation failures this function raises HTTPException
167+
with appropriate HTTP status codes:
168+
- 401 for unknown signing key/algorithm, bad signature, expired token,
169+
or missing required claims;
170+
- 400 for token decode or other JOSE-related decode/validation errors;
171+
- 500 for unexpected internal errors.
172+
173+
Parameters:
174+
request (Request): The incoming FastAPI request containing the Authorization header.
175+
176+
Returns:
177+
AuthTuple: A tuple (user_id, username, skip_userid_check, token)
178+
extracted from the validated token, or NO_AUTH_TUPLE when no
179+
Authorization header is present.
180+
"""
127181
if not request.headers.get("Authorization"):
128182
return NO_AUTH_TUPLE
129183

src/authentication/k8s.py

Lines changed: 58 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,9 @@ def __new__(cls: type[Self]) -> Self:
5252
5353
This method initializes the Kubernetes API clients the first time it is called.
5454
and ensures that subsequent calls return the same instance.
55+
56+
Returns:
57+
instance (K8sClientSingleton): The singleton instance.
5558
"""
5659
if cls._instance is None:
5760
cls._instance = super().__new__(cls)
@@ -105,6 +108,9 @@ def get_authn_api(cls) -> kubernetes.client.AuthenticationV1Api:
105108
"""Return the Authentication API client instance.
106109
107110
Ensures the singleton is initialized before returning the Authentication API client.
111+
112+
Returns:
113+
kubernetes.client.AuthenticationV1Api: The initialized AuthenticationV1Api instance.
108114
"""
109115
if cls._instance is None or cls._authn_api is None:
110116
cls()
@@ -115,6 +121,10 @@ def get_authz_api(cls) -> kubernetes.client.AuthorizationV1Api:
115121
"""Return the Authorization API client instance.
116122
117123
Ensures the singleton is initialized before returning the Authorization API client.
124+
125+
Returns:
126+
kubernetes.client.AuthorizationV1Api: The Kubernetes Authorization
127+
API client instance.
118128
"""
119129
if cls._instance is None or cls._authz_api is None:
120130
cls()
@@ -125,13 +135,30 @@ def get_custom_objects_api(cls) -> kubernetes.client.CustomObjectsApi:
125135
"""Return the custom objects API instance.
126136
127137
Ensures the singleton is initialized before returning the Authorization API client.
138+
139+
Returns:
140+
kubernetes.client.CustomObjectsApi: The CustomObjectsApi client used by the singleton.
128141
"""
129142
if cls._instance is None or cls._custom_objects_api is None:
130143
cls()
131144
return cls._custom_objects_api
132145

133146
@classmethod
134147
def _get_cluster_id(cls) -> str:
148+
"""
149+
Retrieve the OpenShift cluster ID from the ClusterVersion custom object and cache it.
150+
151+
Fetches the "version" ClusterVersion from group
152+
`config.openshift.io/v1`, extracts `spec.clusterID`, assigns it to
153+
`cls._cluster_id`, and returns it.
154+
155+
Returns:
156+
str: The cluster's `clusterID`.
157+
158+
Raises:
159+
ClusterIDUnavailableError: If the cluster ID cannot be obtained due
160+
to missing keys, an API error, or any unexpected error.
161+
"""
135162
try:
136163
custom_objects_api = cls.get_custom_objects_api()
137164
version_data = custom_objects_api.get_cluster_custom_object(
@@ -159,7 +186,21 @@ def _get_cluster_id(cls) -> str:
159186

160187
@classmethod
161188
def get_cluster_id(cls) -> str:
162-
"""Return the cluster ID."""
189+
"""Return the cluster ID.
190+
191+
Get the cached Kubernetes cluster identifier, initializing the
192+
singleton and fetching the ID when necessary.
193+
194+
If running outside a cluster, sets and returns the sentinel value
195+
"local". When running inside a cluster, attempts to fetch and cache the
196+
cluster ID via the private retrieval method.
197+
198+
Returns:
199+
str: The cluster identifier.
200+
201+
Raises:
202+
ClusterIDUnavailableError: If running in-cluster and fetching the cluster ID fails.
203+
"""
163204
if cls._instance is None:
164205
cls()
165206
if cls._cluster_id is None:
@@ -174,7 +215,7 @@ def get_cluster_id(cls) -> str:
174215
def get_user_info(token: str) -> Optional[kubernetes.client.V1TokenReview]:
175216
"""Perform a Kubernetes TokenReview to validate a given token.
176217
177-
Args:
218+
Parameters:
178219
token: The bearer token to be validated.
179220
180221
Returns:
@@ -221,14 +262,27 @@ class K8SAuthDependency(AuthInterface): # pylint: disable=too-few-public-method
221262
"""
222263

223264
def __init__(self, virtual_path: str = DEFAULT_VIRTUAL_PATH) -> None:
224-
"""Initialize the required allowed paths for authorization checks."""
265+
"""Initialize the required allowed paths for authorization checks.
266+
267+
Create a K8SAuthDependency configured for performing
268+
SubjectAccessReview checks on a specific virtual path.
269+
270+
Parameters:
271+
virtual_path (str): The request path used in SubjectAccessReview
272+
non-resource attributes; defaults to DEFAULT_VIRTUAL_PATH.
273+
274+
Attributes set:
275+
virtual_path: Stored `virtual_path` value.
276+
skip_userid_check (bool): Flag indicating whether user ID checks
277+
should be skipped; initialized to False.
278+
"""
225279
self.virtual_path = virtual_path
226280
self.skip_userid_check = False
227281

228282
async def __call__(self, request: Request) -> tuple[str, str, bool, str]:
229283
"""Validate FastAPI Requests for authentication and authorization.
230284
231-
Args:
285+
Parameters:
232286
request: The FastAPI request object.
233287
234288
Returns:

src/authentication/noop.py

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,19 +18,31 @@ class NoopAuthDependency(AuthInterface): # pylint: disable=too-few-public-metho
1818
"""No-op AuthDependency class that bypasses authentication and authorization checks."""
1919

2020
def __init__(self, virtual_path: str = DEFAULT_VIRTUAL_PATH) -> None:
21-
"""Initialize the required allowed paths for authorization checks."""
21+
"""Initialize the required allowed paths for authorization checks.
22+
23+
Create a NoopAuthDependency configured with a virtual path and with
24+
user-ID checking disabled.
25+
26+
Parameters:
27+
virtual_path (str): Virtual path context used by the dependency
28+
(defaults to DEFAULT_VIRTUAL_PATH).
29+
"""
2230
self.virtual_path = virtual_path
2331
self.skip_userid_check = True
2432

2533
async def __call__(self, request: Request) -> tuple[str, str, bool, str]:
2634
"""Validate FastAPI Requests for authentication and authorization.
2735
28-
Args:
29-
request: The FastAPI request object.
36+
Parameters:
37+
request (Request): FastAPI request whose query parameters may contain "user_id".
3038
3139
Returns:
32-
The user's UID and username if authentication and authorization succeed
33-
user_id check is skipped with noop auth to allow consumers provide user_id
40+
tuple[str, str, bool, str]: A 4-tuple containing:
41+
- user_id: the value of the "user_id" query parameter if
42+
present, otherwise DEFAULT_USER_UID.
43+
- username: DEFAULT_USER_NAME.
44+
- skip_userid_check: True to indicate the user ID check is skipped.
45+
- token: NO_USER_TOKEN.
3446
"""
3547
logger.warning(
3648
"No-op authentication dependency is being used. "

src/authentication/noop_with_token.py

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,19 +29,31 @@ class NoopWithTokenAuthDependency(
2929
"""No-op AuthDependency class that bypasses authentication and authorization checks."""
3030

3131
def __init__(self, virtual_path: str = DEFAULT_VIRTUAL_PATH) -> None:
32-
"""Initialize the required allowed paths for authorization checks."""
32+
"""Initialize the required allowed paths for authorization checks.
33+
34+
Parameters:
35+
virtual_path (str): Virtual base path used for authorization
36+
context; defaults to DEFAULT_VIRTUAL_PATH.
37+
38+
Notes:
39+
Sets the instance attribute `virtual_path` and sets `skip_userid_check` to True.
40+
"""
3341
self.virtual_path = virtual_path
3442
self.skip_userid_check = True
3543

3644
async def __call__(self, request: Request) -> tuple[str, str, bool, str]:
3745
"""Validate FastAPI Requests for authentication and authorization.
3846
39-
Args:
47+
Parameters:
4048
request: The FastAPI request object.
4149
4250
Returns:
43-
The user's UID and username if authentication and authorization succeed
44-
user_id check is skipped with noop auth to allow consumers provide user_id
51+
tuple[str, str, bool, str]: A 4-tuple containing:
52+
- user_id: The value of the "user_id" query parameter or
53+
DEFAULT_USER_UID if absent.
54+
- username: DEFAULT_USER_NAME.
55+
- skip_userid_check: True to indicate user-id checks are skipped.
56+
- user_token: Token extracted from the request headers.
4557
"""
4658
logger.warning(
4759
"No-op with token authentication dependency is being used. "

src/authentication/utils.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,12 @@
88
def extract_user_token(headers: Headers) -> str:
99
"""Extract the bearer token from an HTTP Authorization header.
1010
11-
Args:
12-
headers: The request headers containing the Authorization value.
11+
Parameters:
12+
headers (Headers): Incoming request headers from which the
13+
Authorization header will be read.
1314
1415
Returns:
15-
The extracted bearer token.
16+
str: The bearer token string extracted from the header.
1617
1718
Raises:
1819
HTTPException: If the Authorization header is missing or malformed.

0 commit comments

Comments
 (0)