Enforce MFA in HIPAA orgs

Task 5943 · Done · Coder · created · updated

Epic ask (verbatim): "Research the gap to get Jaah HIIPA compliant and fix everything that does not require human and does not break what is working now by imposing limits" (HIIPA = HIPAA). Full PRD/architecture: the epic's settled `prd` (Task 5917) and `architecture` (Task 5918) — architecture child **C6**. Builds on the HIPAA setting core child (`apps/api/app/hipaa.py` `is_on`). BA decision (verbatim) B4: "In a HIPAA org, `Org.mfa_skip` and per-user `AdminUser.mfa_exempt` are not honoured (they are limits only that org opted into). Trusted-device remember stays as today." ## Requirements (verbatim) - **AC5.5 MFA.** For a member of a HIPAA ON org acting in that org, `Org.mfa_skip` and `AdminUser.mfa_exempt` are ignored: sign-in requires enrolment/second factor. The org settings show "MFA skip is not applied while HIPAA is on" where the skip control lives. OFF orgs unchanged. - verify: api test: HIPAA org with `mfa_skip` true → login returns `enrol_required`/`mfa_required`; same org OFF → skip honoured. - **AC5.8 (MFA half)** Turning it OFF (operator only) restores every default immediately for the next request. - **AC5.9 Quality** (own surface: the TwoFactor note) — ui-copy-standards, fits supported widths, matches MFA presentation. ## Approach (architecture C6, D-A4) - `apps/api/app/mfa.py:405-424` `skip_reason`/`org_skips`: return None when `hipaa.is_on(org)` for the membership's org (used `login_funnel.py:228-230`, `:396`). Trusted device unchanged. - D-A4: a session with `amr == "skip"` whose MEMBERSHIP row's `org_id` is a HIPAA org is refused at the next request (401 `unauthorized`) — same row-load seam in `auth.py` as the idle child — otherwise a 30-day skip session survives the switch. Scope = the membership's own org: an operator acting in a tenant through an operator-plane membership is governed by the operator org's MFA (`org_skips` already returns false for superadmins, `mfa.py:405-411`), so the operator login is never locked out by a tenant's HIPAA switch. - `apps/web/src/pages/orgs/SsoTab.tsx` `TwoFactor`: "MFA skip is not applied while HIPAA is on." where the switch lives (read `org.hipaa.enabled`). - The idle child edits the same `auth.py` seam concurrently; keep your change small and self-contained on `hipaa.is_on`. Credentials & reach: api tests — worktree only, **verified**. AC5.5 keeps the api-test oracle only (no non-operator HIPAA Test login exists — architecture §3b). `jaah/debug/main/operator-login` on `https://app.dev.jaah.ai` — **verified**, for viewing the TwoFactor note in "HIPAA Test" (never turn HIPAA on in Org B — its admin signs in without MFA and runners use that login). Out of scope: N1 "No new limit for any org whose HIPAA setting is OFF: idle 7 d / 30 d absolute, MFA skip/exempt, providers, Slack, Help chat, Jira, plan entitlements — all unchanged." N5 "No change to password rules, lockout thresholds, or session absolute max for non-HIPAA orgs." Do NOT change trusted-device remember, idle logic (C5), or operator-org MFA behaviour. Done means: - AC5.5 — api tests: HIPAA org + `mfa_skip` true → login `enrol_required`/`mfa_required`; HIPAA org + user `mfa_exempt` → same; OFF org → skip honoured; existing `amr=skip` session in an org turned HIPAA ON → next request 401; operator acting in a HIPAA tenant is NOT locked out. - AC5.8 (MFA half) — api test: toggle HIPAA off → skip honoured again on the next login. - AC5.9 (own surface) — dev browser in "HIPAA Test": TwoFactor shows the note beside the skip control, fits at desktop and 360 px. - `./dc check full` green; PR merged to develop and Jaah Deploy green on dev.

Outcome

HIPAA orgs ignore mfa_skip/mfa_exempt at sign-in; turning HIPAA on retires skip sessions (epoch bump, all cookie paths); TwoFactor note verified on dev at desktop and 360 px. PR #5156 merged, deploy green. Full gate green bar 1 timing flake. Follow-up: Orgs list shreds org names.

Pull request

Artifacts

Activity

  • Draft
  • Ready
  • Runner started
  • Runner ended (done)