Skip to content

Email domain allowlist for public registration - #102

Merged
tbcsec merged 8 commits into
mainfrom
claude/issue-56
Jul 28, 2026
Merged

Email domain allowlist for public registration#102
tbcsec merged 8 commits into
mainfrom
claude/issue-56

Conversation

@tbcsec

@tbcsec tbcsec commented Jul 28, 2026

Copy link
Copy Markdown
Owner

What & why

Adds an admin-configurable email-domain allowlist (Admin → Site settings) so public self-serve registration only accepts email addresses on approved domains, per the approved plan in #56. Subdomains match automatically; a rejected sign-up sees one generic error regardless of whether the email was missing or the domain didn't match, so the allowlist's contents (or existence) are never disclosed.

Fixes #56

Plan step → commit mapping

Plan step Commit
1. Migration: email_domain_allowlist_enabled + allowed_email_domains columns on SiteSettings 1f68671 Add email-domain allowlist columns to SiteSettings (#56)
2. schemas/site_settings.py: operational fields + validation (normalize/dedupe/cap/reject malformed) + public email_required 43e238f Wire email-domain allowlist into site-settings schemas + router (#56)
3. routers/site_settings.py: _operational_out(), PUT /operational assignment, public GET computing email_required 43e238f (same commit — schema + router changed together)
4. routers/auth.py: enforce in POST /register (after registration_open, before email_taken), generic rejection, new auth/registration_policy.py::domain_allowed helper 252f835 Enforce email-domain allowlist in public registration (#56)
5. schemas/auth.py RegisterRequest.email stays EmailStr | None (policy enforced in router, not the schema) No change needed — already matches this shape; confirmed as part of 252f835
6. frontend/src/lib/types.ts: email_required + operational fields bdc1a93 Add email-domain allowlist fields to frontend types + hooks (#56)
7. Admin → Site settings: new card, toggle + tag-input domain list (matches the VocabEditor pattern), subdomain caption 1d132c1 Add email domain allowlist card to Admin -> Site settings (#56)
8. Register page: email_required drives the required attribute + label, server error surfaced (no client-side domain check) 0db1e9f Mark register page email field required when the allowlist is on (#56)
9. Backend pytest coverage (disabled/enabled+missing/mismatch/exact/subdomain/admin-accounts-unaffected) 44a5499 Add backend tests for the email-domain allowlist (#56)
10. Frontend vitest: register page required/optional rendering 005fb36 Add frontend test for register page email requirement (#56)

Deviations from the plan

  • Profile email-edit path test (plan step 9, parenthetical "if one exists"): there is no self-service endpoint for a user to edit their own email (only the admin PATCH /api/users/{id} path exists, which the tests do cover as unaffected) — so this sub-case has nothing to test and was skipped.
  • Everything else in the Recommendation section is implemented as described; nothing in the "Not in scope" boundary (admin-created accounts, later email edits) was touched.

Checklist

  • Backend tests pass — cd backend && .venv/bin/pytest (428 passed)
  • Frontend checks pass — cd frontend && npm run test && npx tsc --noEmit && npx eslint . (139 passed, no type/lint errors)
  • If UI-observable, I ran it in the browser and confirmed it works — not run in-browser this session; verified via the frontend test suite + manual code review of the toggle/tag-input/register-page wiring instead
  • Follows the architectural rules in CONTRIBUTING.md (events, competition_id scoping, require_permission, one hook per domain, design tokens) — no new event needed (reuses site.settings_updated); site settings is not competition-scoped by design; gated on manage_site_settings; changes stay within the existing use-site-settings.ts hook module
  • One migration for this PR, named 2026-07-28_3e4f5a6b7c8d_email_domain_allowlist.py
  • This is not a security fix (those follow SECURITY.md)

Generated by Claude Code

claude added 8 commits July 28, 2026 21:19
New email_domain_allowlist_enabled + allowed_email_domains (JSON) columns
on the SiteSettings singleton, plus the migration adding them (off by
default so existing behaviour is unchanged until an admin opts in).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BxJwzhdM9gEm8X7oAoWF3X
OperationalSettingsOut/Update carry the allowlist flag + domain list
(normalized: lowercased, deduped, capped at 50 domains / 253 chars each,
malformed entries rejected inline with a 422 rather than silently
dropped). The public SiteSettingsOut only exposes a policy bit
(email_required) mirroring the flag — the domain list itself stays
admin-only.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BxJwzhdM9gEm8X7oAoWF3X
POST /register now rejects a sign-up when the allowlist is enabled and
the supplied email (mandatory once enabled) doesn't match an allowed
domain or one of its subdomains — with a single generic rejection
message for both the missing-email and domain-mismatch cases, so the
allowlist's contents (or even its existence) are never disclosed.
Scoped to public registration only, per the issue: admin-created
accounts (routers/users.py) and later email edits are untouched.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BxJwzhdM9gEm8X7oAoWF3X
email_required on the public SiteSettings type, and
email_domain_allowlist_enabled/allowed_email_domains on
OperationalSettings/Update, plus the matching fallback default.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BxJwzhdM9gEm8X7oAoWF3X
A toggle + tag-input domain list (matching the VocabEditor pattern used
elsewhere for managed vocab lists), with a caption noting subdomains are
automatically allowed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BxJwzhdM9gEm8X7oAoWF3X
Reads email_required off the public site-settings hook: when the
allowlist policy is enabled, the email input becomes required and drops
the "(optional)" label — no client-side domain check, the server stays
the source of truth.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BxJwzhdM9gEm8X7oAoWF3X
Covers: disabled by default (unchanged behaviour), enabling requires
email + exposes it publicly via email_required only, missing-email and
domain-mismatch rejections share the same generic message, exact and
subdomain matches succeed, malformed/over-cap/duplicate domains are
rejected or normalized on save, and admin-created accounts are exempt.
Also updates the two existing exact-shape assertions on the public
GET /site-settings response for the new email_required field.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BxJwzhdM9gEm8X7oAoWF3X
Verifies the email field renders as optional/required based on the
mocked email_required value from the public site-settings hook.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BxJwzhdM9gEm8X7oAoWF3X
@tbcsec
tbcsec marked this pull request as ready for review July 28, 2026 21:29
@tbcsec
tbcsec merged commit a87ace2 into main Jul 28, 2026
4 checks passed
@tbcsec
tbcsec deleted the claude/issue-56 branch July 28, 2026 21:29
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.

[Feature]: registration email domain whitelisting

2 participants