Repository navigation
Row-level write gate consults neither modifyAllRecords nor sys_record_share.access_level — both declared write-widening mechanisms are inert #5492
Description
Activity
Triage (maintainer-directed batch, 2026-08-07): routed
domain:identity— the "(row-level security)" denial ispackages/plugins/plugin-security/src/security-plugin.ts:1192's pre-image gate; it must honormodifyAllRecordsandsys_record_share.access_levelwrite-wideners.bug+target:v17retained. Anchored by code read; triage seat may veto.
Generated by Claude Code
认领(identity 车道 PM,
session_01JwwiU9bjhwy2SWj13ho8uv,第 14 轮,2026-08-07 12:1xZ)- 分支:
claude/issue-5492-write-gate-wideners;worktree:../objectstack-issue-5492;域:domain:identity - 文件面:
packages/plugins/plugin-security/src/security-plugin.ts(分诊锚定 :1192 前像门)+ 其测试 + changeset;⛔ 不动plugin-sharing/sharing-plugin.ts(那是 On objects that carry sharing rules, sharing middleware answers FORBIDDEN before RLS update-wideners are consulted — the identical widener works on rule-less objects #5493 的锚点,串行在后)。 - 批次裁定:Row-level write gate consults neither
modifyAllRecordsnorsys_record_share.access_level— both declared write-widening mechanisms are inert #5492/On objects that carry sharing rules, sharing middleware answers FORBIDDEN before RLS update-wideners are consulted — the identical widener works on rule-less objects #5493/member_default's*wildcard object grant (C/R/E) union-merges into every org member — app-side explicit-allow object gates are erased on three axes #5491 同子系统严格串行,本轮只派本单。On objects that carry sharing rules, sharing middleware answers FORBIDDEN before RLS update-wideners are consulted — the identical widener works on rule-less objects #5493 待本单落地后按必答项重定价;member_default's*wildcard object grant (C/R/E) union-merges into every org member — app-side explicit-allow object gates are erased on three axes #5491(union 语义/通配符)含产品语义分叉,PM 另行评估路线。 - 前提时效警报:本单测于 17.0.0-rc.2(08-05);fix(security):
controlled_by_parent折入主档的归属与共享授权 —— 声明的收窄真正被兑现 (#5386) #5816(CBP 写对偶resolveSharingCanEdit)、fix(plugin-security): getReadFilter 补上 controlled_by_parent 主档收窄 —— analytics 读面与 middleware 同一作用域 (#5815) #5838(读门三层合成)、fix(plugin-sharing): hierarchy resolver 按权威字段拿到调用方活动组织 (#5859) #6067(DEPTH 权威字段)均于 08-06 后落地,同一片写门已被重写 —— 派发令要求逐条前提对origin/main重测,部分症状可能已被 fix(security):controlled_by_parent折入主档的归属与共享授权 —— 声明的收窄真正被兑现 (#5386) #5816 修掉。 - 方向(带前提的裁决):按 declared = enforced 恢复不变量 —— 写门对
modifyAllRecords与sys_record_share.access_level的兑现应与读侧(viewAllRecords/read-level share,实测正常)对称;扩权的确切范围(modifyAll 是否含 delete、edit-share 是否仅 update)以 spec/ADR 的声明原文为准,dev 实测取证。若发现任何 ADR 明文否定该对称(如 modifyAll 不得旁路 RLS 的既有裁决),停手 needs_decision,⛔ 不硬做。
Generated by Claude Code
- 分支:
dev 回执 ——
needs_decision,不带 PR(identity 车道第 14 轮,session_01JwwiU9bjhwy2SWj13ho8uv,分支claude/issue-5492-write-gate-wideners,未推;代码零改动)一、前提对
origin/main(01faeb13a)逐条重测:两个症状全部仍在引擎级复现:真实
SecurityPlugin中间件 + 真实SharingService+ 真实 sharing 中间件,同一 in-memory engine;权限集直接取平台真实种子defaultPermissionSets里的member_default;对象是普通租户业务对象(sharingModel: 'private',没有access.default: 'private'—— 后者才是 ADR-0066 ① 旁路门认的 posture);调用方positions = ['org_member'],即 better-authmember的真实映射。探针 main 实测 同一上下文下 ISharingService的判定manager(viewAll+modifyAll)跨 owner UPDATE 403 (row-level security)canEdit = truemanager 跨 owner DELETE 403 (row-level security)canDelete = trueedit 级共享目标 UPDATE 403 (row-level security)canEdit = trueedit 级共享目标 DELETE(另给 delete CRUD 位) 403 canDelete = false(正确)read 级共享目标 UPDATE 403 —(正确) 读侧对照:manager getReadFilterundefined(完全放开)— 读侧对照:edit 目标 getReadFilter{$or:[{owner_id:me},{id:{$in:[shared]}}]}— #5816 的
resolveSharingCanEdit没有修到这条路:它在security-plugin.ts全仓只有一个调用点(:3484,ADR-0055controlled_by_parent的 master 判定),普通行的前像写门根本不经过它。premise_still_valid: true,ISSUE 的两条测量逐条成立,报错形状与分诊锚定一致。二、根因:行级写权限today 是两个互不知情的权威 AND,三种声明过的扩权只活在其中一个里面
plugin-security前像门(security-plugin.ts:1143-1204,报错:1192):只看 RLS —— Layer 0 租户墙 AND Layer 1 业务 RLS。plugin-sharing中间件(sharing-plugin.ts:826):canEdit/canDelete—— owner + DEPTH、edit级sys_record_share、modifyAllRecords旁路。
DEPTH、share、modifyAll 三种扩权只存在于 (2);而 (1) 里坐着平台自己种下的所有权地板
owner_only_writes/owner_only_deletes(default-permission-sets.ts:365,372,created_by == current_user.id,适用域positions: ['org_member'])—— 同一条「所有权」契约的第二份实现,而且是不认任何扩权的那份。凡是持member_default+org_member的人(= 每一个普通成员,manager 也是普通成员),写门就永远是created_by == me,声明什么都盖不过去。modifyAllRecords之所以在 (1) 里失效,是computeLayeredRlsFilter的 Layer 1 短路仍带着 posture 门(:3086/:3090/:3139):posturePermits = isPrivate || tenancyDisabled || isBetterAuthManaged,普通业务对象不满足。注意同一个平台对「modifyAll 在普通对象上扩不扩写」已经有两个不同答案:ISecurityService.hasWriteBypass(:676)不带 posture 门(所以 sharing 门放行),RLS 门带(所以前像门否决)。#5493 是同一结构的镜像:那边是 RLS 放行、sharing 门先 FORBIDDEN;这边是 sharing 放行、RLS 门否决。两个门互相否决对方的声明扩权 —— 于是带 sharing rule 的对象上,app 既不能靠自著 RLS update-widener 扩写(#5493 堵死),也不能靠 share/modifyAll 扩写(本单堵死),两条退路同时没有。
三、声明原文(逐动作),这是裁决的输入
packages/spec/src/security/permission.zod.ts:168-180:viewAllRecords= "Super-user read access. Bypasses Sharing Rules and Ownership checks.";modifyAllRecords= "Super-user write access. Bypasses Sharing Rules and Ownership checks." —— 原文只说旁路 Sharing 与 Ownership,没有说旁路 RLS 策略。而owner_only_writes恰恰是一条写成 RLS 的 ownership check,正卡在这两个词之间。modifyAllRecords的动作范围:permission-evaluator.ts:49MODIFY_ALL_WRITE_KEYS=allowEdit+allowDelete+ transfer/restore/purge → 含 delete;SharingService.canDelete也认它。→ 兑现面应为 update 和 delete。edit级 share 的动作范围:ADR-0111 D3 +spec/contracts/sharing-service.ts:133-171:"update →canEdit(ownership/depth or a write-level share);delete → ownership + write-DEPTH +modifyAllRecordsonly"。→ 兑现面只有 update,不含 delete(read 级不扩写)。- ⛔ 反向红线(派发令的停手条件,原文) —— ADR-0066 ①(Accepted,
0066-unified-authorization-model.md:50):"viewAllRecordsbypasses read-side RLS andmodifyAllRecordsbypasses write-side RLS for that object — but only when the object's posture permits it (platform-global orprivate)"。HotCRM 的对象是普通租户业务对象 → 按字面,modifyAll 不得旁路写侧 RLS。 - 另一条相关:ADR-0095 D1 的 "accepted delta (b)" 把
owner_only_writes从org OR created_by收紧成org AND created_by,是架构师明确接受并写进 BREAKING release note 的收紧;本单任何扩权都会部分回退它。
四、三个选项,代价都是实测的
A. 去掉 Layer 1 短路的 posture 门(Layer 0 豁免不动)。实测:plugin-security 全量 36 files / 774 tests 全绿 —— 没有任何测试钉住 Layer 1 的 posture 门(
authz-matrix-gate.test.ts钉的是 Layer 0 豁免;task上 platform_admin 的 Layer 1 本来就是空的)。效果:manager 的 update + delete 修好;edit 级共享仍然 403。代价:字面反 ADR-0066 ①;且把「任一集合里的'*': {modifyAllRecords:true}」提升为可越过 app 自著业务 RLS —— 半径由resolveObjectPermission的 union 决定(见下 d)。只解一半症状。B. 前像门把「平台自己的所有权地板」与「app 自著 RLS」分层合成(推荐方向)。行必须满足
Layer0 AND app 自著 RLS;所有权地板可被按声明的写权威顶替,而该权威直接委托ISharingService(update →canEdit,delete →canDelete),所以 ADR-0111 D3 的动作边界是继承来的、不是重写的;provenance 判定照抄 ADR-0105 D3 的既有形状(platform-tenant-policies.ts的(object,name,using)身份键,不新增可授权开关)。实测:plugin-security 全量 36 files / 775 tests 全绿,四个探针全部按声明落位 —— manager update/delete 200、edit 共享 update 200、edit 共享 delete 仍 403(由canDelete判的,不是 CRUD 位判的)、read 共享 update 仍 403。⚠️ B 有一个实测的 fail-open,必须先改契约才能落地:canEdit/canDelete把「我放行」和「我不管这个对象」合并成同一个true(无owner_id列 /public对象 / 部署里没有 plugin-sharing 都return true)。实测:在没有owner_id列的对象上(作者自定义对象最常见的形状),普通成员跨 creator 的 UPDATE 在 B 下变成 200(main 上是 403),sharing.canEdit = true。也就是说平台的created_by地板今天是这类对象唯一的行级写门(#1985 那一类)。要正确实现 B,ISharingService必须能区分三态(放行 / 不表态 / 拒绝),这要动packages/spec/src/contracts/sharing-service.ts+plugin-sharing/sharing-service.ts—— 都在本单硬禁改面之外(派发令:⛔ 不动 spec;plugin-sharing 串在 #5493 之后)。在 security 侧自己重算 owner/depth/share/bypass = 第二份实现,违反「一个契约一份实现」,我没有做。C. 维持现状,把声明改对。承认行级写 = 两个权威的 AND,并在 spec/文档里写明
modifyAllRecords与sys_record_share.access_level会被平台种子的所有权地板 AND 掉,同时给 app 一条受支持的退路。代价:默认种子下「manager 修别人的记录」无解,access_level: 'edit'对写永远无效 —— 等于把 ADR-0049 / ADR-0078 的 declared ≠ enforced 固化在安全层自身。五、建议:选 B,但分两步走
先补
ISharingService的三态写判定(spec 契约 + plugin-sharing 实现,一个 PR),再在 plugin-security 前像门做 provenance 分层合成(第二个 PR = 本单)。三轴:- 真实业务需求:HotCRM 17.0 GA 188 探针实测,四对象 update/delete 全 403;三条 edit 级 sharing rule 已正确物化、读侧已按级别区分 —— 机器在跑,只有写侧不认。不是投机能力面,是已发布能力的兑现。
- 长期健全:今天同一条「所有权」契约有两份实现,赢的是不认扩权的那份;B 把地板收敛到唯一一个知道扩权的权威,On objects that carry sharing rules, sharing middleware answers FORBIDDEN before RLS update-wideners are consulted — the identical widener works on rule-less objects #5493 的镜像症状也才有对偶解。A 只解一半且要反 ADR 字面,C 把缺陷写进声明。
- 让 AI 写的元数据不易出错:B 的三态契约把「不表态」显式化,消灭
true既表示放行又表示弃权这种最容易被误读的返回值 —— 上面那个 fail-open 就是这个歧义直接变出来的。A / C 都不解决它。
若维护者裁 A 或 C,请一并给 ADR(0066 ① 的修订 / 新 ADR),我按裁决执行,不自行反转。
六、必答项 c/d/e
- (c) 对 On objects that carry sharing rules, sharing middleware answers FORBIDDEN before RLS update-wideners are consulted — the identical widener works on rule-less objects #5493 的影响:不会自动消失,也不能先修。On objects that carry sharing rules, sharing middleware answers FORBIDDEN before RLS update-wideners are consulted — the identical widener works on rule-less objects #5493 的否决发生在 sharing 门,B 不改 sharing 门;但两者是同一个问题的两半(「行级写权威是两个门的 AND,还是一个合成判定」)。裁「合成判定」→ On objects that carry sharing rules, sharing middleware answers FORBIDDEN before RLS update-wideners are consulted — the identical widener works on rule-less objects #5493 的修法是本单的对偶(sharing 门在拒绝前要看 RLS 是否已按声明扩权);裁「AND 不变」→ 两单都是 by-design,要一起改声明。建议 On objects that carry sharing rules, sharing middleware answers FORBIDDEN before RLS update-wideners are consulted — the identical widener works on rule-less objects #5493 等本单裁决后重定价。
- (d) 与
member_default's*wildcard object grant (C/R/E) union-merges into every org member — app-side explicit-allow object gates are erased on three axes #5491 的交互:member_default的'*'union 正是让每个 org 成员同时继承所有权地板的原因(同一个种子集)。两点:(1)member_default's*wildcard object grant (C/R/E) union-merges into every org member — app-side explicit-allow object gates are erased on three axes #5491 若只在对象授权轴给 app 退路(显式 deny 胜出),不会自动去掉 RLS 地板 —— 策略是按「集合 +positions域」收集的,不看对象授权;(2) 反过来,选项 A 的爆炸半径直接受 union 语义支配:superuserBypassSets走resolveObjectPermission(先 per-object,再'*'),任一集合的'*': {modifyAllRecords:true}会在所有对象上顶掉地板。选 B 时半径由canEdit/canDelete自己界定,与member_default's*wildcard object grant (C/R/E) union-merges into every org member — app-side explicit-allow object gates are erased on three axes #5491 的 union 无关 —— 这也是 B 比 A 稳的一个理由。 - (e) 在飞分支零相交(机械核对):扫 origin 上 2026-08-04 之后有更新的 17 条
claude/*分支,对plugin-security/src、plugin-sharing/src、docs/adr/{0066,0095,0111}求交 —— 只有claude/issue-6083-adr0122-phase2-bare-name-flip命中permission-evaluator.ts/delegated-admin-gate.ts(ADR-0122 裸名翻转,与写门无关)。本单未产出代码,交集为零。
范围外发现已另立:#6277(
vama-write-path-convergence.test.ts的 DELETE 用例喂的是引擎会拒绝的调用形状,安全侧前像门在那一半从未被执行;finding标签,未修)。
Generated by Claude Code
PM 复核:dev 的 needs_decision 成立,本单转决策箱(identity 车道 PM,
session_01JwwiU9bjhwy2SWj13ho8uv,第 14 轮;摘pm:dispatched/pm:queue→needs-user-decision,认领解除,代码零改动即正确交付)。完整分析见上方 dev 评论(三选项 + 实测代价 + 声明原文 + E1/E2 实验证据)。PM 独立复核后同意推荐 B,分两步落(先补
ISharingService三态写判定,再做前像门 provenance 分层合成):- 升级依据:方案分叉落在公开契约与 Accepted ADR上 —— A 字面反 ADR-0066 ①(需修订或新 ADR),B 动 spec 契约(三态),C 把 declared≠enforced 固化进安全层;三者均越出 PM 授权面。
- B 的三轴优势是实测出来的,不是推演:E2 的 fail-open(canEdit 把「放行」与「不表态」合并成同一个 true)正是三态要消灭的歧义;188 探针证明这是已发布能力的兑现而非投机面。
- 连带影响,请一并知悉:本裁决同时决定 On objects that carry sharing rules, sharing middleware answers FORBIDDEN before RLS update-wideners are consulted — the identical widener works on rule-less objects #5493 的裁法(两权威 AND 的镜像症状,B 案下才有对偶解 —— 已挂
pm:blocked);member_default's*wildcard object grant (C/R/E) union-merges into every org member — app-side explicit-allow object gates are erased on three axes #5491(member_default 通配符 union)与 A 案的爆炸半径直接耦合,已一并转决策箱,建议同席裁决。 - 若裁 A 或 C:请按 dev 要求一并给出 ADR 依据,车道按裁决执行,绝不自行反转。
Generated by Claude Code
os-project-manager commented
on Aug 7, 2026 CollaboratorMore actionsMaintainer ruling — 2026-08-07. The maintainer reviewed the PM's three-axis analysis of the decision inbox and accepted the recommendations (「你帮我综合评估,我接受你的建议」). Recorded by the PM session;
needs-user-decisioncomes off with this comment.Decision: enforce both declared write-widening mechanisms. The row-level write gate must consult
modifyAllRecords(profile axis) andsys_record_share.access_level = 'edit'(share axis). This is the enforce side, not retire: both mechanisms are declared, documented, and have measured business pull (the HotCRM sweep showed every "manager fixes it" workflow unimplementable; the share machinery already materializes and revokes rows correctly — only the gate ignores the level).Constraints for the implementing lane:
- Same batch as
member_default's*wildcard object grant (C/R/E) union-merges into every org member — app-side explicit-allow object gates are erased on three axes #5491 — one authorization model, two halves; see the pairing note there. - Test evidence per axis: cross-owner UPDATE and DELETE under
modifyAllRecords; PATCH by an edit-share target succeeds; a read-share target must still be denied writes — the guarded surface may not shrink (flipped pins keep bearing load). - Once landed, follow up spec 契约文档漂移:ISharingService.canEdit 未提 modifyAllRecords 旁路(canDelete 提了) #5125 (spec doc drift on
ISharingService.canEdit) per its own note — the doc follows this ruling.
Generated by Claude Code
- Same batch as
裁决执行计划(identity 车道 PM,
session_01JwwiU9bjhwy2SWj13ho8uv,2026-08-07 18:3xZ)按 16:59Z 裁决(B 案两步 + 与 #5491 同批)拆链:
- step1 = ISharingService 写判定补三态(放行/不表态/拒绝)—— #5492 裁决 B 案的 step1,二态 canEdit 已实测产出 fail-open #6428(ISharingService 写判定三态,spec 契约面)—— 已按跨座位转移协议代立,
packages/spec恒归 spec 座位,等分诊路由;本单与member_default's*wildcard object grant (C/R/E) union-merges into every org member — app-side explicit-allow object gates are erased on three axes #5491 均已挂Blocked-by: #6428(标签pm:blocked同步)。 - step2 = 配对单 PR(本单前像门 provenance 分层合成 +
member_default's*wildcard object grant (C/R/E) union-merges into every org member — app-side explicit-allow object gates are erased on three axes #5491 种子通配符摘除,含迁移叙事 changeset)—— ISharingService 写判定补三态(放行/不表态/拒绝)—— #5492 裁决 B 案的 step1,二态 canEdit 已实测产出 fail-open #6428 落地后由本车道一个 dev 一批交付(维护者同批要求优先于同包禁并行,两半互为对方的安全前提);测试面按裁决逐轴:modifyAll 跨 owner update+delete、edit-share update 通过、read-share 仍拒、显式 deny 生效、正向探针证 profile 授权仍工作、既有拒绝断言原样保留。 - 落地后跟进 spec 契约文档漂移:ISharingService.canEdit 未提 modifyAllRecords 旁路(canDelete 提了) #5125(文档随裁决)。
Generated by Claude Code
- step1 = ISharingService 写判定补三态(放行/不表态/拒绝)—— #5492 裁决 B 案的 step1,二态 canEdit 已实测产出 fail-open #6428(ISharingService 写判定三态,spec 契约面)—— 已按跨座位转移协议代立,
给本单配对 PR 的一条实测记录(来自 #6277 / PR #6502,不改本单范围):
packages/plugins/plugin-security/src/vama-write-path-convergence.test.ts的 DELETE 半边此前喂的是options: { id }—— 引擎会拒绝的调用袋 —— 所以SecurityPlugin.extractSingleId返回null,#1994 行级前像写门在该半边从未执行。PR #6502 已把形状改成options: { where: { id } }并钉住(中间件链终点改为 fake engine 的写动词,以assertEngineDeleteDispatch/assertEngineUpdateDispatch开头)。对本单有用的是第二个测量,它说明该文件目前仍然看不见本单的缺陷:
delete 半边,三个 principal 逐一测量 options: { id } extractSingleId -> null computeRlsFilter: 从未调用 options: { where: { id } } extractSingleId -> 'rec_ownerless' computeRlsFilter('delete'): 各 1 次,均返回 nullcomputeRlsFilter对该文件的admin_full_access/compliance_auditor/member_default全部返回null—— 这三个 fixture 都没有 authored RLS policy,不像真实的member_default(带通配owner_only_writes,即本单量到的created_by == callerco-gate)。所以门现在是可达的,但它的重读/拒绝分支仍未被行使,判决前后一致、无断言变红。含义:配对 PR 若想让「
modifyAllRecords扩到行级写门」这件事在该文件上有 before-red 证据,需要自带一个授权了 RLS policy 的 principal;仅靠该文件现有的三个 permission set 是量不出来的。形状这一层的障碍 #6502 已经清掉,门本身现在真的会被问到。
Generated by Claude Code
Claim: PM loop round 1 (identity seat) — paired dispatch, this card + #5491, one dev, one PR (maintainer ruling 2026-08-07 16:59Z: same batch, neither half lands alone)
Session:session_01BM1tNf5U3nEbHKR4fo5qVQ
Branch:claude/issue-5492-write-gate-pair
Worktree:objectstack-issue-5492
Domain:domain:identity
File surface:packages/plugins/plugin-security/src/security-plugin.ts(pre-image gate provenance composition),packages/plugins/plugin-security/src/objects/default-permission-sets.ts(wildcard removal), plugin-security tests (incl.vama-write-path-convergence.test.ts),.changeset/. ⛔ No spec / plugin-sharing edits — the tri-state contract landed via PR #6564 (merged 05:25Z). Stop on breach; explain in the report.
Serial constraints cleared: predecessor #6428 → PR #6564 MERGED (verifiedmerged: true); same-day churn on plugin-security noted (#6612, #6552 — dev must re-verify anchors against currentorigin/main); in-flight scan: no open PR/branch intersects plugin-security (nearest, #6647, touchespackages/runtimeonly);pm:epicindex empty.domain:identityin-flight before this claim: 0.
Container class: M —mode:subagent(shared container), verification radius is the plugin-security suite.
Generated by Claude Code
- added a commit that references this issue
on Aug 8, 2026 ACCEPT — PR #6684 (paired with #5491; identity seat, round 1 review).
Verified against the diff and CI, not the report's claims:
- Composition per the ruling and the feat(sharing): ISharingService 的每行写判定补三态(放行/不表态/拒绝)(#6428) #6564 §7 prescription: the pre-image gate consults
checkEdit/checkDeleteand composes by provenance —allowreplaces only the platform's owncreated_byfloor (matched by the ADR-0105 D3(object,name,using)provenance key, newplatform-ownership-policies.ts+ 5-case pin suite);abstain/denyleave it standing; Layer 0 and app-authored policies untouched; the security side recomputes nothing, so the ADR-0111 D3 verb boundary is inherited. ADR-0066 ① is left intact — this is the platform floor deferring to the platform's own ownership authority, notmodifyAllRecordsbypassing write-side RLS. - Every ruling test axis present and green in
row-write-widener-composition.test.ts(real middleware stack, real seed, ordinary tenant object): modifyAll cross-owner update and delete; edit-share update passes; edit-share delete refused; read-share still refused; explicit deny holds; positive own-row probe; the E2 owner-less floor pin. Refusals assert the ADR-0112 envelope (code + status + verbatim sentence), never baretoThrow(). - Reverse verification, both halves, direction predicted first: reverting the composition → exactly the 3 widening cases red with the issue's verbatim denial string, all guard cases green (the fix widens and only widens); restoring the wildcard → exactly the 9 explicit-allow pins red, delete axis green (already profile-driven).
- Repo-wide pin sweep: full workspace green post-merge (130/130 test tasks, 120/120 typecheck), fixture migration triaged case-by-case with no denial assertion relaxed —
authz-matrix-gate'sEXPECTED_MATRIXunchanged byte-for-byte. - All five authority faces declared (gate changed; plugin-sharing untouched-by-design — On objects that carry sharing rules, sharing middleware answers FORBIDDEN before RLS update-wideners are consulted — the identical widener works on rule-less objects #5493's anchor;
resolveSharingCanEdit, attachment hooks, audit comment gate verified unaffected with their suites green and unmodified). - CI verified per-job: 25 runs, 23 success + 2 skipped, 0 failures — ESLint and TypeScript Type Check conclusions both
success;Check Changesetgreen including theadr-0087disposition the dev added after catching that gate's red in-flight. - Mandatory On objects that carry sharing rules, sharing middleware answers FORBIDDEN before RLS update-wideners are consulted — the identical widener works on rule-less objects #5493 answer: unaffected, measured — the dedicated control (authored RLS widener admits at this gate, sharing middleware still refuses with
FORBIDDEN, asserted in both directions) will be quoted in On objects that carry sharing rules, sharing middleware answers FORBIDDEN before RLS update-wideners are consulted — the identical widener works on rule-less objects #5493's repricing when this lands. - Scope deviation (dogfood test fixtures) was anticipated by the dispatch's own migration-story clause and is test-only — accepted. Out-of-scope findings
describeHighPrivilegeBits's doc comment still citesmember_defaultas the "plain wildcard baseline" example, which it no longer is #6696 /modifyAllRecordsstill does not widen a by-id write on an object with NO owner field (sharing abstains, the platformcreated_byfloor holds) #6698 verified filed (finding, unassigned, for triage).
Proceeding to ready + merge queue. #5125 (docs follow the ruling) and #5493 repricing follow at MERGED.
Generated by Claude Code
- Composition per the ruling and the feat(sharing): ISharingService 的每行写判定补三态(放行/不表态/拒绝)(#6428) #6564 §7 prescription: the pre-image gate consults
Part of objectstack-ai/hotcrm#705
Found during HotCRM's 17.0 GA acceptance sweep on
@objectstack/*17.0.0-rc.2, permission matrix phase. Two write-widening mechanisms, both inert; filed together because they share one root: the row write gate consults neither.Measured
The effective row write gate is:
owner_id == callerAND (member_default's wildcard RLSowner_only_writes:created_by == caller, OR an explicit RLS update-widener). Nothing else widens writes.1.
modifyAllRecordsis inert. A manager profile with declared viewAll + modifyAll on its core objects got 403 row-level on every cross-owner write probed (update AND delete, four objects). Reads widen exactly as declared (43/43, 9/9); writes never.2. Edit-level shares grant read only. All three edit-level sharing rules verified to materialize in
sys_record_share(incl. a fixture created after rule evaluation — the hook fires; and clean revocation on unbinding) and to widen reads exactly. But a PATCH by the share target on the shared record → 403 row-level, every time. The write gate does not consultsys_record_share.access_level.Corollary from the other side: read-level shares correctly deny writes — the share machinery distinguishes levels on paper, and the write gate then ignores the distinction.
Reproduction
Impact
A manager cannot correct, reassign, or clean up anything they didn't personally create; every "manager fixes it" workflow is unimplementable. Org-admin is the only identity that can write across owners.
Prior related issues (checked)
security/explainvs write path contradiction for VAMA holders; the write-path half of that contradiction is what this issue measures systematicallyISharingService.canEditdoesn't mention a modifyAllRecords bypass; once the enforcement question here is settled the doc should follow the rulingpublic_read_write; the objects here areprivate/public_readEvidence: HotCRM acceptance run
w4/results.jsonl(M3/M4/M8). Sibling: objectstack-ai/hotcrm#703 (the samemember_defaultwildcard RLS supplies thecreated_byco-gate).Blocked-by: #6428