feat(admin): super-admin-gated rate limit and IP ban management #424

Merged
Claude-Run merged 1 commit from feat/BUNYIP-413-super-admin-rate-limit-ip-ban-management into main 2026-07-31 19:51:14 +02:00
Member

The rate limits screen could only reset an active throttle and the IP bans screen could only lift a ban, so neither rate limiting nor manual blocking could be configured from a running system. Both screens now support the missing half, restricted to the super admin, because a mis-set cap or a careless ban can lock the platform out for everybody.

Super admin is the "first setup account": a new users.is_super_admin column, set when the BOOTSTRAP_ADMIN_EMAIL account is promoted and when SETUP_DEFAULT_ADMIN seeds the first admin, and backfilled by the migration onto the earliest-created admin so existing deployments have one. The flag is read per request by a new SuperAdminUser extractor rather than carried as a JWT claim, so revoking it takes effect immediately and existing sessions need no re-issue; only the low-traffic management endpoints pay the lookup.

Rate-limit configuration is now persisted per action in rate_limit_configs, with precedence const -> RATE_LIMIT_{ACTION}_MAX_REQUESTS / _WINDOW_SECONDS env -> persisted row. A fresh install keeps the built-in defaults, an override applies on the next request with no restart, and deleting the override reverts to the default. Resolution happens inside RateLimitRepository::check_and_increment / check / get_retry_after, so every one of the enforcement call sites picks an override up unchanged. The four places that compared a count against an unresolved cap (the shared check_rate_limit log-once gate, the 2FA per-account lockout, the OCI failure caps, and the admin active-throttle view) now resolve the effective config first, and the rate_limits retention horizon is derived from the longest window in force instead of a hard-coded hour, which a longer configured window would have invalidated.

Overrides are managed for the known enforced actions rather than free-form names: an action with no enforcement call site would be stored but inert, so PUT/DELETE /v1/admin/rate-limit-configs/{action} reject an unknown action.

IP bans gain a create path (POST /v1/admin/ip-bans) through AutoBanService::ban, which updates the in-memory map first (effective on the address's next request) and persists the ip_bans row synchronously (surviving a restart). The auto-ban middleware's enabled flag now governs strike recording only, not enforcement, so a manual ban still holds on a deployment with auto-banning switched off.

Reading the limit configuration stays open to any admin; every mutation is super-admin-only and audited via three new audit actions.

#BUNYIP-413

The rate limits screen could only reset an active throttle and the IP bans screen could only lift a ban, so neither rate limiting nor manual blocking could be configured from a running system. Both screens now support the missing half, restricted to the super admin, because a mis-set cap or a careless ban can lock the platform out for everybody. Super admin is the "first setup account": a new `users.is_super_admin` column, set when the BOOTSTRAP_ADMIN_EMAIL account is promoted and when SETUP_DEFAULT_ADMIN seeds the first admin, and backfilled by the migration onto the earliest-created admin so existing deployments have one. The flag is read per request by a new `SuperAdminUser` extractor rather than carried as a JWT claim, so revoking it takes effect immediately and existing sessions need no re-issue; only the low-traffic management endpoints pay the lookup. Rate-limit configuration is now persisted per action in `rate_limit_configs`, with precedence const -> `RATE_LIMIT_{ACTION}_MAX_REQUESTS` / `_WINDOW_SECONDS` env -> persisted row. A fresh install keeps the built-in defaults, an override applies on the next request with no restart, and deleting the override reverts to the default. Resolution happens inside `RateLimitRepository::check_and_increment` / `check` / `get_retry_after`, so every one of the enforcement call sites picks an override up unchanged. The four places that compared a count against an unresolved cap (the shared `check_rate_limit` log-once gate, the 2FA per-account lockout, the OCI failure caps, and the admin active-throttle view) now resolve the effective config first, and the `rate_limits` retention horizon is derived from the longest window in force instead of a hard-coded hour, which a longer configured window would have invalidated. Overrides are managed for the known enforced actions rather than free-form names: an action with no enforcement call site would be stored but inert, so `PUT`/`DELETE /v1/admin/rate-limit-configs/{action}` reject an unknown action. IP bans gain a create path (`POST /v1/admin/ip-bans`) through `AutoBanService::ban`, which updates the in-memory map first (effective on the address's next request) and persists the `ip_bans` row synchronously (surviving a restart). The auto-ban middleware's `enabled` flag now governs strike recording only, not enforcement, so a manual ban still holds on a deployment with auto-banning switched off. Reading the limit configuration stays open to any admin; every mutation is super-admin-only and audited via three new audit actions. #BUNYIP-413
feat(admin): super-admin-gated rate limit and IP ban management
All checks were successful
E2E PR gate / Install + reachability (no deployment secrets) (pull_request) Successful in 16s
Check / fmt + clippy + build + tests (pull_request) Successful in 12m3s
Create release / Create release from merged PR (pull_request) Has been skipped
a4ee380170
The rate limits screen could only reset an active throttle and the IP bans screen could only lift a ban, so neither rate limiting nor manual blocking could be configured from a running system. Both screens now support the missing half, restricted to the super admin, because a mis-set cap or a careless ban can lock the platform out for everybody.

Super admin is the "first setup account": a new `users.is_super_admin` column, set when the BOOTSTRAP_ADMIN_EMAIL account is promoted and when SETUP_DEFAULT_ADMIN seeds the first admin, and backfilled by the migration onto the earliest-created admin so existing deployments have one. The flag is read per request by a new `SuperAdminUser` extractor rather than carried as a JWT claim, so revoking it takes effect immediately and existing sessions need no re-issue; only the low-traffic management endpoints pay the lookup.

Rate-limit configuration is now persisted per action in `rate_limit_configs`, with precedence const -> `RATE_LIMIT_{ACTION}_MAX_REQUESTS` / `_WINDOW_SECONDS` env -> persisted row. A fresh install keeps the built-in defaults, an override applies on the next request with no restart, and deleting the override reverts to the default. Resolution happens inside `RateLimitRepository::check_and_increment` / `check` / `get_retry_after`, so every one of the enforcement call sites picks an override up unchanged. The four places that compared a count against an unresolved cap (the shared `check_rate_limit` log-once gate, the 2FA per-account lockout, the OCI failure caps, and the admin active-throttle view) now resolve the effective config first, and the `rate_limits` retention horizon is derived from the longest window in force instead of a hard-coded hour, which a longer configured window would have invalidated.

Overrides are managed for the known enforced actions rather than free-form names: an action with no enforcement call site would be stored but inert, so `PUT`/`DELETE /v1/admin/rate-limit-configs/{action}` reject an unknown action.

IP bans gain a create path (`POST /v1/admin/ip-bans`) through `AutoBanService::ban`, which updates the in-memory map first (effective on the address's next request) and persists the `ip_bans` row synchronously (surviving a restart). The auto-ban middleware's `enabled` flag now governs strike recording only, not enforcement, so a manual ban still holds on a deployment with auto-banning switched off.

Reading the limit configuration stays open to any admin; every mutation is super-admin-only and audited via three new audit actions.

#BUNYIP-413
Claude-Run deleted branch feat/BUNYIP-413-super-admin-rate-limit-ip-ban-management 2026-07-31 19:51:15 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
psa-systems/bunyip!424
No description provided.