背景
写路径上「哪些字段由服务端托管、客户端不得自由写」的规则,目前散落成 plugin-security 中间件里的 N 个互相独立的特例 ,每个都是单独硬编码 + 单独执法:
owner_id(属主锚点,安全:owner_id(属主锚点)客户端可写、服务端无守卫 → 非属主可伪造/转移记录属主 #3004 / PR fix(security): 守卫 owner_id 属主锚点 + bulk 写按属主收敛 (#3004, #2982) #3018 ) — step 3.5 硬编码字段名 owner_id:insert 空值回填 acting user、伪造/转移/弃权需 allowTransfer/modifyAllRecords;非 scalar 拒绝、原型链防护、数组 change-set fail-closed;级联 set_null 经 __referentialFieldClear 豁免(属主守卫与级联 set_null 冲突:非特权删除 sys_user 时 owner_id 级联置空被 #3004 守卫拦截(级联中途失败) #3023 / PR fix(security): 级联 set_null 豁免属主转移守卫 (#3023) #3048 )。
公开表单托管集(安全:公开表单(publicFormGrant)提交绕过 owner_id 属主守卫 → 匿名可伪造属主 #3022 / PR fix(security): 公开表单不可再伪造服务端托管锚点(owner_id 等)(#3022) #3036 ) — 新增 @objectstack/spec/security 的 PUBLIC_FORM_SERVER_MANAGED_FIELDS(id / owner_id / organization_id / tenant_id / 审计列 / is_deleted / deleted_at / __search),在匿名公开表单路径逐行剥离 ;路由层白名单无条件排除。
organization_id(租户墙,ADR-0095 / security(authz): 多组织下伪造 organization_id 的 insert 可越租户墙 — Layer 0 未门控 insert post-image #2937 / fix(plugin-security): 堵跨租户 UPDATE 写 + org_admin private 对象越墙(security) #2946 ) — step 3.7 硬编码字段名 organization_id:供给值必须过 Layer 0 租户 CHECK,否则拒绝(防跨租户搬运)。
三处各自定义「托管字段是哪些、怎么执法」,彼此不共享抽象。
问题(为什么这是债)
提议(结构性,需 ADR)
把「服务端托管字段」提升为一个声明式 schema 概念 ,一处声明、一处执法:
在 packages/spec 的 Field/Object schema 上引入一个声明(草案命名,待 ADR 定稿),例如字段级 systemManaged: { onInsert: 'stampActor' | 'reject' | 'strip', onUpdate: 'reject' | 'strip' | 'immutable', requiresCapability?: string },或对象级的托管字段策略表。
owner_id → onInsert: stampActor(空值回填)、onUpdate: reject(除非 allowTransfer);
审计列(created_by 等)→ immutable/strip;
organization_id → 由租户层(@objectstack/organizations)贡献其托管策略(跨租户 CHECK);
公开表单托管集 → 从同一声明派生 (PUBLIC_FORM_SERVER_MANAGED_FIELDS 不再手维护)。
单一执法点 :中间件读该声明统一执行(insert 回填/拒绝、update 拒绝/剥离、能力门),已认证与匿名公开表单共用同一派生集合;__referentialFieldClear 这类引擎内部豁免也纳入统一模型。
explain 引擎据同一声明解释「为什么这个字段写不进去」。
范围与非目标
关联
背景
写路径上「哪些字段由服务端托管、客户端不得自由写」的规则,目前散落成
plugin-security中间件里的 N 个互相独立的特例,每个都是单独硬编码 + 单独执法:owner_id(属主锚点,安全:owner_id(属主锚点)客户端可写、服务端无守卫 → 非属主可伪造/转移记录属主 #3004 / PR fix(security): 守卫 owner_id 属主锚点 + bulk 写按属主收敛 (#3004, #2982) #3018) — step 3.5 硬编码字段名owner_id:insert 空值回填 acting user、伪造/转移/弃权需allowTransfer/modifyAllRecords;非 scalar 拒绝、原型链防护、数组 change-set fail-closed;级联 set_null 经__referentialFieldClear豁免(属主守卫与级联 set_null 冲突:非特权删除 sys_user 时 owner_id 级联置空被 #3004 守卫拦截(级联中途失败) #3023 / PR fix(security): 级联 set_null 豁免属主转移守卫 (#3023) #3048)。@objectstack/spec/security的PUBLIC_FORM_SERVER_MANAGED_FIELDS(id/owner_id/organization_id/tenant_id/ 审计列 /is_deleted/deleted_at/__search),在匿名公开表单路径逐行剥离;路由层白名单无条件排除。organization_id(租户墙,ADR-0095 / security(authz): 多组织下伪造 organization_id 的 insert 可越租户墙 — Layer 0 未门控 insert post-image #2937 / fix(plugin-security): 堵跨租户 UPDATE 写 + org_admin private 对象越墙(security) #2946) — step 3.7 硬编码字段名organization_id:供给值必须过 Layer 0 租户 CHECK,否则拒绝(防跨租户搬运)。三处各自定义「托管字段是哪些、怎么执法」,彼此不共享抽象。
问题(为什么这是债)
owner_id是系统托管锚点)在 step 3.5、PUBLIC_FORM_SERVER_MANAGED_FIELDS、以及未来任何新入口各写一遍。新增一个托管字段(例如created_by想在某入口强制),要在 N 处分别改。提议(结构性,需 ADR)
把「服务端托管字段」提升为一个声明式 schema 概念,一处声明、一处执法:
packages/spec的 Field/Object schema 上引入一个声明(草案命名,待 ADR 定稿),例如字段级systemManaged: { onInsert: 'stampActor' | 'reject' | 'strip', onUpdate: 'reject' | 'strip' | 'immutable', requiresCapability?: string },或对象级的托管字段策略表。owner_id→onInsert: stampActor(空值回填)、onUpdate: reject(除非allowTransfer);created_by等)→immutable/strip;organization_id→ 由租户层(@objectstack/organizations)贡献其托管策略(跨租户 CHECK);PUBLIC_FORM_SERVER_MANAGED_FIELDS不再手维护)。__referentialFieldClear这类引擎内部豁免也纳入统一模型。范围与非目标
关联
PUBLIC_FORM_SERVER_MANAGED_FIELDS)