Skip to content

feat: db-backed admin bans with login + registration enforcement (#8)#15

Open
rafiki270 wants to merge 1 commit into
mainfrom
feat/db-backed-bans
Open

feat: db-backed admin bans with login + registration enforcement (#8)#15
rafiki270 wants to merge 1 commit into
mainfrom
feat/db-backed-bans

Conversation

@rafiki270

Copy link
Copy Markdown
Contributor

Closes #8 — replaces the non-functional ban stub (hardcoded emptyBans, no model, no endpoints, no enforcement) with a real, per-tenant deny list. The brief was silent on bans, so scope was confirmed with the owner: full per-tenant.

Data

  • New bans table + BanType enum (email | pattern | ip | user). Scoped to a client domain by default, or a specific org_id / team_id within it (FKs cascade). Scope is derived from which id is set.
  • Admin-managed like client_domains: granted to the BYPASS-RLS uoa_admin role, REVOKEd from uoa_app, with a deny-all RLS policy. Login enforcement runs before any tenant context, so it uses the admin client.
  • Hand-written migration matches the Prisma model (verified with prisma migrate diff — no bans drift) and applies cleanly into a fresh schema.

Matching (ban-policy.service.ts)

  • email: exact, case-insensitive · pattern: shell-glob over the email (*,?) — never a raw regex, so a ban can't become a ReDoS vector · ip: exact or IPv4 CIDR against request.ip · user: exact userId.

Enforcement — ban overrides allow; SUPERUSER on the login domain bypasses (mirrors the allow-list)

  • Login: finalizeAuthenticatedUser checks domain/org/team scopes after the allow-list. request.ip threaded through all six call sites (login, social callback, email link, verify-email, 2FA verify + enroll). Fails closed with ACCESS_DENIED (generic to the user).
  • Registration / social new-user: domain-scope email/pattern/ip checked before any user row is created. Registration stays silent + timing-equalised (no enumeration); social returns blocked. Existing social users are caught by the login chokepoint.

Admin

  • GET/POST /internal/admin/bans + DELETE /internal/admin/bans/:id (superuser only). getAdminSettings now returns the live list grouped by type instead of emptyBans. /api machine schema updated.
  • Admin React Settings page: the Add dialog and Remove buttons are wired to the new endpoints (create + delete, invalidating the settings query). Domain-scope from the UI; org/team scope available via the API.

Tests

  • ban-policy.service.test.ts (10): matcher per type, multi-scope, SUPERUSER bypass, IP/CIDR in/out, no-IP-supplied, registration boolean.
  • ban-enforcement.test.ts (DB-backed, skips without DATABASE_URL): domain-scope email ban blocks login + registration and lifts on delete; org-scope ban hits only the org member, not the owner; cross-tenant scope rejected. Validated against a real Postgres.
  • pnpm typecheck + lint green for @uoa/api and @uoa/admin; Admin build green. Pre-existing auth-entrypoint* / internal-admin-auth suite failures are environmental (missing Auth/dist/Admin/dist) and unchanged.

Brief documents the feature (Docs/brief.md → "2026-06 Admin Bans").

Note: stacks conceptually alongside #14 (config/validate + org schema) but is independent — no file overlap.

🤖 Generated with Claude Code

Replaces the non-functional ban stub (issue #8) with a real, per-tenant deny
list. The brief said nothing about bans, so scope was confirmed with the owner:
full per-tenant.

Data: new `bans` table (BanType email|pattern|ip|user), scoped to a client
domain or a specific org/team within it (FKs cascade). Admin-managed like
client_domains — granted to the BYPASS-RLS admin role, denied to the runtime app
role. Hand-written migration matches the Prisma model (drift-checked).

Matching: exact email (case-insensitive), shell-glob pattern over the email
(never a raw regex — no ReDoS), exact IP or IPv4 CIDR, and exact userId.

Enforcement (ban overrides allow; SUPERUSER on the login domain bypasses, like
the allow-list):
- login chokepoint finalizeAuthenticatedUser checks domain/org/team scopes;
  request IP threaded through all six call sites. Fails closed with ACCESS_DENIED.
- registration (auth-register) and social new-user paths check domain-scope
  email/pattern/ip before any user row is created; registration stays silent and
  timing-equalised (no enumeration).

Admin: GET/POST /internal/admin/bans + DELETE /internal/admin/bans/:id (superuser
only); settings read now returns the live list grouped by type instead of the
empty stub. /api schema updated. Admin React Settings page wires create + delete
against the new endpoints.

Tests: ban-policy unit tests (matcher, scopes, superuser bypass, IP/CIDR) and a
DB-backed enforcement test (domain + org scope, cross-tenant rejection, delete
lifts enforcement) validated against a real Postgres. Brief documents the feature.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Ban feature is a non-functional stub — banned users can still sign in (no persistence, no enforcement)

1 participant