|
1 | 1 | """Authorization middleware and decorators.""" |
2 | 2 |
|
| 3 | +import time |
3 | 4 | from collections.abc import Callable |
4 | 5 | from functools import lru_cache, wraps |
5 | 6 | from typing import Any, Optional |
|
18 | 19 | ) |
19 | 20 | from configuration import configuration |
20 | 21 | from log import get_logger |
| 22 | +from metrics import recording |
21 | 23 | from models.api.responses.error import ( |
22 | 24 | ForbiddenResponse, |
23 | 25 | InternalServerErrorResponse, |
|
27 | 29 | logger = get_logger(__name__) |
28 | 30 |
|
29 | 31 |
|
| 32 | +def _record_authorization_metrics( |
| 33 | + action: Action, |
| 34 | + result: str, |
| 35 | + start_time: float, |
| 36 | +) -> None: |
| 37 | + """Record authorization metrics without affecting request authorization flow. |
| 38 | +
|
| 39 | + Args: |
| 40 | + action: Protected action being authorized. |
| 41 | + result: Authorization result label. |
| 42 | + start_time: Monotonic timestamp captured before authorization began. |
| 43 | + """ |
| 44 | + duration = time.monotonic() - start_time |
| 45 | + |
| 46 | + try: |
| 47 | + recording.record_authorization_check(action.value, result) |
| 48 | + except Exception: # pylint: disable=broad-exception-caught |
| 49 | + logger.warning("Failed to record authorization check metric", exc_info=True) |
| 50 | + |
| 51 | + try: |
| 52 | + recording.record_authorization_duration(action.value, result, duration) |
| 53 | + except Exception: # pylint: disable=broad-exception-caught |
| 54 | + logger.warning("Failed to record authorization duration metric", exc_info=True) |
| 55 | + |
| 56 | + |
30 | 57 | @lru_cache(maxsize=1) |
31 | 58 | def get_authorization_resolvers() -> tuple[RolesResolver, AccessResolver]: |
32 | 59 | """Get authorization resolvers from configuration (cached). |
@@ -124,39 +151,47 @@ async def _perform_authorization_check( |
124 | 151 | HTTPException: with 403 Forbidden if the resolved roles are not |
125 | 152 | permitted to perform `action`. |
126 | 153 | """ |
127 | | - role_resolver, access_resolver = get_authorization_resolvers() |
| 154 | + start_time = time.monotonic() |
| 155 | + result = "error" |
128 | 156 |
|
129 | 157 | try: |
130 | | - auth = kwargs["auth"] |
131 | | - except KeyError as exc: |
132 | | - logger.error( |
133 | | - "Authorization only allowed on endpoints that accept " |
134 | | - "'auth: Any = Depends(get_auth_dependency())'" |
135 | | - ) |
136 | | - response = InternalServerErrorResponse.generic() |
137 | | - raise HTTPException(**response.model_dump()) from exc |
138 | | - |
139 | | - # Everyone gets the everyone (aka *) role |
140 | | - everyone_roles = {"*"} |
141 | | - |
142 | | - user_roles = await role_resolver.resolve_roles(auth) | everyone_roles |
143 | | - |
144 | | - if not access_resolver.check_access(action, user_roles): |
145 | | - response = ForbiddenResponse.endpoint(user_id=auth[0]) |
146 | | - raise HTTPException(**response.model_dump()) |
147 | | - |
148 | | - authorized_actions = access_resolver.get_actions(user_roles) |
149 | | - |
150 | | - req: Optional[Request] = None |
151 | | - if "request" in kwargs and isinstance(kwargs["request"], Request): |
152 | | - req = kwargs["request"] |
153 | | - else: |
154 | | - for arg in args: |
155 | | - if isinstance(arg, Request): |
156 | | - req = arg |
157 | | - break |
158 | | - if req is not None: |
159 | | - req.state.authorized_actions = authorized_actions |
| 158 | + role_resolver, access_resolver = get_authorization_resolvers() |
| 159 | + |
| 160 | + try: |
| 161 | + auth = kwargs["auth"] |
| 162 | + except KeyError as exc: |
| 163 | + logger.error( |
| 164 | + "Authorization only allowed on endpoints that accept " |
| 165 | + "'auth: Any = Depends(get_auth_dependency())'" |
| 166 | + ) |
| 167 | + response = InternalServerErrorResponse.generic() |
| 168 | + raise HTTPException(**response.model_dump()) from exc |
| 169 | + |
| 170 | + # Everyone gets the everyone (aka *) role |
| 171 | + everyone_roles = {"*"} |
| 172 | + |
| 173 | + user_roles = await role_resolver.resolve_roles(auth) | everyone_roles |
| 174 | + |
| 175 | + if not access_resolver.check_access(action, user_roles): |
| 176 | + response = ForbiddenResponse.endpoint(user_id=auth[0]) |
| 177 | + result = "denied" |
| 178 | + raise HTTPException(**response.model_dump()) |
| 179 | + |
| 180 | + authorized_actions = access_resolver.get_actions(user_roles) |
| 181 | + |
| 182 | + req: Optional[Request] = None |
| 183 | + if "request" in kwargs and isinstance(kwargs["request"], Request): |
| 184 | + req = kwargs["request"] |
| 185 | + else: |
| 186 | + for arg in args: |
| 187 | + if isinstance(arg, Request): |
| 188 | + req = arg |
| 189 | + break |
| 190 | + if req is not None: |
| 191 | + req.state.authorized_actions = authorized_actions |
| 192 | + result = "success" |
| 193 | + finally: |
| 194 | + _record_authorization_metrics(action, result, start_time) |
160 | 195 |
|
161 | 196 |
|
162 | 197 | def authorize(action: Action) -> Callable: |
|
0 commit comments