Spaces:
Sleeping
Sleeping
Download app/security.py from appQQQ/FinDataPilot: direct link, hf CLI and curl.
- Browser
- Download file 3.93 kB
-
https://huggingface.co/spaces/appQQQ/FinDataPilot/resolve/main/app/security.py
- Command line
-
hf download hf://spaces/appQQQ/FinDataPilot/app/security.py
-
curl -L -o security.py https://huggingface.co/spaces/appQQQ/FinDataPilot/resolve/main/app/security.py
3.93 kB
| """Authentication and authorization helpers for the HTTP API. | |
| The public web app uses a server-signed anonymous bearer identity. It is a | |
| capability token, not a replacement for an enterprise IdP, but it prevents | |
| different browser visitors from sharing the old ``default`` user namespace. | |
| An OIDC/JWT gateway can later replace ``issue_anonymous_token`` without | |
| changing the API routes. | |
| """ | |
| from __future__ import annotations | |
| import base64 | |
| import hashlib | |
| import hmac | |
| import secrets | |
| import time | |
| from dataclasses import dataclass | |
| from fastapi import Header, HTTPException, Request, status | |
| from app.config import get_settings | |
| class AuthContext: | |
| user_id: str | |
| is_admin: bool = False | |
| def _secret() -> bytes: | |
| return get_settings().effective_auth_secret.encode("utf-8") | |
| def issue_anonymous_token(user_id: str | None = None) -> tuple[str, AuthContext]: | |
| """Create an opaque, signed bearer token for one browser profile.""" | |
| settings = get_settings() | |
| user_id = user_id or f"anon_{secrets.token_urlsafe(18)}" | |
| if not user_id.startswith("anon_"): | |
| raise ValueError("Anonymous user id required") | |
| expires_at = int(time.time()) + settings.auth_token_ttl_seconds | |
| payload = f"v1.{user_id}.{expires_at}" | |
| signature = hmac.new(_secret(), payload.encode("utf-8"), hashlib.sha256).digest() | |
| token = f"{payload}.{base64.urlsafe_b64encode(signature).decode('ascii').rstrip('=')}" | |
| return token, AuthContext(user_id=user_id) | |
| def _decode_token(token: str) -> AuthContext: | |
| try: | |
| version, user_id, expires_raw, signature = token.split(".", 3) | |
| expires_at = int(expires_raw) | |
| except ValueError as exc: | |
| raise HTTPException(status.HTTP_401_UNAUTHORIZED, "Invalid bearer token") from exc | |
| if version != "v1" or not user_id.startswith("anon_") or expires_at < int(time.time()): | |
| raise HTTPException(status.HTTP_401_UNAUTHORIZED, "Expired or invalid bearer token") | |
| payload = f"{version}.{user_id}.{expires_at}".encode() | |
| expected = hmac.new(_secret(), payload, hashlib.sha256).digest() | |
| try: | |
| supplied = base64.urlsafe_b64decode(signature + "=" * (-len(signature) % 4)) | |
| except ValueError as exc: | |
| raise HTTPException(status.HTTP_401_UNAUTHORIZED, "Invalid bearer token") from exc | |
| if not hmac.compare_digest(supplied, expected): | |
| raise HTTPException(status.HTTP_401_UNAUTHORIZED, "Invalid bearer token") | |
| return AuthContext(user_id=user_id) | |
| async def require_user(authorization: str | None = Header(default=None)) -> AuthContext: | |
| if not authorization or not authorization.startswith("Bearer "): | |
| raise HTTPException(status.HTTP_401_UNAUTHORIZED, "Bearer token required") | |
| return _decode_token(authorization.removeprefix("Bearer ").strip()) | |
| def decode_optional_bearer(authorization: str | None) -> AuthContext | None: | |
| """Decode a valid bearer when present; invalid/expired values renew as new.""" | |
| if not authorization or not authorization.startswith("Bearer "): | |
| return None | |
| try: | |
| return _decode_token(authorization.removeprefix("Bearer ").strip()) | |
| except HTTPException: | |
| return None | |
| async def require_admin( | |
| request: Request, | |
| x_api_key: str | None = Header(default=None), | |
| ) -> AuthContext: | |
| """Guard mutable operator endpoints with a distinct server-side key.""" | |
| configured_key = get_settings().operator_api_key | |
| if not configured_key: | |
| raise HTTPException(status.HTTP_503_SERVICE_UNAVAILABLE, "Admin API is not configured") | |
| if not x_api_key or not hmac.compare_digest(x_api_key, configured_key): | |
| raise HTTPException(status.HTTP_403_FORBIDDEN, "Administrator credentials required") | |
| # Read the request so rate-limit decorators can retain a normal request | |
| # argument and so this dependency has the same shape as other guards. | |
| _ = request | |
| return AuthContext(user_id="admin", is_admin=True) | |