Repository navigation
sys_organization's platform-owned columns are engine-writable but no product surface reaches them — the data door answers 405 (apiMethods: ['get', 'list']) and better-auth's update endpoint does not carry them #15873
Description
Activity
- addedenhancementNew feature or requestNew feature or request
on Sep 6, 2026 分诊:
domain:services+domain:engine/enhancement+finding/needs-user-decision/priority:p3⛔
needs-user-decision不与pm:*并存(先例 #15854 / #15617 / #15542)。
✅ 双 lane:三支修法分属两个包(见下)。落点核实(
origin/main,本轮实读)packages/platform-objects/src/identity/sys-organization.object.ts:337 apiMethods: ['get', 'list'], packages/plugins/plugin-auth/src/managed-extension-fields.ts:87 sys_organization: new Set([ packages/plugins/plugin-auth/src/managed-extension-fields.ts:88 // ADR-0069 D3 — per-org MFA tightening above the global floor.⇒ 两侧都成立:数据门只开
get/list(写在 REST 边界被 405 挡掉,rest-server.ts:1699),而引擎路径上MANAGED_EXTENSION_EDITABLE_FIELDS.sys_organization把这几列开给了 user-context 调用方。⭐ 而
managed-extension-fields.ts:18-22的档头把这个设计意图写得很清楚:sys_organization.require_mfa, and the ADR-0105 D6 group-structure fields. better-auth never reads or writes these, so they can be edited through the ordinary path under normal FLS /requiredPermissions. … A field is an extension field because better-auth does not write it, not …⇒ 档头声称「可以走普通路径编辑」,而普通路径(REST 数据门)在这个对象上被 405 挡死。 这正是卡片指认的那条断裂:声明说可编辑,可达性说不可达。
⚠️ 本席未复核卡片的两条辅助读数:update_organization行动作只带name/slug/logo;以及git grep require_mfa -- packages/rest/src packages/runtime/src packages/apps packages/qa= 0。⛔ 不要把上面的核对当成对它们的复现继承下去。
为什么是决定箱
卡片自己把它定性得很准,本席复核后同意:
Unassigned — for triage; it may be a documented posture rather than a defect, and that is the decision.
⇒ 「四列只能由 system-context 调用方设置(插件、flow、CLI seed、SQL)」这件事,既可能是有意的姿态,也可能是一次遗漏——而两者的处置完全相反。⛔ 且它触及 ADR-0092 / #1591 对身份表方法门的既有决定,卡片明说 #14238 的席位刻意没碰
apiMethods,因为那在裁决 A 的范围之外。⛔ 本席不裁决(本会话
claude-opus-5,CONTRACT_REVIEW_TIER硬门要求 fable)。四facet
① 事实(已核)
sys_organization的enable.apiMethods=['get','list'](其自身注释:#1591,只读,写由身份写守卫拒绝、归 better-auth 所有,「HTTP answers 405 before the 403」);rest-server.ts:1699的非空白名单以 405 拒绝未列操作 ⇒PATCH /api/v1/data/sys_organization/ID永不到达引擎。同时MANAGED_EXTENSION_EDITABLE_FIELDS.sys_organization在kernel:ready注册为守卫的按对象更新白名单,把require_mfa/parent_organization_id/sort_order(以及 #14238 落地后的timezone)开在引擎路径上。better-auth 的organization/update只带name/slug/logo。⇒ 四列只有 system-context 可写。② 分叉(卡片列出,本席原样转达并标注 lane)
- (a) 在
sys_organization.apiMethods里放开update,让 D2 白名单做列级把关——白名单正是为此存在的,better-auth 自己的列仍被剥离。⇒ 落packages/platform-objects⇒domain:engine。 - (b) 为这些平台自有列开一条专用管理路由。⇒ 落
packages/rest或packages/plugins/plugin-auth⇒domain:cli/domain:services。 - (c) 声明姿态:这些列只允许系统写,并把它们从
MANAGED_EXTENSION_EDITABLE_FIELDS里删掉,让声明与可达性一致。⇒ 落packages/plugins/plugin-auth⇒domain:services。
③ 各支的代价
- (a):最省事,且复用既有机制;
⚠️ 但它改变一张身份表的方法门——那是 ADR-0092 / managedBy is not enforced: generic CRUD bypasses better-auth on sys_team (data-integrity / security) #1591 的地盘,且 managedBy is not enforced: generic CRUD bypasses better-auth on sys_team (data-integrity / security) #1591 的注释明确写了「写被拒绝、归 better-auth 所有」。⇒ 需要说明为什么那条理由对这四列不适用。 - (b):不动方法门;代价是新增一条路由面,且要回答它的权限模型。
- (c):唯一让声明与现实一致而不新增任何东西的一支;代价是承认这四列(含刚落地的
timezone)不能由管理员通过产品界面设置——而 No platform object carries a timezone, so every app that computes a date boundary has to invent one — and each will invent it differently #14238 的裁决把timezone描述为一个管理员设置的值。⚠️ 这个矛盾是本卡最该被裁决者看到的一点:走 (c) 就等于说 No platform object carries a timezone, so every app that computes a date boundary has to invent one — and each will invent it differently #14238 的那句描述需要修正。
④ 需要裁决者提供的东西
一句话:sys_organization上的平台自有列,是否应当由管理员通过产品界面设置?
答「是」⇒ (a) 或 (b);答「否,只允许系统写」⇒ (c),并请一并确认 #14238 对timezone的描述如何修正。定级理由(其余)
enhancement而非bug:main 上没有东西是假的——405 是按声明发生的,白名单也是按声明注册的;错的是两个声明合起来给出一个不可达的能力。任一修法都是改变已发布的可达面 ⇒ Feature 侧,人工地板,与决定箱相符。- p3:无运行期故障、无数据风险、无安全泄漏(方向是更严:能力宣称有而实际没有)。⛔ 不降更低:它正是 ADR-0049 / ADR-0078 明确要挡在外面的「declared-but-unusable」形状,除非它是有意的姿态——而那正是本卡要问的。
⚠️ 卡片明说未测的两项,本席原样转达为取卡前置- Setup app(objectui 侧)是否真的为这些列渲染了一个组织编辑表单——若渲染了,它会撞上那个 405。⇒ 这是最可能已经有用户在踩的路径,且它会把本卡从「声明不一致」变成「一个界面按钮永远失败」。⛔ 本席不跨仓测。
- 是否有任何 dogfood 测试跑过对
sys_organization的 user-context 写(卡片的 grep 没找到)。
已发布面(卡片已测):
sys_organization的apiMethods在构建出的packages/platform-objects/dist/identity/index.js/index.mjs里;content/docs/**未被测量——⭐ 卡片明说了这一点,⇒ ⛔ 不要把「文档没说」当成读数。Refs:#14238(本卡的来源)· ADR-0092 D2(身份写守卫)· #1591(方法门的既有决定)· ADR-0069 D3 · ADR-0105 D6 · ADR-0049 / ADR-0078(declared-but-unusable)。
⛔ 本席为 triage 席位:不认领、不派单、不写码、不合并、不裁决 decision-box。
Generated by Claude Code
- (a) 在
Ruling recorded — option (a): administrators set the platform-owned columns through the product; the data door admits
updateand the D2 whitelist gates the columns (director seat, decision batch #64, 2026-09-07)Maintainer reply, verbatim: 「同意」 (all five batch #64 recommendations adopted).
Answer to the card's question. Yes —
require_mfa,parent_organization_id,sort_orderandtimezoneonsys_organizationare values an administrator sets through the product surface (this confirms #14238's description oftimezone). Option (c) — declaring them system-writable only — is refused: it would make the product unable to set an organisation's timezone or MFA posture.Execution notes.
packages/platform-objects/src/identity/sys-organization.object.ts:enable.apiMethodsgainsupdate. The ADR-0092 D2 identity write guard withMANAGED_EXTENSION_EDITABLE_FIELDS.sys_organizationis the column gate; better-auth's own columns (name,slug,logo, and everything else not on the whitelist) stay stripped/refused on that path and continue to go through better-auth'sorganization/update. Update the managedBy is not enforced: generic CRUD bypasses better-auth on sys_team (data-integrity / security) #1591 comment: its "writes owned by better-auth" reasoning holds for better-auth's columns, which the whitelist still protects; it never covered the platform-owned extension columns.- Verify the refusal ordering after the change: an attempt to write
namethrough the data door must be refused by the guard (403 with the D2 code), not silently ignored. - Precondition reading, recorded here before landing: does the objectui Setup app already render an organisation edit form for these columns? If yes, it starts working; if no, a follow-up objectui card adds it (the director will not file it blind).
- Clause-② yes (a published method gate on an identity table widens): landing PR carries
needs:contract-review; changeset states the change.
Labels:
needs-user-decision→pm:queue; lanedomain:engine(theplatform-objectschange),domain:servicesdropped. Ledger on #12708 (batch #64).
Generated by Claude Code
State restored to the recorded ruling —
needs-user-decision→pm:queue(director seat, summon #17,session_01XesLUWmuhjuRwmU618AZ1M, 2026-09-07T14:1xZ)This card was ruled at 02:52Z today: option (a) — administrators set the platform-owned columns through the product; the data door admits
updateand the D2 whitelist gates the columns (director seat, decision batch #64, comment 5564369547; maintainer reply, verbatim: 「同意」). That ruling explicitly refused option (c). The 12:08Z comment (5570393087) re-presented A/B/C to the maintainer recommending (c) and moved the card back into the decision inbox; it does not cite the ruling and the ruling was not overturned by the maintainer, so it is a duplicate presentation of a decided question, not a reopening. A recorded ruling stands until the maintainer overturns it — the director's ledger on objectstack#12708 is the authority for its existence.Executed now:
needs-user-decision→pm:queuein one label write (read back), assignee clear. Thedomain:enginelane dispatches under ruling (a) — the answer to the card's question is "yes, through the product", and #14238's description oftimezonestands. If the maintainer wants (c) after all, that is a new ruling to record here, not a re-presentation.
Generated by Claude Code
- added a commit that references this issue
on Sep 9, 2026 - added a commit that references this issue
on Oct 7, 2026
Found while landing #14238's
sys_organization.timezone— the root default of the business-unit timezone chain, which the ruling describes as a value an administrator sets. The column follows the precedent every earlier platform-owned column on that object follows (require_mfa,parent_organization_id,sort_order): registered in plugin-auth'sMANAGED_EXTENSION_EDITABLE_FIELDSso the ADR-0092 D2 identity write guard admits it. This card is about where that precedent leads. Unassigned — for triage; it may be a documented posture rather than a defect, and that is the decision.The reading
Measured on
origin/mainat7b6825477:packages/platform-objects/src/identity/sys-organization.object.ts—enable.apiMethods: ['get', 'list'](its own comment: managedBy is not enforced: generic CRUD bypasses better-auth on sys_team (data-integrity / security) #1591, reads only, writes refused by the identity write guard and owned by better-auth; "HTTP answers 405 before the 403").packages/rest/src/rest-server.ts:1699— a non-emptyenable.apiMethodswhitelist rejects unlisted operations with 405 on the REST data surface, the external API boundary. SoPATCH /api/v1/data/sys_organization/IDnever reaches the engine.packages/plugins/plugin-auth/src/managed-extension-fields.ts—MANAGED_EXTENSION_EDITABLE_FIELDS.sys_organization=require_mfa,parent_organization_id,sort_order(andtimezoneonce No platform object carries a timezone, so every app that computes a date boundary has to invent one — and each will invent it differently #14238's PR lands), registered atkernel:readyas the guard's per-object update whitelist (auth-plugin.ts, the D7 loop). This opens the columns on the engine path for user-context callers — a path the data door does not let a user-context caller reach.update_organizationrow action targets better-auth'sorganization/update(bodyShape: { wrap: 'data' }) with paramsname,slug,logoonly; the extension fields are deliberately not better-authadditionalFields(theai_access/localenotes inauth-manager.ts), so that endpoint does not carry them either.require_mfaoutside plugin-auth's own read site (auth-manager.ts):git grep -n require_mfa -- packages/rest/src packages/runtime/src packages/apps packages/qa= 0.So the four columns are settable only by a system-context caller — a plugin, a flow, a CLI seed, SQL. They are declared generically editable and reachable from no product surface, which is the declared-but-unusable shape ADR-0049 / ADR-0078 keep out unless it is the intended posture.
Not measured
Whether the Setup app (objectui side) renders an organization edit form for these columns at all; if it does, it meets the 405 above. Whether any dogfood test exercises a user-context write to
sys_organization(none found by the grep above).Fix space (a decision, not a prescription)
updateinsys_organization.apiMethodsand let the D2 whitelist do the column gating — the whitelist already exists for exactly this, and better-auth's own columns stay stripped;MANAGED_EXTENSION_EDITABLE_FIELDSso the declaration matches the reach.The #14238 seat did not touch
apiMethods: the method gate on an identity table is an ADR-0092 / #1591 decision, outside ruling A's scope.Published surface (measured)
sys_organization'sapiMethodsis in the builtpackages/platform-objects/dist/identity/index.js/index.mjs;content/docs/**was not measured for this observation.