Update_REGISTRATION_DISABLED_behavior (breaking change) - #8383
Conversation
|
REGISTRATION_DISABLED=false, true, local, SSO |
ADR-004 Review — verification, industry comparison, and recommended revisionsReviewed against the heimdall2 source ( 1. Verification findings (against actual source)
The LDAP finding settles review questions 1 and 2 together: the gate covers LDAP automatically — and therefore the variable name 2. Industry comparison
Every product surveyed agrees with this ADR's core call: local registration and external-auth JIT are separate controls (rejecting Option A is industry-consistent). The gap: GitLab and SonarQube show two patterns this ADR doesn't consider — the approval queue and the mode enum. Sources: GitLab OmniAuth · Grafana auth · SonarQube provisioning · Harbor OIDC 3. Recommended design changes3a. Mode enum instead of boolean — GitLab's lesson is that booleans accrete into a config maze. One enum absorbs the future without migration: EXTERNAL_AUTH_USER_PROVISIONING=jit | approval | disabled # default: jit
Fail-fast on unknown values at startup. Note the current boolean pattern silently treats a typo ( 3b. State the choke point as an architectural invariant. All six strategies (google, github, gitlab, okta, oidc, ldap) funnel through async provisionExternalUser(identity: ExternalIdentity): Promise<User> {
const existing = await this.findByEmail(identity.email);
if (existing) return this.refreshMetadata(existing, identity);
switch (this.config.externalAuthProvisioning()) { // exhaustive
case 'jit': return this.createActiveUser(identity);
case 'approval': return this.createPendingUser(identity); // phase 2
case 'disabled': throw new AccountNotProvisionedError();
}
}3c. Promote audit logging from review question (§11 Q3) to in-scope AC. §1.3's justification is "auditors expect account creation to be traceable" — the rejection event IS the audit artifact this feature exists to produce. Structured events at the choke point: 3d. Specify the frontend error contract now (§11 Q4, §9.3 risk). 3e. Add the misconfiguration advisory — app + docs + defaults. The ADR's own §9.2 negative ("admins can set
4. UX sketchesLogin modal — rejected ( Admin panel — persistent configuration advisory (post-login, dismissible): Admin ▸ Users — approval queue (phase 2 payoff: default-deny with one-click approvals, no manual email matching): 5. Migration story this enables
6. Direct answers to §11 review questions
Smaller items
|
Updated ADR scope based on reviews 7/9/2026
Re-review of the revised ADR — endorsed, with answers to §12 and a set of editsThe Answers to the §12 review questions, plus edits being applied directly to the ADR in a follow-up commit: Q1 — unknown values: fail fast. The enum makes permissive-on-unknown more dangerous than before: Q2 — yes, it's a breaking change, and release notes are the weakest channel. The cohort that breaks ( Q3 — split it. Admin user creation while Also restored: the frontend rendering contract. The revision keeps the Two hygiene edits:
Edits incoming on this branch. |
…n-page rendering contract, Option E, LDAP matrix rows Resolves review questions 1-2: unknown REGISTRATION_DISABLED values refuse to start (enum typo must not silently open both account-creation paths); breaking change promoted to required deliverable with three communication channels (release note, boot warning targeting the affected cohort, login alert). Restores the frontend rendering contract for account_not_provisioned. Adds Option E (approval queue) as rejected-with-structural-note, explicit LDAP rows in the behavior matrix, and work-order phases 5-6 for the new surfaces. Authored by: Aaron Lippold<lippold@gmail.com> Signed-off-by: Aaron Lippold <lippold@gmail.com>
…aking cohorts, provisioning gates, test matrix - 3.5/3.6: specify the REAL error transports (authenticationErrorCode cookie for OAuth callbacks, 401 JSON body for LDAP/local POST — the redirect query-parameter mechanism described previously does not exist), alert precedence over the generic snackbars, filter log redaction - 3.3: structured error code on the exception, NotFoundException-only catch, missing-email guard (external_identity_missing_email), LDAP await fix and multi-valued-mail binding rules - 6.2: second breaking cohort (formerly-permissive invalid values now refuse to boot) and the upgrade-ordering/rollback rule for local/sso - 6.3: per-strategy verified-email reality (GitHub/Google check, OIDC bypassable via OIDC_USES_VERIFIED_EMAIL=false, GitLab/Okta/LDAP do not) plus in-scope mitigations - 8.2: LOCAL_LOGIN_DISABLED=true blocks admin user creation too — provision before disabling; LDAP first-value email note - 3.7: enabled-strategy predicate defined; warning stated as a permanent configuration-shape notice - 3.1: trim before validate; 7.1 whitespace/legacy-value rows and per-family warning tests; 7.2 non-NotFound rethrow + missing-email tests; new 7.5 exception-filter and 7.6 frontend test sections; 7.4 actual test-surface facts and required automated deny-path e2e - 3.2/13: frontendStartupSettings registrationEnabled surface documented; 3.4 manifest example quoted as a string; 9.3 risks grounded; work-order phases 4-9 updated to match; header credits both authors Findings verified by independent adversarial review against the worktree source before application. Authored by: Aaron Lippold<lippold@gmail.com> Signed-off-by: Aaron Lippold <lippold@gmail.com>
…ormalization, NIST mapping, precedents Findings from a verified web-research pass (every citation re-fetched against primary sources): - 2/5/6.3: name the sso self-provisioning bypass precisely — the Classic-Federated Merge attack (Sudhodanan & Paverd, USENIX Security 2022 Sec 4.1); sso prevents silent JIT, not admin-gated provisioning; new behavior-matrix row; email-ownership verification carded as open question 12.4 - 3.3: trim+lowercase external emails before lookup/create (findByEmail is case-sensitive, no normalization exists anywhere in the users module); 8.2 operator rule; 7.2 test 14 - 6.1: NIST 800-53 rev 5 mapping (AC-2a/e/f/i, AC-2(1), IA-4a/d) and 800-63C-4 Sec 4.6.3 provisioning-model framing for ATO documentation - 6.3: email is the wrong long-term binding key — OIDC Core 5.7 (sub+iss only stable identifier), ASVS 5.0 V10.5.2, 800-63C-4 3.4/2.3/3.8.1; iss+sub identity table named as the future ADR; no user-disable capability documented as an AC-2f gap - 3.5: audit event JSON shape pinned (winston loggers have no structured convention to inherit) - 3.6: alert names the attempted provider (GitLab precedent); ASVS V6.3.8 enumeration reasoning recorded; Keycloak generic-failure anti-pattern cited - 3.1/3.7/6.2: precedents cited — PostgreSQL password_encryption boolean-to-enum + v14 strictness, Django 4.0 CSRF system check, Grafana in-product warnings, GitLab upgrade-notes process; permanent warning noted as a deliberate extension beyond precedent - 2: default-JIT-on rationale recorded (Gitea/Forgejo default off; GitLab/Grafana default on) - 4: Option C gains per-provider precedents + Mattermost choke-point cautionary tale; Option E gains blocked_pending_approval specifics and a composition rule for future provisioning variables - 3.4/9/11: mitre/saf-packaging backend.env surface; 13: standards reference list Authored by: Aaron Lippold<lippold@gmail.com> Signed-off-by: Aaron Lippold <lippold@gmail.com>
… source Replace inferred framework behavior with facts verified against the exact versions this repo resolves (@nestjs/common@11.1.28, @nestjs/passport@11.0.5 per yarn.lock) and the current NestJS exception-filter docs: - 3.3: object-form UnauthorizedException replaces the ENTIRE 401 body verbatim (HttpException.createBody returns the object as-is), so the snippets now include statusCode explicitly; exception.message still resolves to the message field, keeping existing filter/log code working - 3.5: exact wire body documented for the POST transport; the filter extracts the code via exception.getResponse().error (verified public API) - 3.3: LDAP un-awaited promise mechanism made precise — auth.guard.js handleRequest rethrows the original strategy error, while the un-awaited case survives only via promise adoption at canActivate's await; the await-fix routes rejection through the verified rethrow path Authored by: Aaron Lippold<lippold@gmail.com> Signed-off-by: Aaron Lippold <lippold@gmail.com>
ADR-005 proposes the mitre/vulcan docs pattern for heimdall2: self-contained docs/ (own package.json/yarn.lock, VitePress 2 + Vue 3) isolated from the Vue 2 app by workspace-glob exclusion — root package.json/lerna.json never change; GitHub Pages deploy workflow; migration of all 24 wiki content pages plus repo Markdown and ADRs into a published decisions/ section; wiki reduced to pointer stubs. Alternatives: status quo, plain markdown, wiki-sync action, MkDocs, Docusaurus. Six implementation phases. ADR-004: 3.4 and References note the wiki is the channel today and ADR-005 supersedes it when implemented. Authored by: Aaron Lippold<lippold@gmail.com> Signed-off-by: Aaron Lippold <lippold@gmail.com>
Epic heimdall2-4qg (10 cards, ADR-004 phases 1-10) and epic heimdall2-yvx (6 cards, ADR-005 phases 1-6) now referenced from each ADR's work-order section; card IDs mirror phase numbers. Cross-epic soft reference on Phase 9 (wiki path is the fallback until the docs site lands). Authored by: Aaron Lippold<lippold@gmail.com> Signed-off-by: Aaron Lippold <lippold@gmail.com>
…abilities §2.3.1 file-level reference tree (every wiki page's destination, NEW pages marked: production-checklist, decisions index, release-notes section), §2.3.2 landing hero/features/nav spec, §2.3.3 local search + canonical env-vars page rule + CONTRIBUTING.md gap noted as an owner decision. Authored by: Aaron Lippold<lippold@gmail.com> Signed-off-by: Aaron Lippold <lippold@gmail.com>
…ing links Both work-order sections now tell the team how to pull the board (bd from gastownhall/beads, bd dolt pull / bd bootstrap), carry the v49->v54 pull-before-upgrading-bd migration note, and link mitre/mitre-saf-skills for the card/TDD/AC-verify workflow. Authored by: Aaron Lippold<lippold@gmail.com> Signed-off-by: Aaron Lippold <lippold@gmail.com>
Deep review + research pass applied — seven commits on this branchFollowing up on my earlier endorsement: I ran a full verification pass over the revised ADR (every finding independently re-checked against the code before being applied), plus a targeted research pass with every citation verified against primary sources. Net result: the enum design held up across all of it — nothing overturned the core decision. What changed is precision, evidence, and scope-honesty. Highlights worth your eyes: The two things that were blocking
Other substantive additions
ADR-005 + work tracking
Eugene — the seven commits are individually scoped if you want to review pass-by-pass; happy to walk through any finding. If anything reads as over-reach, flag it and we'll cut it back. |
|
OK, now that I've actually read this PR, ADR, comments and all, a few things stick out. @NickShumar you're good to proceed, but note that Aaron apparently already created beads for this work, so you should figure out if those are present in your local board and sync them if they aren't; I think you said you'd been making some yourself? Just check they're not duplicative. Let me know if you have trouble. The ADR doc hasn't actually changed since we had that meeting about it weeks ago. Aaron added a comment with more context, that's all. The comment is a refinement to the suggested implementation of the decisions in the ADR, it doesn't change anything we agreed on as a team. As previously discussed, ADR-005 should have gone in its own PR so we can review one thing at a time; I'll split it out later, @NickShumar you can ignore it for now. Proceed to work and close the rest of the beads and then we'll review the actual code. At a glance it seems like you've got the tests for TDD in place, which is a good sign. |
Human-readable cards for current code from @NickShumar: ADR-004: External Login Account PolicyPlain-language summary of cards .1 through .10What this project was meant to solveHeimdall lets people create and access accounts in two ways:
Before this work, an administrator could disable local registration, but Heimdall could still silently create a new account the first time someone used an external login. This project gives administrators separate control over those two account-creation paths. The new settings
People who already have a matching Heimdall account can continue signing in through their external provider under every setting. Card .1 — Add the new configuration choicesPurpose Give administrators the four settings shown above and make configuration mistakes safe. What was completed
Why this matters A typo in a security setting can no longer accidentally open both account-creation paths. How it was checked Every valid value, capitalization variation, spacing variation, and representative invalid value was tested. Startup was also tested with valid and invalid settings. Status: Completed Card .2 — Prevent unwanted external account creationPurpose Stop Heimdall from creating a new account when external account creation is disabled. What was completed
Why this matters Successful identity-provider authentication no longer automatically means that the person is authorized to receive a Heimdall account. How it was checked Tests covered new users, existing users, mixed-case email addresses, missing email addresses, database failures, and all policy values. Status: Completed Card .3 — Record rejected provisioning attemptsPurpose Give administrators an audit record when a verified external identity is denied because no Heimdall account exists. What was completed Heimdall records:
Passwords and authentication tokens are not included. Why this matters Administrators can distinguish a policy rejection from a bad password, provider failure, or application problem. How it was checked Tests verified the event’s contents and confirmed that sensitive authentication material was excluded. Status: Completed Card .4 — Preserve the reason for the login failurePurpose Ensure the browser receives a reliable reason code when a person is authenticated externally but lacks a Heimdall account. What was completed
Why this matters The frontend can show the correct explanation instead of treating the event as an ordinary login failure. How it was checked Tests covered both browser-redirect and direct-login response paths, generic failures, cookies, response bodies, and log redaction. Status: Completed Card .5 — Show a clear message to the person signing inPurpose Replace the generic authentication error with a useful explanation. What was completed When the policy blocks account creation, the login page shows a warning similar to:
The message identifies the provider the person attempted to use, such as LDAP, Okta, or the configured OIDC provider. The normal generic error message is suppressed for this specific condition. The Sign Up option follows the policy:
Why this matters People know that their external identity worked and that they need a Heimdall account. They are not incorrectly told that their credentials failed. How it was checked Frontend tests covered OIDC and LDAP, recognized and unrecognized errors, provider names, Sign Up visibility, and generic-message suppression. The alert was also reviewed in light and dark modes. Status: Completed Card .6 — Warn administrators about important startup conditionsPurpose Alert administrators when an upgrade or unsafe identity-provider configuration may change access behavior. What was completed
Why this matters Administrators receive actionable guidance at the time a configuration could surprise them or create an account-security risk. How it was checked Tests and live startup checks covered OIDC-only, LDAP-only, no-provider, safe, unsafe, and migration-related configurations. Status: Completed Card .7 — Test the complete policy matrixPurpose Prove that each setting behaves correctly for local registration and external login. What was completed Tests cover:
Additional mutation checks deliberately broke important conditions to confirm that the tests would detect those failures. Why this matters The policy is not supported by only a few happy-path tests. Each meaningful branch has evidence. How it was checked The affected backend test matrices passed 108 out of 108 checks. The full backend suite, builds, type checks, and accepted lint baseline were also reviewed. Status: Completed Card .8 — Make LDAP and OIDC follow the same policyPurpose Ensure LDAP failures travel through the correct authentication path and that external providers cannot bypass the shared policy. What was completed
Why this matters LDAP is covered by the same “SSO” policy even though it is not an OAuth provider. How it was checked
Status: Completed Card .9 — Document the setting and upgrade impactPurpose Explain the new choices and prevent upgrades from unexpectedly blocking or allowing users. What was completed in this repository
External documentation still awaiting authorization
Those external writes were not made because authorization was not provided. The exact proposed wiki wording is preserved in the local Card .9 notes. What was not included ADR-005’s proposed VitePress documentation site was not implemented. How it was checked Repository documentation surfaces were searched and reviewed, the backend regression suite passed, and the production build passed. Status: Repository work completed; external documentation follow-up remains Card .10 — Test the complete behavior with a real OIDC providerPurpose Verify the feature against a running Heimdall application and a real OIDC authorization flow rather than relying only on unit-test simulations. What was completed A live Dex OIDC provider was used to test:
Each login used a separate identity and included a database check. Why this matters The real provider, callback, token exchange, user-information lookup, Heimdall account check, browser response, and database behavior were exercised together. How it was checked
Status: Completed Manual user test performed after the PR updateWe also started Heimdall locally with The observed behavior was:
The mock provider deliberately marks only its first identity response as verified. Restarting the mock resets it; the later “verify your name and email” message was mock behavior rather than a problem with the feature. Overall result
Remaining follow-up
Items such as SCIM, group and role mapping, provider-specific controls, approval queues, email verification for local registration, and ADR-005’s VitePress site were intentionally outside this project. |
Note that these comments came after our ADR review/agreement on June 9. Hold off on these to a future phase, if project can support.
|
Keep the external-auth PR scoped to ADR-004. The VitePress plan now lives on its own branch.
Signed-off-by: NickShumar <nshumar@mitre.org>
|
|
This pull request has a conflict. Could you fix it @ejaronne? |



https://github.com/mitre/heimdall2/blob/external_auth_provisioning_policy/docs/adr-004-external-auth-user-provisioning-policy.md