Download api/security.py from HarryDataScience/sentinel-fraud-detection: direct link, hf CLI and curl.
- Browser
- Download file 3.14 kB
-
https://huggingface.co/spaces/HarryDataScience/sentinel-fraud-detection/resolve/main/api/security.py
- Command line
-
hf download hf://spaces/HarryDataScience/sentinel-fraud-detection/api/security.py
-
curl -L -o security.py https://huggingface.co/spaces/HarryDataScience/sentinel-fraud-detection/resolve/main/api/security.py
3.14 kB
| """API-key auth, in-memory sliding-window rate limiting, request-ID logging.""" | |
| import contextvars | |
| import hashlib | |
| import logging | |
| import re | |
| import secrets | |
| import threading | |
| import time | |
| import uuid | |
| from collections import defaultdict, deque | |
| from fastapi import Depends, Header, HTTPException, Request | |
| request_id_var = contextvars.ContextVar("request_id", default="-") | |
| _RID_OK = re.compile(r"^[A-Za-z0-9_\-]{1,64}$") | |
| class RequestIdFilter(logging.Filter): | |
| def filter(self, record): | |
| record.request_id = request_id_var.get() | |
| return True | |
| def configure_logging(level: str): | |
| handler = logging.StreamHandler() | |
| handler.addFilter(RequestIdFilter()) | |
| handler.setFormatter(logging.Formatter("%(asctime)s %(levelname)s [%(request_id)s] %(name)s: %(message)s")) | |
| root = logging.getLogger() | |
| root.handlers = [handler] | |
| root.setLevel(level.upper()) | |
| logging.getLogger("httpx").setLevel(logging.WARNING) | |
| def new_request_id(incoming): | |
| return incoming if incoming and _RID_OK.match(incoming) else uuid.uuid4().hex[:16] | |
| class SlidingWindowLimiter: | |
| """Per-identity limiter. In-memory => per process; use a shared store for multi-replica deployments.""" | |
| def __init__(self, limit: int, window: float = 60.0): | |
| self.limit, self.window = limit, window | |
| self._hits = defaultdict(deque) | |
| self._lock = threading.Lock() | |
| def check(self, identity: str): | |
| """Returns (allowed, retry_after_seconds).""" | |
| if self.limit <= 0: | |
| return True, 0 | |
| now = time.monotonic() | |
| with self._lock: | |
| q = self._hits[identity] | |
| while q and now - q[0] > self.window: | |
| q.popleft() | |
| if len(q) >= self.limit: | |
| return False, max(1, int(self.window - (now - q[0])) + 1) | |
| q.append(now) | |
| if len(self._hits) > 10_000: # drop idle identities | |
| for k in [k for k, v in self._hits.items() if not v or now - v[-1] > self.window]: | |
| self._hits.pop(k, None) | |
| return True, 0 | |
| def _identity(key, request: Request) -> str: | |
| if key: | |
| return "key:" + hashlib.sha256(key.encode()).hexdigest()[:12] | |
| return "ip:" + (request.client.host if request.client else "unknown") | |
| def require_access(request: Request, x_api_key: str = Header(default=None, alias="X-API-Key")): | |
| """Dependency: authenticate (unless AUTH_DISABLED) then rate-limit.""" | |
| settings = request.app.state.settings | |
| key = None | |
| if not settings.auth_disabled: | |
| if not settings.api_keys: | |
| raise HTTPException(503, "API authentication is not configured (set API_KEYS)") | |
| if not x_api_key or not any(secrets.compare_digest(x_api_key, k) for k in settings.api_keys): | |
| raise HTTPException(401, "Invalid or missing API key", headers={"WWW-Authenticate": "ApiKey"}) | |
| key = x_api_key | |
| allowed, retry = request.app.state.limiter.check(_identity(key, request)) | |
| if not allowed: | |
| raise HTTPException(429, "Rate limit exceeded", headers={"Retry-After": str(retry)}) | |
| return key | |
| AccessDep = Depends(require_access) | |