security: rate limit admin authentication
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
from datetime import datetime, timedelta, timezone
|
||||
import hashlib
|
||||
import hmac
|
||||
|
||||
from fastapi import HTTPException, Request
|
||||
from sqlalchemy import delete, func, select, text
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from .models import AdminAuthAttempt
|
||||
from .submission_security import RateLimitConfig, client_address
|
||||
|
||||
|
||||
class AdminAuthConfig(RateLimitConfig):
|
||||
admin_token: str
|
||||
admin_auth_attempt_limit: int
|
||||
admin_auth_window_seconds: int
|
||||
|
||||
|
||||
def verify_admin(
|
||||
request: Request,
|
||||
db: Session,
|
||||
authorization: str | None,
|
||||
config: AdminAuthConfig,
|
||||
) -> str:
|
||||
now = datetime.now(timezone.utc)
|
||||
cutoff = now - timedelta(seconds=config.admin_auth_window_seconds)
|
||||
client = client_address(request, config.trusted_proxy_cidrs)
|
||||
client_hash = hmac.new(
|
||||
config.rate_limit_secret.encode(), f"admin:{client}".encode(), hashlib.sha256
|
||||
).hexdigest()
|
||||
if db.get_bind().dialect.name == "postgresql":
|
||||
lock_key = int(client_hash[:16], 16) & 0x7FFF_FFFF_FFFF_FFFF
|
||||
db.execute(text("SELECT pg_advisory_xact_lock(:lock_key)"), {"lock_key": lock_key})
|
||||
db.execute(delete(AdminAuthAttempt).where(AdminAuthAttempt.created_at < now - timedelta(days=1)))
|
||||
failures = db.scalar(
|
||||
select(func.count()).select_from(AdminAuthAttempt).where(
|
||||
AdminAuthAttempt.client_hash == client_hash,
|
||||
AdminAuthAttempt.created_at >= cutoff,
|
||||
)
|
||||
) or 0
|
||||
if failures >= config.admin_auth_attempt_limit:
|
||||
db.commit()
|
||||
raise HTTPException(
|
||||
status_code=429,
|
||||
detail="too many admin authentication attempts",
|
||||
headers={"Retry-After": str(config.admin_auth_window_seconds)},
|
||||
)
|
||||
expected = f"Bearer {config.admin_token}"
|
||||
if not authorization or not hmac.compare_digest(authorization, expected):
|
||||
db.add(AdminAuthAttempt(client_hash=client_hash, created_at=now))
|
||||
db.commit()
|
||||
raise HTTPException(
|
||||
status_code=401,
|
||||
detail="invalid admin token",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
db.execute(delete(AdminAuthAttempt).where(AdminAuthAttempt.client_hash == client_hash))
|
||||
db.commit()
|
||||
return "admin"
|
||||
Reference in New Issue
Block a user