Repository navigation
fix(plugin-auth,platform-objects): an organization's require-MFA refusal reads in the admin's language, a turn-on names the grace period, and the help says two-factor must be on (#30411) - #30529
Conversation
…aller's language, and an admitted turn-on answers the grace period in force (WIP) Claude-Session: https://claude.ai/code/session_01Utp6Cai3TgWyncLtR5EwF7 Co-authored-by: Claude <noreply@anthropic.com>
…r on (WIP) Claude-Session: https://claude.ai/code/session_01Utp6Cai3TgWyncLtR5EwF7 Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Utp6Cai3TgWyncLtR5EwF7 Co-authored-by: Claude <noreply@anthropic.com>
…re_mfa help (WIP) Claude-Session: https://claude.ai/code/session_01Utp6Cai3TgWyncLtR5EwF7 Co-authored-by: Claude <noreply@anthropic.com>
… grace-period pins (WIP) Claude-Session: https://claude.ai/code/session_01Utp6Cai3TgWyncLtR5EwF7 Co-authored-by: Claude <noreply@anthropic.com>
…tion in every locale (WIP) Claude-Session: https://claude.ai/code/session_01Utp6Cai3TgWyncLtR5EwF7 Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Utp6Cai3TgWyncLtR5EwF7 Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Utp6Cai3TgWyncLtR5EwF7 Co-authored-by: Claude <noreply@anthropic.com>
Brings #30449 (#30300) onto the branch before the PR, at the seat's request. Claude-Session: https://claude.ai/code/session_01Utp6Cai3TgWyncLtR5EwF7 Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 2 package(s): 9 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 6 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 23 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 d4f287be8aa2e231669e0cb999eb64a201b2c6d9 && git checkout d4f287be8aa2e231669e0cb999eb64a201b2c6d9
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin fa9c0aa2a0a8c2752f83b57765f3aae978ae196a f3b41646a354713e2abc1159ab6b01284a3d2018 && git checkout -B drift-repro fa9c0aa2a0a8c2752f83b57765f3aae978ae196a && git merge --no-ff f3b41646a354713e2abc1159ab6b01284a3d2018
node scripts/docs-audit/affected-docs.mjs --json fa9c0aa2a0a8c2752f83b57765f3aae978ae196a
|
|
domain:server#2 · os-bill · session_01Utp6Cbvcmv8YWFhFJwzjKL 维护者速读
席位核对 — ACCEPT · domain:server#2 · head
|
Conflicts: sys_organization.require_mfa help (source, en and zh-CN bundles) against #30529. Kept this branch's administrator wording, which already carries #30529's two-factor sentence; en confirmed by extract (byte-identical). Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Utp6CbDNK83SuSb8zsRt2s
Requested by Bill · project thread
domain:server#2 · os-bill · session_01Utp6Cbvcmv8YWFhFJwzjKL
维护者速读
notice字段):没绑定身份验证器的成员要在几天内绑定,天数就是当前生效的宽限期;require_mfa的字段说明四种语言都补上「需要本部署开启双重验证(OS_AUTH_TWO_FACTOR=true),关闭时保存为开会被拒」。manage_platform_settings,组织管理员(organization_admin)没有这个权限,所以界面只能写「默认 7 天」;字段说明也没说要先开双重验证。packages/plugins/plugin-auth/src/organization-mfa-enrollment-guard.ts(四语拒绝文案、保存提示及何时发出);auth-manager-password-session-policy.ts的resolveMfaGracePeriodDays(门禁读数不变);packages/platform-objects/src/identity/sys-organization.object.ts的require_mfa说明。pnpm --filter @objectstack/plugin-auth exec vitest run src/organization-mfa-enrollment-guard.test.ts,预期全部通过,其中真实引擎用例断言 zh-CN 管理员被拒时读到中文、保存成功时onNotice收到写着生效天数的中文提示。Fixes #30411
Clause-②: no
Metadata-protocol: none
What changes
The three Wanted items of the card, all on the organization write path (
sys_organizationinsert and update, every data door):organization-mfa-enrollment-guard.tsrefuses a write that turnsrequire_mfaon while two-factor enrollment is off, as before. The sentence now reads in the caller's language: the request'sAccept-Languagetag, which the engine hands every hook asctx.session.locale, matched by core'sresolveLocaleagainst the four languages the copy carries (exact, then by language, sozhreacheszh-CN, else English). The copy lives beside the guard, the same home the global switch's refusal (mfa-required-enrollment-predicate.ts) uses. The field-level shape is unchanged in every language:400 VALIDATION_FAILED, onerequire_mfa/invalid_valueentry whosemessageis the top-level one. The English sentence is byte-identical to before.authsettings namespace that holdsmfa_grace_period_daysis read throughGET /api/settings/auth, whosereadPermissionismanage_platform_settings(service-settings/src/manifests/auth.manifest.ts, enforced bySettingsService.assertPermittedfor every HTTP caller). The organization administrator who can edit the organization holdsorganization_admin(or its wall-less variant), whose system permissions aremanage_org_users,setup.access,setup.write,manage_mailboxesandmanage_connector_connections(plugin-security/src/objects/default-permission-sets.ts), so that read answers 403 for them. No read is added and/api/v1/auth/configis not touched. Instead, the card's own alternative: a save the guard admits that turns the requirement on (two-factor on, the stored value off, or an insert) callsctx.notice(message), which the create and update answers already carry as their optionalnoticemember (WRITE_ANSWER_NOTICE_DESCRIPTION,@objectstack/spec/api). The message says every member without an authenticator app must enroll, within the grace period in force in days (0 and 1 have their own sentences), in the caller's language. The number is read per write fromAuthManager.getMfaGracePeriodDays(), which asks the sameresolveMfaGracePeriodDaysthe MFA gate's clock now asks, so the saver is told the number the gate holds to. A write that turns nothing on (a clear, a re-save of a storedtrue, another column) answers no notice; a refused write answers its error.sys_organization.require_mfa's description says it needs two-factor authentication on for the deployment (OS_AUTH_TWO_FACTOR=true) and that saving it on is refused while it is off.en.objects.generated.tsis regenerated from the source withpnpm i18n:extract; the three translated values (zh-CN,ja-JP,es-ES) are re-translated by hand, which AGENTS.md's Documentation Guardrails allow for translated-locale values (keys unchanged, so no drift).The read the ui half should use for the grace period: the
noticemember of the update answer ofPATCH /api/v1/data/sys_organization/{id}(and of the create answer), which the console already receives asRecordSaveFeedback.noticethroughanswerWarningClaiminui/packages/app-shell/src/console/AppContent.tsx. It is present only on a save that turned the requirement on, and is already in the console's language.Files:
organization-mfa-enrollment-guard.tsand its test;auth-manager-password-session-policy.ts(the grace-period reading extracted, gate unchanged);auth-manager.ts(one@internalaccessor);auth-plugin.ts(one option at the guard's registration, outsideapplySettings);sys-organization.object.tsand the four*.objects.generated.ts; one changeset.Why
mainthe guard'sorganizationMfaWithoutEnrollmentRefusal()(fix(plugin-auth)!: an organization cannot require MFA while two-factor enrollment is off (#30201) #30293) took no locale and threw one English sentence whatever the caller's language, so a zh-CN administrator read a Chinese form and an English refusal under the field.pnpm dev -- --fresh, showcase, single posture; the settings read path is not touched by this diff): an organization administrator (organization_admin_no_bypass, made through the admin create-user and member-role doors) askingGET /api/settings/authgot403 SETTINGS_FORBIDDEN("requires the 'manage_platform_settings' capability"); the platform administrator got200. No read the organization administrator holds carriesmfa_grace_period_days, so the console can only write "7 days unless changed".require_mfa's description did not say that two-factor must be on.Tests
All in
organization-mfa-enrollment-guard.test.ts, as new top-leveldescribeblocks beside the existing ones:zh,zh-cn,zh-Hans-CN,ja,es-MX,fr-FR, empty), the same envelope in all four languages, and the hook answering inctx.session.locale(English without one);previous), its number, its zh-CN text, the 0-day and 1-day sentences in every language, silence for a clear, another column and a re-save of a storedtrue, and an admitted write with no notice member;sys_organizationbehind the D2 guard: a zh-CN administrator's refused save reads Chinese and the row keeps its value (control: no locale reads English); an admitted save hands the write'sonNoticelistener the zh-CN notice with the configured days, a changed grace period is read on the next write, and a re-save answers none;AuthPlugin's realkernel:readyregistration: the live manager's grace period reaches the notice, and a settings patch changes it on the next write;resolveMfaGracePeriodDaysoverundefined,0,14,2.9,-3,120, and, end to end on better-auth, a member whose clock started 30 days ago is held under a 20-day and the default period and not under a 40-day one, whilegetMfaGracePeriodDays()answers the same number.Ablations (each through
scripts/ablation-replace.mjsin wrap mode, restored, blob equal to HEAD after)Each mutation landed (anchor 1 → 0, blob changed), the guard test file went red as below, and the file was restored to its HEAD blob (
git diff HEADempty). The file is 56/56 green before and after.organizationMfaWithoutEnrollmentRefusal()withoutcallerLocale(ctx)): the real-engine zh-CN refusal and the hook-level locale case go red. 2 failed, 54 passed.true: five cases go red, among them "says nothing for a write that turns nothing on", the real-engine notice case and theAuthPluginregistration case. 5 failed, 51 passed.AuthPluginsettings-patch case go red. 2 failed, 54 passed.resolveMfaGracePeriodDays: theundefinedrow, the end-to-end unset case and the registration case go red. 3 failed, 53 passed. This is the pin that the gate's reading did not move.A first A2 attempt used an anchor that the replacement contained, so the anchor count did not drop and
ablation-replacerefused it before running anything (not a measurement). A2b used a different anchor. A1 to A4 ran before the commit that made the unset grace-period case configure none (f2c7a1fa, test file only); A5b and the green runs ran after it. The guard and policy blobs are unchanged since (862fa2ec,f7160fdb).Acceptance notes
noticeand then adds its own after-save line for this field (organizationMfaSavedNotice,console.organizationMfa.nowRequired, which names "7 days unless changed"). Once this lands, an accepted turn-on shows both. The ui follow-up is to show the server's notice instead of that line when the answer carries one; it is outside this diff (ui/**) and handed to the seat.@objectstack/clienthas no settings read either; not needed for this shape, and not touched.Verification
All readings are on head
f3b41646: this branch merged withorigin/mainf506b79d, which carries #30449. Heavy runs were underscripts/pm/os-verify-lock.shon a shared box.Real boot (pnpm dev, showcase, fresh database)
Two-factor off, signed in as the platform administrator:
PATCH /api/v1/data/sys_organization/{id} {"require_mfa":true}withAccept-Language: zh-CN→400 {"error":"本组织暂时无法强制多因素认证:本部署未开启双重验证的绑定,…","code":"VALIDATION_FAILED","fields":[{"field":"require_mfa","code":"invalid_value","message":"本组织暂时无法强制多因素认证:…"}],"object":"sys_organization"}.Accept-Language: en-US→400with the English sentence, byte-identical to before.require_mfafield help is served in zh-CN and en with the new sentence.Restarted with
OS_AUTH_TWO_FACTOR=true:Accept-Language: zh-CN→200, the record with"require_mfa":true, and"notice":"本组织现已强制多因素认证。尚未绑定身份验证器应用的成员有 7 天时间完成绑定,…".PUT /api/settings/auth {"values":{"mfa_grace_period_days":3}}, the column set back tofalse, then on again → the notice reads有 3 天, with no restart.true→200with nonoticekey.Suites, typecheck, lint
pnpm build --concurrency=2→VERDICT command-exit 0, 119/119 tasks.pnpm --filter @objectstack/plugin-auth test→ Test Files 224 passed (224), Tests 4001 passed, 10 skipped (4011).pnpm --filter @objectstack/platform-objects test→ Test Files 111 passed (111), Tests 1532 passed (1532).pnpm --filter @objectstack/plugin-auth typecheckandpnpm --filter @objectstack/platform-objects typecheck→VERDICT command-exit 0.src/organization-mfa-enrollment-guard.test.tsalone → 56/56.node scripts/check-i18n-bundles.mjs→ OK.pnpm lint→VERDICT command-exit 0.Full dogfood battery
pnpm --filter @objectstack/dogfood test(no shard, no path), after the whole-workspace build above, onf3b41646→VERDICT command-exit 0, held the lock 29m31s. Test Files 419 passed, 3 skipped (422). Tests 3492 passed, 36 skipped (3528). No failure. The same battery on the pre-merge heade950155egave the same counts.Gates
node scripts/pm/dispatch-gates.mjs --commandsderived 66 commands onf3b41646. Each was run with its exit code recorded before any pipe, and all 66 exited 0.--ran: 66 derived, 66 run, 0 NOT-MEASURED, 0 UNRUN.On the pre-merge head
e950155e, two commands were non-zero, neither a finding:check-plugin-teardown-shape --self-test: exit 3, its pinned fixture commit missing from the shallow clone. It passed aftergit fetch --depth=1of that commit.check:type-check-debt: exit 3, NOT MEASURED, because its closure build never got a turn on the verify lock while this branch's own dogfood held it. It exited 0 on the final head.Human Minimum
packages/specholds no runtime logic. (No spec change.)pnpm lintpasses.main, one pull request.packages/spectouched: n/a.content/docs/references/,packages/*/CHANGELOG.mdorcontent/docs/releases/.🤖 Generated with Claude Code
https://claude.ai/code/session_01Utp6Cai3TgWyncLtR5EwF7