Skip to content

fix(auth): enforce the SSO email-code sign-in policy on every surface (ent#849) - #3428

Merged
vybe merged 7 commits into
devfrom
feature/ent849-sso-email-code-policy
Oct 10, 2026
Merged

vybe merged 7 commits into
devfrom
feature/ent849-sso-email-code-policy

Conversation

@obasilakis

Copy link
Copy Markdown
Contributor

Fixes abilityai/trinity-enterprise#849 (cross-repo: the private issue closes by hand at release).

Problem

The SSO setting "Keep email-code sign-in available" was stored and never read. Unchecking it did not stop emailed 6-digit codes from signing users in.

Change

New open-core seam services/login_policy_gate.py (same shape as mfa_gate). With no provider registered, behaviour is unchanged. A provider error fails open. Every function that mints, redeems or sends an emailed code now asks it:

Surface When the policy refuses
POST /api/auth/email/request, /verify 403 email_code_disabled for every address, before the body is read
GET /api/auth/mode new email_code_login_enabled
MCP inline sign-in no code minted, redeem refused
Workspace guest sign-in, Telegram/WhatsApp /login, Slack require_email, public-link email verification platform users and whitelisted addresses refused; outsiders keep codes
POST /api/token (admin password) never consults the gate (break-glass)

On the surfaces that also serve outsiders, a refusal reads like a normal send or a wrong code. While codes are refused, every request step sends in a detached task, and the public-link per-address 429 counts every request, so neither the body, the status nor the timing reveals membership.

UI: the login page shows the SSO buttons and Admin Login when email codes are off, and SSO buttons no longer vanish when email auth is off. A page opened before the toggle maps the 403 to "Sign in with SSO"; the CLI prints the same next step. The settings hint names the surfaces and says admin password login always stays available.

Guard: test_every_login_code_caller_asks_the_gate fails CI when a new function calls create_login_code / verify_login_code / create_verification / verify_code / send_verification_code without asking the gate (exemptions listed with reasons).

The private module's provider lands in a separate draft PR that depends on this one. No submodule pointer move here.

Tests

  • tests/unit/test_ent849_email_code_policy.py: 35 tests (gate, every surface, uniform bodies/timing/429, /token untouched, AST caller guard).
  • src/frontend/tests/unit/loginEmailCodePolicy.spec.js: 9 mounted tests (render states, loading hold, store refusal, stale-page 403).
  • Mutation: each fix reverted from a scratch copy turned its test red; restored byte-identical.
  • Backend unit island: 24,806 passed, 0 failed. Frontend: all pass except portalAgentsAtCentre.spec.js, which fails on dev without this change (date-dependent).

Security

CSO diff report: docs/security-reports/cso-diff-2026-10-09-ent849-email-code-policy.md. Two medium findings from the adversarial pass (Slack flow ungated; public-link 429 membership oracle) are fixed in this branch. Open: 0.

Not in this PR: revoking sessions, MCP keys and channel bindings issued before the toggle (abilityai/trinity-enterprise#851).

🤖 Generated with Claude Code

obasilakis and others added 3 commits October 9, 2026 12:55
…Abilityai/trinity-enterprise#849)

The "keep email-code sign-in available" setting was stored and never read.
New open-core seam services/login_policy_gate.py (provider registered by the
private module; no provider = unchanged OSS behaviour; provider error fails
open). Every function that mints or redeems an emailed code now asks it:

- /api/auth/email/request and /verify: uniform 403 email_code_disabled,
  checked before the body is read. /api/auth/mode adds
  email_code_login_enabled.
- MCP inline sign-in: silent suppression and refusal before redeeming.
- Workspace guest sign-in, Telegram/WhatsApp /login, public-link email
  verification: platform users and whitelisted addresses are refused,
  outsiders keep codes. Refusals read like a normal send or a wrong code;
  while the policy refuses, every code request takes the same detached
  background path, so timing does not reveal membership.
- /token (admin password) never consults the gate.

Login page shows SSO buttons and Admin Login when email codes are off, and
SSO buttons no longer disappear when email auth is off. A page opened before
the toggle maps the 403 to "Sign in with SSO"; the CLI does the same.

A per-function AST guard fails CI when a new caller of create_login_code,
verify_login_code, create_verification or verify_code skips the gate.
Mutation-checked: each new backend and frontend test goes red with its fix
removed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…bilityai/trinity-enterprise#849)

The Slack legacy require_email check mints and redeems its own emailed code;
it now takes the same member rule and detached-send path as Telegram and
WhatsApp, and draws the code from secrets. The public-link request step counts
every request per address while codes are refused, so the 429 lands on the
same request for a member and an outsider. The caller guard also covers
send_verification_code, which is how the Slack flow escaped it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@obasilakis obasilakis added the ui PR touches the frontend UI — triggers Playwright e2e tests label Oct 9, 2026
@obasilakis

Copy link
Copy Markdown
Contributor Author

Private half: the provider lands in a draft enterprise PR that depends on this one.

obasilakis and others added 3 commits October 9, 2026 13:06
…gs fragment (Abilityai/trinity-enterprise#849)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts:
#	tests/registry.json
@vybe

vybe commented Oct 9, 2026

Copy link
Copy Markdown
Contributor

merge-train (2026-10-09): merged origin/dev into this branch (f4668a3) to clear the tests/registry.json conflict — stage-based union (theirs + this PR's entries vs base, 457 entries, no new duplicates). Mechanical, no source change.

@vybe

vybe commented Oct 9, 2026

Copy link
Copy Markdown
Contributor

merge-train (2026-10-10): merged origin/dev again after #3412 landed. Two conflicts, both the shared from services import … line in client_portal/service.py and routers/public.py — resolved as the union of both sides' names; tests/registry.json union. test_ent849_email_code_policy + test_ent754_skills_list_cache + test_3461_public_status_failure_text pass on the merged tree (75 passed). Mechanical.

@vybe vybe left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

merge-train: validated individually on top of trains A (#3500) and B (#3504); own CI green on the merged head

@vybe
vybe merged commit db963b3 into dev Oct 10, 2026
26 checks passed
vybe added a commit that referenced this pull request Oct 10, 2026
webmixgamer added a commit that referenced this pull request Oct 10, 2026
…erges) into the review round

The remote PR branch carried two merge-train merges of dev on top of
37829f0 (#3412, #3428, #3479 and others). One conflict, Settings.vue:
- the whitelist add now goes through #3479's settingsStore.addWhitelistEmail,
  which returns { email, existingAccountRole } so the form can still name an
  existing account's role (emailWhitelist.spec updated, pass-through case
  added);
- the add's refusals use #3455's InlineError under the form, beside the
  account notice, and the page-wide error is no longer set (the eyeball of
  d9473d3: a 409 landed at the bottom of Settings); pinned in
  workspaceOnlyWiring.spec;
- #3479's guarded workspaceOnlyUsers computed, plus this round's filter reset.
test_3455's whitelist fake gains the two reads the route now makes.

Re-run on the merge: targeted backend 3983 passed (134 files incl. every unit
test the merges changed), frontend 5501 passed (3 known host-only files), MCP
829/831 (2 = the host's pnpm layout), CLI suites green.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@dolho dolho mentioned this pull request Oct 13, 2026
1 task done
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ui PR touches the frontend UI — triggers Playwright e2e tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants