Repository navigation
fix(plugin-auth)!: the global mfa_required switch cannot be saved on while OS_AUTH_TWO_FACTOR=false keeps two-factor enrollment off (#30302) - #30376
Conversation
…_TWO_FACTOR=false pin Claude-Session: https://claude.ai/code/session_01Utp6CZsesqReyo7ausntP8 Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Utp6CZsesqReyo7ausntP8 Co-authored-by: Claude <noreply@anthropic.com>
…-dw6hfs Claude-Session: https://claude.ai/code/session_01Utp6CZsesqReyo7ausntP8 Co-authored-by: Claude <noreply@anthropic.com>
…wed_ip_ranges one, before the first pass Claude-Session: https://claude.ai/code/session_01Utp6CZsesqReyo7ausntP8 Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 5 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 4 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 22 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 33b877235444b41159e7330231177c639baf1ad9 && git checkout 33b877235444b41159e7330231177c639baf1ad9
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 870ebce333bf0a5794307febe077ab5652cfbfe9 c2233d29d67bd73a2db1101368680c5b144a9262 && git checkout -B drift-repro 870ebce333bf0a5794307febe077ab5652cfbfe9 && git merge --no-ff c2233d29d67bd73a2db1101368680c5b144a9262
node scripts/docs-audit/affected-docs.mjs --json 870ebce333bf0a5794307febe077ab5652cfbfe9
|
|
domain:server#2 · os-bill · session_01Utp6Cbvcmv8YWFhFJwzjKL Contract reviewServed-tier: Face: narrowing — ① Derived judgments
② Semver levelChangeset ③ Boundary flags
④ Design judgmentOn the narrowing face, judged against the published spec and the runtime on Findings, each named:
Implemented-by: VERDICT: PASS Record written 2026-11-05T08:44Z. |
…-dw6hfs Claude-Session: https://claude.ai/code/session_01Utp6CZsesqReyo7ausntP8 Co-authored-by: Claude <noreply@anthropic.com>
|
domain:server#2 · os-bill · session_01Utp6Cbvcmv8YWFhFJwzjKL 维护者速读
席位核对 — ACCEPT · domain:server#2 · head
|
Requested by Bill · project thread
domain:server#2 · os-bill · session_01Utp6Cbvcmv8YWFhFJwzjKL
维护者速读
OS_AUTH_TWO_FACTOR=false把两步验证钉成关时,Setup → 认证里的全局「强制多因素认证」(auth.mfa_required)从关改成开会被拒存(400SETTINGS_VALIDATION,字段级本地化提示,点名这个环境变量和修法),整次保存什么都不写。另外改正两处文档:组织级require_mfa只有两步验证开着才能开,全局开关在上述钉死情况下拒存。no (narrowing)):只在OS_AUTH_TWO_FACTOR设为关的部署上,以前能存的「开」现在被拒。环境变量未设或设为开时行为不变;关掉开关、原值回存永远放行;升级前已经存成「开」的值原样保留(照旧不生效)。回滚即撤本 PR。packages/plugins/plugin-auth/src/mfa-required-enrollment-predicate.ts(规则与四语文案);auth-plugin.ts里bindAuthSettings的注册一行(紧挨allowed_ip_ranges那条写谓词);content/docs/permissions/authentication.mdx的mfa_required行。pnpm --filter @objectstack/plugin-auth exec vitest run src/mfa-required-enrollment-predicate.test.ts,预期 7/7 通过,其中一条断言OS_AUTH_TWO_FACTOR=false下保存被拒且什么都没写、features.twoFactor仍为 false。GET /api/v1/auth/config的features.twoFactor已与闸门同一判定,界面半边(Setup: organization 强制多因素认证 says nothing when two-factor is off, and the enrollment screen's refusal is an untranslated 404 #30295)读它即可。Fixes #30302
Clause-②: no (narrowing) — while
OS_AUTH_TWO_FACTORis set to off, a save that turns theauth.mfa_requiredsetting on is refused400 SETTINGS_VALIDATION(invalid_valueatmfa_required), a value an administrator could save before; saving it off, or re-saving a value already in force, passes. No published export, error code, settings key or/auth/configmember is added; nothing is widened.Metadata-protocol: none
Skills-docs: updated here —
content/docs/permissions/authentication.mdx(themfa_requiredrow) anddocs/HARDENING.md(the MFA row);skills/**teaches neithermfa_requirednorrequire_mfa, so no skill or eval moves.Why
Three should-fix items of the
CONTRACT_REVIEW_TIERreview of PR #30293 (record 6650579682, carried in 6651713006), filed as this card.bindAuthSettingsappliesmfa_required = trueas one refusable piece,{ mfaRequired: true, plugins: { twoFactor: true } }, so the requirement is never in force without its enrollment path. ButresolveTwoFactorEnabledreadsOS_AUTH_TWO_FACTORfirst, so underOS_AUTH_TWO_FACTOR=falsethe piece'splugins.twoFactor: truechanges nothing: the switch saved and read on, nobody could enroll, and the gate (which since Organization require-MFA can be turned on with two-factor off, and after the grace period everyone is locked out with no way to enroll #30201 holds only a user who can enroll) enforced nothing. Measured:OS_AUTH_TWO_FACTORis not a settings env override (theauthmanifest has no two-factor key; the override formfa_requireditself would beOS_AUTH_MFA_REQUIRED); it is read only byresolveTwoFactorEnabled, i.e. onplugins.twoFactor.require_mfais settable per organization with no condition.What changes
mfa-required-enrollment-predicate.tsregisters a settings write predicate onauth.mfa_required(SettingsService.registerWritePredicate, the seamallowed_ip_rangesandlocalization.currencyuse), frombindAuthSettings, beside theallowed_ip_rangespredicate and before the first settings pass. The settings service asks it before any row is written, only when the save changes the key. It refuses a change from off to on (stored toggles read asbindAuthSettingsreads them) whileresolveTwoFactorEnabled({ twoFactor: true })answers false, which is the gate's own decision point over the plugin config the piece applies, so exactly anOS_AUTH_TWO_FACTORvalue that reads as off. The refusal refuses the whole save (SettingsValidationError,SETTINGS_VALIDATION) with one field error atmfa_required,invalid_value, constraint{ twoFactor: false, overriddenBy: 'OS_AUTH_TWO_FACTOR' }, and a message in the caller's language (en, zh-CN, ja-JP, es-ES; others fall back to English) naming the env var and the remedy (set it to true or remove it, restart, then turn the switch on). The rule reads the env var alone, so a sibling key in the same save cannot let it through (no batch hole). A settings service without write predicates is said once atwarn, only while the pin makes it matter.GET /api/v1/auth/configservesfeatures.twoFactorfromresolveTwoFactorEnabled(this.config.plugins)(since Organization require-MFA can be turned on with two-factor off, and after the grace period everyone is locked out with no way to enroll #30201), the same function the gate, the plugin list and both refusals read, live per request.features.twoFactor: falsebeside a storedrequire_mfa = true(or a stored global switch) is exactly "required but not enforced", and it is spec-typed already (AuthFeaturesConfigSchema.twoFactor) and readable from page predicates (features.twoFactor, assys-user.page.tsdoes). So no/auth/configmember is added,packages/specis untouched, and the new test pins the reading on the refused path and on a switch stored on before the check. The UI half (Setup: organization 强制多因素认证 says nothing when two-factor is off, and the enrollment screen's refusal is an untranslated 404 #30295) reads it.mfa_requiredrow ofcontent/docs/permissions/authentication.mdxand the MFA row ofdocs/HARDENING.mdnow say the per-organizationrequire_mfaneeds two-factor enrollment on, that the global switch is refused underOS_AUTH_TWO_FACTOR=false, and (mdx) thatfeatures.twoFactor: falsereports a stored requirement as not enforced.OS_AUTH_TWO_FACTORunset or on, the switch saves and the pass turns the requirement and enrollment on together, as before. A switch saved on before this release is not touched (the settings service asks no predicate about a value already in force) and stays unenforced, whichfeatures.twoFactorsays.Tests
packages/plugins/plugin-auth/src/mfa-required-enrollment-predicate.test.ts, 7 cases, through the realSettingsServiceand the realAuthPlugin/AuthManagerboot (no data engine composed):OS_AUTH_TWO_FACTOR=false:setMany('auth', { mfa_required: true, mfa_grace_period_days: 3 })is refused, code + field + constraint asserted; nothing in the batch is stored; the runtime requirement,isTwoFactorEnabled()andfeatures.twoFactorstay off; saving off passes.PUT /api/settings/:namespaceroute answers 400SETTINGS_VALIDATIONwith the field error in zh-CN (OS_AUTH_TWO_FACTOR=0).true): the save is accepted and the pass turns the requirement, enrollment andfeatures.twoFactoron.features.twoFactorreads false.Ablations (each through
scripts/ablation-replace.mjs, anchor hit 1 → 0, restored to the HEAD blob,git diff HEADempty):auth-plugin.ts→ cases 1 and 2 red (the save went through), 2 failed / 5 passed.if (enrollable()) return undefined;→ both controls and the predicate case red, 3 failed / 4 passed.Acceptance notes
authmanifest'smfa_requireddescription ("Enabling this also turns on the two-factor feature so users can enroll",packages/services/service-settings/src/manifests/auth.manifest.ts) is outside this card's file surface. With the refusal it holds for every save that lands; under the pin the save is refused with the reason.OS_AUTH_MFA_REQUIRED=true(the settings env override of the switch itself) besideOS_AUTH_TWO_FACTOR=falseis a deployment contradiction no save door sees; the gate's once-per-process warning (Organization require-MFA can be turned on with two-factor off, and after the grace period everyone is locked out with no way to enroll #30201) names it. Not filed: no new reach.Verification
All readings on head
7e672312(this branch merged withorigin/mainatd1d90b70, which carries #30309), underscripts/pm/os-verify-lock.sh, shared box.Suites and typecheck
pnpm --filter '@objectstack/plugin-auth...' build && pnpm --filter @objectstack/plugin-auth test && pnpm --filter @objectstack/plugin-auth typecheck:VERDICT command-exit 0; Test Files 223 passed (223), Tests 3925 passed, 10 skipped (3935);check:test-typecheck: OK.git diff HEADempty.pnpm lint: exit 0.Full dogfood battery (owed by the narrowing arm)
pnpm --filter '@objectstack/dogfood^...' build && pnpm --filter @objectstack/dogfood test(no shard, no path):VERDICT command-exit 0, held the lock 27m01s; Test Files 419 passed, 3 skipped (422); Tests 3492 passed, 36 skipped (3528); no failure.Gates
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 95 commands on7e672312; each was run with its exit code recorded before any pipe.--ran: 95 derived, 94 run, 1 NOT-MEASURED, 0 UNRUN. NOT MEASURED:pnpm check:dual-build-cjs-loads(exit 3, PREREQUISITE NOT MET: it needs a whole-workspace build; CI runs it).check:skill-examplesfirst exited 3 (client packages unbuilt) and passed (exit 0, 283 examples) after building@objectstack/clientand@objectstack/client-react. Every other command exited 0.Generated by Claude Code