Spaces:
Running
Running
Mark verified Studio identity for privileged routes
Browse files- app/security/middleware.py +2 -88
app/security/middleware.py
CHANGED
|
@@ -76,6 +76,7 @@ class APIKeyAuthenticationMiddleware(BaseHTTPMiddleware):
|
|
| 76 |
logger.warning("rejected Studio identity envelope: %s", str(exc))
|
| 77 |
raise UnauthorizedError from exc
|
| 78 |
principal = await self.api_keys.tenants.resolve_studio_subject(identity.subject)
|
|
|
|
| 79 |
try:
|
| 80 |
# The shared key authenticates transport only; persisted
|
| 81 |
# membership capabilities constrain its effective scopes.
|
|
@@ -215,91 +216,4 @@ class APIKeyAuthenticationMiddleware(BaseHTTPMiddleware):
|
|
| 215 |
context,
|
| 216 |
"social_schedule",
|
| 217 |
limit=self.settings.social_schedule_requests_per_minute,
|
| 218 |
-
|
| 219 |
-
)
|
| 220 |
-
elif path.endswith("/reschedule"):
|
| 221 |
-
await self.rate_limiter.acquire_category(
|
| 222 |
-
context,
|
| 223 |
-
"social_schedule",
|
| 224 |
-
limit=self.settings.social_schedule_requests_per_minute,
|
| 225 |
-
window_seconds=60,
|
| 226 |
-
)
|
| 227 |
-
elif path.endswith("/bulk"):
|
| 228 |
-
await self.rate_limiter.acquire_category(
|
| 229 |
-
context,
|
| 230 |
-
"social_bulk",
|
| 231 |
-
limit=max(1, self.settings.social_schedule_requests_per_minute // 4),
|
| 232 |
-
window_seconds=60,
|
| 233 |
-
)
|
| 234 |
-
|
| 235 |
-
@staticmethod
|
| 236 |
-
def _bearer_token(header: str | None) -> str:
|
| 237 |
-
if not header:
|
| 238 |
-
raise UnauthorizedError
|
| 239 |
-
parts = header.strip().split()
|
| 240 |
-
if len(parts) != 2 or parts[0].casefold() != "bearer" or not parts[1]:
|
| 241 |
-
raise UnauthorizedError
|
| 242 |
-
return parts[1]
|
| 243 |
-
|
| 244 |
-
@staticmethod
|
| 245 |
-
def _content_length(value: str | None) -> int:
|
| 246 |
-
try:
|
| 247 |
-
return max(0, int(value or 0))
|
| 248 |
-
except ValueError:
|
| 249 |
-
return 0
|
| 250 |
-
|
| 251 |
-
@staticmethod
|
| 252 |
-
def _error(
|
| 253 |
-
status_code: int,
|
| 254 |
-
error: str,
|
| 255 |
-
message: str,
|
| 256 |
-
request: Request,
|
| 257 |
-
headers: dict[str, str] | None = None,
|
| 258 |
-
) -> JSONResponse:
|
| 259 |
-
response_headers = dict(headers or {})
|
| 260 |
-
request_id = getattr(request.state, "request_id", None)
|
| 261 |
-
if request_id:
|
| 262 |
-
response_headers["X-Request-ID"] = request_id
|
| 263 |
-
return JSONResponse(
|
| 264 |
-
{"error": error, "message": message},
|
| 265 |
-
status_code=status_code,
|
| 266 |
-
headers=response_headers,
|
| 267 |
-
)
|
| 268 |
-
|
| 269 |
-
def _client_ip(self, request: Request) -> str | None:
|
| 270 |
-
if self.settings.auth_trust_proxy_headers:
|
| 271 |
-
forwarded = request.headers.get("x-forwarded-for")
|
| 272 |
-
if forwarded:
|
| 273 |
-
return forwarded.split(",", 1)[0].strip()[:64]
|
| 274 |
-
return request.client.host[:64] if request.client else None
|
| 275 |
-
|
| 276 |
-
async def _audit_request(
|
| 277 |
-
self,
|
| 278 |
-
request: Request,
|
| 279 |
-
context: AuthContext,
|
| 280 |
-
response_code: int,
|
| 281 |
-
elapsed_ms: int,
|
| 282 |
-
bytes_uploaded: int,
|
| 283 |
-
bytes_downloaded: int,
|
| 284 |
-
) -> None:
|
| 285 |
-
data = {
|
| 286 |
-
"request_id": getattr(request.state, "request_id", "-"),
|
| 287 |
-
"api_key_id": context.api_key_id,
|
| 288 |
-
"key_name": context.key_name,
|
| 289 |
-
"ip_address": self._client_ip(request),
|
| 290 |
-
"user_agent": request.headers.get("user-agent", "")[:512] or None,
|
| 291 |
-
"endpoint": request.url.path,
|
| 292 |
-
"http_method": request.method,
|
| 293 |
-
"response_code": response_code,
|
| 294 |
-
"processing_time_ms": elapsed_ms,
|
| 295 |
-
"bytes_uploaded": bytes_uploaded,
|
| 296 |
-
"bytes_downloaded": bytes_downloaded,
|
| 297 |
-
}
|
| 298 |
-
try:
|
| 299 |
-
await self.audit.record(**data)
|
| 300 |
-
except Exception:
|
| 301 |
-
logger.exception(
|
| 302 |
-
"authentication audit persistence failed",
|
| 303 |
-
extra={"api_key_id": context.api_key_id},
|
| 304 |
-
)
|
| 305 |
-
logger.info("authenticated request", extra=data)
|
|
|
|
| 76 |
logger.warning("rejected Studio identity envelope: %s", str(exc))
|
| 77 |
raise UnauthorizedError from exc
|
| 78 |
principal = await self.api_keys.tenants.resolve_studio_subject(identity.subject)
|
| 79 |
+
request.state.studio_identity_verified = True
|
| 80 |
try:
|
| 81 |
# The shared key authenticates transport only; persisted
|
| 82 |
# membership capabilities constrain its effective scopes.
|
|
|
|
| 216 |
context,
|
| 217 |
"social_schedule",
|
| 218 |
limit=self.settings.social_schedule_requests_per_minute,
|
| 219 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|