Spaces:
Sleeping
Sleeping
Download api/app/security.py from AustinWealth/Data-wiz-project: direct link, hf CLI and curl.
- Browser
- Download file 2.94 kB
-
https://huggingface.co/spaces/AustinWealth/Data-wiz-project/resolve/main/api/app/security.py
- Command line
-
hf download hf://spaces/AustinWealth/Data-wiz-project/api/app/security.py
-
curl -L -o security.py https://huggingface.co/spaces/AustinWealth/Data-wiz-project/resolve/main/api/app/security.py
2.94 kB
| """Admin-key authentication and small in-memory rate limiters (ADR 0004, T-04..T-06).""" | |
| from __future__ import annotations | |
| import hashlib | |
| import hmac | |
| import time | |
| from collections import defaultdict, deque | |
| from fastapi import Header, HTTPException, Request | |
| from . import constants as C | |
| from .config import get_settings | |
| from . import firebase_auth | |
| _fails: dict[str, deque] = defaultdict(deque) | |
| _hits: dict[str, deque] = defaultdict(deque) | |
| def client_ip(request: Request) -> str: | |
| fwd = request.headers.get("x-forwarded-for") | |
| if fwd: | |
| return fwd.split(",")[0].strip() | |
| return request.client.host if request.client else "unknown" | |
| def _prune(q: deque, window: float) -> None: | |
| cutoff = time.monotonic() - window | |
| while q and q[0] < cutoff: | |
| q.popleft() | |
| def reset_limits() -> None: | |
| _fails.clear() | |
| _hits.clear() | |
| def admin_enabled() -> bool: | |
| if get_settings().production: # named, signed-in staff only in production | |
| return False | |
| return len(get_settings().admin_key) >= C.ADMIN_KEY_MIN_LENGTH | |
| def keys_match(supplied: str, expected: str) -> bool: | |
| a = hashlib.sha256(supplied.encode()).digest() | |
| b = hashlib.sha256(expected.encode()).digest() | |
| return hmac.compare_digest(a, b) | |
| class Admin: | |
| def __init__(self, name: str): | |
| self.name = name | |
| def require_admin(request: Request, | |
| x_admin_key: str | None = Header(default=None), | |
| x_admin_name: str | None = Header(default=None), | |
| authorization: str | None = Header(default=None)) -> Admin: | |
| # Preferred: a named staff member signed in with Firebase (email must be verified and on the list). | |
| claims = firebase_auth.claims_from_header(authorization) | |
| if claims: | |
| if not claims["email_verified"]: | |
| raise HTTPException(403, "This account is not staff: its email address is not verified.") | |
| if claims["email"] not in get_settings().staff_emails: | |
| raise HTTPException(403, "This account is not staff: its email address is not on the staff list.") | |
| return Admin(claims["email"]) | |
| if not admin_enabled(): | |
| raise HTTPException(503, "Admin console is disabled: sign in as staff or set ADMIN_KEY (16+ characters).") | |
| ip = client_ip(request) | |
| q = _fails[ip] | |
| _prune(q, C.ADMIN_FAIL_WINDOW_SECONDS) | |
| if len(q) >= C.ADMIN_FAIL_LIMIT: | |
| raise HTTPException(429, "Too many failed attempts. Try again later.") | |
| if not x_admin_key or not keys_match(x_admin_key, get_settings().admin_key): | |
| q.append(time.monotonic()) | |
| raise HTTPException(401, "Invalid admin key.") | |
| name = (x_admin_name or "").strip()[:60] or "admin" | |
| return Admin(name) | |
| def allow_correction(request: Request) -> bool: | |
| ip = client_ip(request) | |
| q = _hits[ip] | |
| _prune(q, 3600) | |
| if len(q) >= C.CORRECTION_RATE_PER_HOUR: | |
| return False | |
| q.append(time.monotonic()) | |
| return True | |