feat: db-backed admin bans with login + registration enforcement (#8)#15
Open
rafiki270 wants to merge 1 commit into
Open
feat: db-backed admin bans with login + registration enforcement (#8)#15rafiki270 wants to merge 1 commit into
rafiki270 wants to merge 1 commit into
Conversation
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]>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
banstable +BanTypeenum (email | pattern | ip | user). Scoped to a client domain by default, or a specificorg_id/team_idwithin it (FKs cascade). Scope is derived from which id is set.client_domains: granted to the BYPASS-RLSuoa_adminrole,REVOKEd fromuoa_app, with a deny-all RLS policy. Login enforcement runs before any tenant context, so it uses the admin client.prisma migrate diff— nobansdrift) 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 againstrequest.ip·user: exact userId.Enforcement — ban overrides allow; SUPERUSER on the login domain bypasses (mirrors the allow-list)
finalizeAuthenticatedUserchecks domain/org/team scopes after the allow-list.request.ipthreaded through all six call sites (login, social callback, email link, verify-email, 2FA verify + enroll). Fails closed withACCESS_DENIED(generic to the user).blocked. Existing social users are caught by the login chokepoint.Admin
GET/POST /internal/admin/bans+DELETE /internal/admin/bans/:id(superuser only).getAdminSettingsnow returns the live list grouped by type instead ofemptyBans./apimachine schema updated.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 withoutDATABASE_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+lintgreen for@uoa/apiand@uoa/admin; Adminbuildgreen. Pre-existingauth-entrypoint*/internal-admin-authsuite failures are environmental (missingAuth/dist/Admin/dist) and unchanged.Brief documents the feature (
Docs/brief.md→ "2026-06 Admin Bans").🤖 Generated with Claude Code