Repository navigation
Closed
Labels
Description
背景
写路径上「哪些字段由服务端托管、客户端不得自由写」的规则,目前散落成 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,否则拒绝(防跨租户搬运)。
三处各自定义「托管字段是哪些、怎么执法」,彼此不共享抽象。
问题(为什么这是债)
- 漂移风险:同一个概念(
owner_id是系统托管锚点)在 step 3.5、PUBLIC_FORM_SERVER_MANAGED_FIELDS、以及未来任何新入口各写一遍。新增一个托管字段(例如created_by想在某入口强制),要在 N 处分别改。 - 入口覆盖不均:已认证写路径(step 3.5/3.7)与匿名公开表单(安全:公开表单(publicFormGrant)提交绕过 owner_id 属主守卫 → 匿名可伪造属主 #3022)是两套独立实现;两者互补但没有单一事实源,一个入口补了、另一个可能漏(安全:公开表单(publicFormGrant)提交绕过 owner_id 属主守卫 → 匿名可伪造属主 #3022 正是这样被单独发现的)。
- 契约不透明:「这个字段服务端托管」是对象/字段的语义属性,却只存在于运行时中间件的 if 分支里,authoring/spec 层看不到、explain 引擎也无法统一解释。违反 Prime Directive Add comprehensive test suite for Zod schema validation #12(contract-first:把约束放在正确的层、声明式表达)。
提议(结构性,需 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 引擎据同一声明解释「为什么这个字段写不进去」。
范围与非目标
- 结构性重构,需先起 ADR 定字段/对象声明的形状、迁移路径(现有三处特例逐步收敛到声明,保留行为等价),再分阶段落地。
- 非目标:改变现有任一特例的运行时行为(安全:owner_id(属主锚点)客户端可写、服务端无守卫 → 非属主可伪造/转移记录属主 #3004 / 安全:公开表单(publicFormGrant)提交绕过 owner_id 属主守卫 → 匿名可伪造属主 #3022 / 属主守卫与级联 set_null 冲突:非特权删除 sys_user 时 owner_id 级联置空被 #3004 守卫拦截(级联中途失败) #3023 / ADR-0095 的语义保持不变),只是把它们统一到一个声明式来源之下。
关联
- 安全:owner_id(属主锚点)客户端可写、服务端无守卫 → 非属主可伪造/转移记录属主 #3004 / PR fix(security): 守卫 owner_id 属主锚点 + bulk 写按属主收敛 (#3004, #2982) #3018(owner_id 守卫)、属主守卫与级联 set_null 冲突:非特权删除 sys_user 时 owner_id 级联置空被 #3004 守卫拦截(级联中途失败) #3023 / PR fix(security): 级联 set_null 豁免属主转移守卫 (#3023) #3048(级联豁免)、fix(security): bulk 写谓词守卫按调用者谓词判定 + pre-image 读取去重(#3018 审查后续) #3053(守卫后续加固)
- 安全:公开表单(publicFormGrant)提交绕过 owner_id 属主守卫 → 匿名可伪造属主 #3022 / PR fix(security): 公开表单不可再伪造服务端托管锚点(owner_id 等)(#3022) #3036(公开表单托管集
PUBLIC_FORM_SERVER_MANAGED_FIELDS) - ADR-0095 / security(authz): 多组织下伪造 organization_id 的 insert 可越租户墙 — Layer 0 未门控 insert post-image #2937 / fix(plugin-security): 堵跨租户 UPDATE 写 + org_admin private 对象越墙(security) #2946(organization_id 租户墙)
- 授权总跟踪 tracking(authz): 双轨赛道下的授权改进开发任务 v2(内核链 ADR 待编号 + 补齐轨 + 地基) #2920
- 来源:PR fix(security): 守卫 owner_id 属主锚点 + bulk 写按属主收敛 (#3004, #2982) #3018 对抗式代码审查的 altitude 结论(owner_id 守卫是「共享基础设施上的特例」,应提炼底层机制而非叠加特例)。
Activity
os-zhuang commentedon Jul 18, 2026
架构评估:声明式
systemManaged抽象暂缓,先收敛漂移与补齐测试(PR #3168)探过三处特例(step 3.5
owner_id、step 3.7organization_id、匿名公开表单PUBLIC_FORM_SERVER_MANAGED_FIELDS)的现状后,结论是:本 issue 提议的完整声明式抽象目前尚不划算,而它真正点出的债可以用更轻的手段收掉。为什么暂缓做
systemManaged声明层onInsert/onUpdate策略机器。__referentialFieldClear豁免)/ 租户 Layer 0 CHECK / 匿名逐行剥离。做成枚举后引擎里仍是那三段代码,只是多一层查表分发——名义统一,却要搬动经过对抗式审查的安全关键代码。PR #3168 的右尺寸做法(零运行时行为变更)
PUBLIC_FORM_SERVER_MANAGED_FIELDS钉成一个精确划分——「开核实际注入的字段(从applySystemFields动态枚举 +__search+id)」∪「有文档的纵深防御保留名(tenant_id/is_deleted/deleted_at)」。新增一个注入的系统字段若没同步进名单,会在此处报错并给出可操作提示,而不再是匿名面上的静默写洞。isSystem豁免)。此前只有读侧 Layer 0 与 dogfood 覆盖。systemFields的过期 JSDoc/describe 对齐 registry(organization_id列无条件注入、仅索引受多租户门控;audit是已接线的 opt-out;owner_id由ownership属性治理而非owner键)。顺带发现(未在本 PR 改)
readonly现已在数据写入 ingress 上于 INSERT 也剥离。原计划的这条后续因此在 main 上已失效。schema.ownership存在命名冲突:packages/cli/.../info.ts按'own'|'extend'(元数据归属)读,registry.ts的applySystemFields按'user'|'org'|'none'(记录归属模型)读。这正是本 PR 不把ownership声明进ObjectSchema的原因——那会强行提前拍板一个真实的双语义冲突。建议单开 issue 处理。重启本 issue 的触发条件(满足其一再起 ADR-0100)
在此之前,
owner_id/organization_id两个字段的守卫已由 step 3.5/3.7 覆盖,漂移由上面的一致性测试兜住。Generated by Claude Code
test(security): pin server-managed field set + tenant-wall write guar…