Problem
Today's rate-limit middleware (backend/securescan/middleware/rate_limit.py) buckets by raw X-API-Key header value or remote IP. Bogus keys still get their own bucket — a caller can spoof X-API-Key: anything to refresh their rate-limit quota before auth catches the bad key.
Real per-key bucketing requires the rate-limit decision to happen AFTER require_api_key resolves the key to an authenticated Principal. FastAPI middleware runs before route dependencies, so the current ordering can't see the principal.
Acceptance criteria
Suggested approach
- Convert rate-limiting from
BaseHTTPMiddleware to a route-level dependency (Depends(rate_limit_dep)) that runs after require_api_key.
- Read the principal off
request.state.principal (already populated by require_api_key in v0.8.0+).
- Apply the dependency on the small set of routes that need it (today: just
POST /api/v1/scans per the existing scope).
Alternative: keep middleware but have it call require_api_key itself synchronously. Less idiomatic, more coupling.
Difficulty
~3 hr. Architectural reordering + tests. Tracked from v0.7.0 deferral.
Problem
Today's rate-limit middleware (
backend/securescan/middleware/rate_limit.py) buckets by rawX-API-Keyheader value or remote IP. Bogus keys still get their own bucket — a caller can spoofX-API-Key: anythingto refresh their rate-limit quota before auth catches the bad key.Real per-key bucketing requires the rate-limit decision to happen AFTER
require_api_keyresolves the key to an authenticatedPrincipal. FastAPI middleware runs before route dependencies, so the current ordering can't see the principal.Acceptance criteria
key:<key_id>from the resolved Principal, not the raw header.SECURESCAN_RATE_LIMIT_*env vars keep working.Suggested approach
BaseHTTPMiddlewareto a route-level dependency (Depends(rate_limit_dep)) that runs afterrequire_api_key.request.state.principal(already populated byrequire_api_keyin v0.8.0+).POST /api/v1/scansper the existing scope).Alternative: keep middleware but have it call
require_api_keyitself synchronously. Less idiomatic, more coupling.Difficulty
~3 hr. Architectural reordering + tests. Tracked from v0.7.0 deferral.