Skip to content

authz(ADR-0099): P0–P2 落地 — posture 成为分级裁决唯一输入(Layer 0 豁免门 + Layer 1 级判定收敛) #3211

Description

@os-zhuang

背景

ADR-0099(PR #3109,merged 2026-07-17)是 ADR-0095 posture 阶梯的裁决收尾篇:阶梯派生了(B2/B4,#2938)、#2956 把 rung 挂上了 ExecutionContext、explain 侧已收敛读 ctx.posture(#2949)——但 enforcement 侧没有任何分级裁决读它,每个判定站点仍从 capability 位重新推导主体等级。这类「多站点独立派生」已在一个发布周期里踩出两次分叉 bug:#2946 Finding 2(org admin 越租户墙)、#2949(explain 与 enforcement 的 posture 标签漂移)。

本 issue 承接 ADR-0099 的 P0 → P1 → P2′ 实现;从 tracking #2920(已收口)移交。

修订说明

v1.1(2026-07-18):经落点级复核,P1 由原文「小改动、行为保持」修正为「fail-safe 方向的收窄,但确有行为变更」——探针与 rung 的证据不同源,P0 等价门预期不会全绿,其产出是需逐条裁决的 delta 名单。

v1.2(2026-07-18):加入完整开发计划(里程碑 PR 拆分 + 两个 maintainer 决策门、验证矩阵、风险与回滚、发布节奏)。

v1.3(2026-07-18,M3 裁决):M2 合并后、动 M3 前的核实推翻了「M3 纯行为保持」——Layer 1 短路的 tier 判据(持超级位)是每主体×每对象的委派原语(自定义集给非管理员岗授某对象 viewAllRecords = 审计员模式 = Salesforce 对象级 View All 同款),posture(每主体一个 rung)结构上无法表达;严格照 D2 收敛会让该位条件性失效(ADR-0049 禁止的 declared-but-unenforced 类)并砍掉主流委派能力。maintainer 四视角综合裁决(主流平台 / 长期架构 / 业务 / AI 防错,见 issue 讨论):选 B+ —— 原 P2 否决,ADR-0099 修正为两轴模型(posture=边界轴管阶梯两端,per-object 位=范围轴管 Layer 1 tier,「单一裁决输入」精化为「每轴单一权威」,新增不变量 I7)。ADR 修正已出 PR #3245;原 M3 方案作废,替换为下方 M3′。

进度


开发计划(v1.3 修订后)

已完成里程碑(存档)

M3′ — P2′:两轴模型钉住格(≤1 PD,前置 PR #3245 合并)

性质:纯测试 + 已随 #3245 的 ADR 修正。零行为变更。

落点与内容:

  1. ADR-0099 两轴 Amendment → PR feat(authz): ADR-0099 two-axis model — Layer 1 tier stays the per-object scope bit; amendment + P2′ pins (#3211 M3/M3′) #3245(D2 Layer 1 行修正、每轴单一权威、I7、I2 澄清、P2 superseded 标记)。
  2. 矩阵钉住格(authz-matrix-gate.test.ts):
    • 种子等价格:种子面「持超级位的主体 ⟺ posture ≥ TENANT_ADMIN」一致(两轴在种子面重合的显式断言,未来漂移报警);
    • 委派格(核心):MEMBER + 自定义集对某 private 对象授 viewAllRecords → 组织内全行可见 且 被 Layer 0 墙住(I7)——把「合法委派」钉进 CI,防未来任何「posture 收敛清理」静默砍掉它;
    • I7 格:同一委派主体供异租户 organization_id 的写被拒(若 M2 既有格已覆盖则引用注明)。
  3. advisory lint 后续 issue(独立小项,不阻塞收口):member 可达权限集在 private 对象上授 viewAllRecords/modifyAllRecords 时的授权期提示(复用 describeHighPrivilegeBits)。

判据:零行为变更;plugin-security 全套绿;docs 门绿。

G2 — 收尾核查

非目标

  • P3(EXTERNAL rung 激活):demand-gated(ADR-0090 follow-up Migrate from npm to pnpm #6 / Permission Model v2 — named follow-up ADRs (post-ADR-0090 parking lot) #2776 item 6)。
  • Layer 1 tier 收敛(原 P2):已否决(G-M3,ADR-0099 Amendment 记录在案)。若未来要重启,须另立 ADR 面对「废掉对象级 view-all 委派」这一 breaking 并给出迁移路径——不是本 issue 范围。
  • 显式 deny/muting:ADR-0099 D4 已 Rejected。
  • 运行时开关:能调宽租户墙豁免的开关即 D4 否决之物;回滚 = revert PR。

执行约束

  1. worktree-first(Prime Directive Migrate documentation site to Fumadocs with monorepo structure and shared content #11)。
  2. 每里程碑独立 PR;M3′ 待 feat(authz): ADR-0099 two-axis model — Layer 1 tier stays the per-object scope bit; amendment + P2′ pins (#3211 M3/M3′) #3245(ADR 修正)合并后落,保持「先定规范再落格」的顺序。
  3. 两轴模型给 AI 作者的规则一句话:位定对象内行范围,posture 定租户边界——所有后续文档/skill 以此口径。

关联

ADR-0099 / #3109 · #3218(M1/P0)· #3226(M2/P1)· #3245(两轴 Amendment)· ADR-0095 · #2956 · #2949 · #2946 Finding 2 · #2937 · ADR-0049(declared-but-unenforced 禁令,G-M3 裁决依据)· tracking #2920(已收口)· #2776 item 6(EXTERNAL 产品轨)

Activity

  1. os-zhuang commented on Jul 18, 2026

    @os-zhuang
    ContributorAuthor

    M1 完成(PR #3218,draft,CI 待跑)+ ADR-0099 已转 Accepted

    M1(P0 等价门)已实现并全绿:plugin-security 498 passed / 20 files。同 PR 携带 ADR-0099 Proposed → Accepted 转态(maintainer 2026-07-18 批准)+ Acceptance addendum。

    G1 裁决输入 — 分歧名单(M1 的侦察产出)

    种子形态(platform_admin / org_admin / member / W1 fixture)探针与 rung 全部一致——D1 的行为保持声明在种子面成立。分歧仅存在于两类对抗形态:

    # 主体形态 今天(探针) P1 翻转后(rung) 恢复路径
    (a) 作用域 admin_full_access grant(sys_user_permission_set.organization_id 非空) 在 private / platform-global / better-auth 对象上跨租户豁免 墙到自己 org 补授 unscoped admin_full_access
    (b) 自定义权限集零散授平台专属能力(如仅 studio.access,且另有超级位) 同上 同上 同上

    方向性已被 I3 格证明:rung ⊆ 探针 → P1 只可能收窄豁免,不可能放宽。两形态已在 matrix gate 以 KNOWN DIVERGENCE 格钉住,posture-blind staging 格锁定了翻转前行为(P1 时逐格翻绿)。

    待 maintainer(G1 剩余两项)

    裁决通过后 M2 即可开工(落点与 fallback 语义 ADR D1 已定:两信号并存 rung 胜出、分歧记 defect 日志、执行较窄者)。


    Generated by Claude Code

  2. os-zhuang commented on Jul 18, 2026

    @os-zhuang
    ContributorAuthor

    G1 决策门 — 已闭合(maintainer 裁决,2026-07-18)

    ① 分歧收窄裁决:两条均接受

    # 形态 定性 裁决
    (a) 作用域 admin_full_access grant 今天豁免跨租户,违背授权者「你是 org X 的管理员」意图,且与 explain 已显示的 MEMBER 标签当面矛盾(#2949)。P1 后墙到本 org = 语义对齐。 ✅ 接受
    (b) 自定义集零散授平台专属能力(如 studio.access)+ 另持超级位 一次「可用 Studio」式的普通授权即可静默重开 #2946 Finding 2 修复的跨租户后门。P1 从结构上封死(posture 只从非作用域 grant 派生,任何能力拼装到不了 PLATFORM_ADMIN)= 完成 #2946 未竟部分,非新收窄。这正是 ADR-0099 不变量 I1 的原文所防。 ✅ 接受

    方向由 I3 格证明:rung ⊆ 探针 → 只收窄、不放宽,无反向越权风险。爆炸半径仅 EE 多组织 shared-DB(开源单组织 / cloud env-per-DB 零影响)。

    ② M2 配套三件(随 P1 落地)

    1. defect 日志:探针 true / rung false 时打 WARN(含主体 id + 命中形态 a/b),受影响部署升级前后均有面包屑。
    2. release-notes 升级前自查指引:扫 sys_user_permission_set 中 admin_full_access 的作用域授权行 + 自建权限集 systemPermissions ∩ {manage_metadata,manage_platform_settings,studio.access,manage_users};恢复路径 = 补授 unscoped admin_full_access。
    3. 文档立规矩一句话:跨租户运维权 = unscoped admin_full_access(唯一路径);零散平台能力只控功能入口,不控数据边界。

    ③ 发布窗口

    P1 不进 15.2.0(changesets PR #3098),落下一个 minor + release-notes callout。

    残余缝隙(已登记,非阻塞)

    P1 收窄仅覆盖携带 posture 的路径(REST/runtime/MCP 用户请求,#2956 已覆盖 = 全部外部攻击面)。不带 posture 的内部调用方(delegated-admin 桥、sharing 服务、getReadFilter 消费者)走探针 fallback、保持旧行为,直至 ADR-0096 D3 消灭手搓 context。M2 的 fallback 矩阵格钉住这些路径。


    G1 闭合 → M2 前置 = 「M1 PR #3218 合并」+「G1 裁决」,后者已达成。M1 CI 全绿、已转 ready-for-review,待合并后即按 harness 的 merged-branch 流程从最新 main 重开分支开工 M2。


    Generated by Claude Code

  3. os-zhuang commented on Jul 18, 2026

    @os-zhuang
    ContributorAuthor

    M2(P1 翻转)完成 — PR #3226(draft,CI 待跑)

    Layer 0 跨租户豁免门已从能力探针翻转为读携带 ctx.posture rung,探针降为 resolver-less fallback。M1(#3218)已合并、G1 已裁决,两前置满足后开工。

    落地

    • 核心翻转 computeLayeredRlsFilter:携带 rung 权威、无 rung 探针兜底;读写共用一决策(computeWriteTenantCheckFilter 复用),不可能漂移。
    • 配套三件:① defect 日志 [authz/ADR-0099] WARN(探针↔rung 分歧);② changeset 带 release-notes 升级前自查指引 + 恢复路径;③ authorization.mdx 立规矩一句话。dogfood multi-tenant-exemption-posture ledger note 同步更新。退役 security-plugin.ts:71-77 过时注释。
    • 行为收窄:G1 裁决的两类主体(作用域 grant / 零散平台能力)墙到本 org,恢复 = 补 unscoped grant。(b) 实质封 fix(plugin-security): 堵跨租户 UPDATE 写 + org_admin private 对象越墙(security) #2946 能力拼装后门(I1)。真 admin 仍豁免、org_admin 仍被墙、单组织零影响。

    验证(全绿)

    plugin-security 503 passed · dogfood 296 passed(含更新 ledger 格)· build 71/71 · tsc + role-word + doc-authoring clean。

    进度

    M3 前置是 #3226 合并。合并后按 merged-branch 流程重开分支开工。


    Generated by Claude Code

  4. os-zhuang commented on Jul 18, 2026

    @os-zhuang
    ContributorAuthor

    M2 已合并 ✅ + ⚠️ M3 落点核实推翻了「纯行为保持」判断

    M2(P1 豁免门翻转)已 merge 进 main(#3226)。进度:M1 ✅ · G1 ✅ · M2 ✅ · M3 ⚠️ 见下。

    核实结论:M3 不是免费收尾,是 M2 的同构收窄——且 blast radius 可能更大

    动 M3 前对 Layer 1 短路做了落点级核实,修正 v1.2 计划里「M3 纯行为保持、预期零 delta」的判断——那是错的。

    证据链(resolve-authz-context.ts + security-plugin.ts:2482 + default-permission-sets.ts):

    • Layer 1 短路现在的条件是 posturePermits && superuserBypass——tier 判据是持超级位(viewAllRecords/modifyAllRecords,来自任一权限集的对象 rule)。
    • ADR-0099 D1/D2 要 tier 收敛读 posture ≥ TENANT_ADMIN;而 TENANT_ADMIN ← 持有名为 organization_admin 的权限集(ctx.permissions 装的是权限集名字,:262/:305)。
    • 种子面同源:只有 admin_full_access / organization_admin 授超级位,二者都 posture ≥ TENANT_ADMIN。
    • 对抗面分歧:一个自定义权限集给普通 member 授某对象 viewAllRecords(不叫 organization_admin)→ 持超级位、但 posture=MEMBER。M3 收敛后:该 member 在 private 对象上从「短路看组织内全行」收窄为「business RLS 按 ownership/sharing 收窄」。

    为什么这个 delta 可能比 M2 更要紧

    M2 的分歧形态(作用域 grant / 零散平台能力)罕见。M3 的分歧形态是 「把『查看某对象全部记录』委派给非管理员角色」——这是常见、合法的授权模式(典型:「审计员可读全部发票」)。因为超级位只在 private/platform-global 对象上短路(#3005:public 对象上超级位本就不生效),M3 的 delta 精确命中**「private 敏感对象上的 view-all 委派」**——这类委派 M3 后会被 ownership 墙掉。

    换句话说:M3 严格照 ADR-0099 D2 收敛,会让「给 member 授 viewAllRecords」这个位在 private 对象上失效(posture 定 tier → MEMBER 走 business RLS)。这可能不是我们想在没有 maintainer 明确裁决下做的收窄。

    这是一个架构裁决点,不是我能默认推进的

    ADR-0099 I5 原文:「Posture selects tier, never side: no rung grants a read/write bypass that the per-side capability bit does not」——它约束的是「posture 不能给位没给的」。但反方向(位给了、posture 没到 tier)ADR 用 D2 表判「MEMBER → business RLS applies」,即位被 posture 压制。这正是上面的 delta。

    需要你(maintainer)先定方向,再谈实现:

    选项 A — 照 ADR-0099 D2 严格收敛:tier=posture,member 的自定义 view-all 在 private 对象上被压制(fail-safe 收窄)。走 M2 同款流程:等价门侦察 → 分歧名单 → G 裁决 → 翻转 + release-notes。若采纳,需接受「view-all 委派在 private 对象上要求 organization_admin 或更高」这一收窄。

    选项 B — Layer 1 tier 保持读超级位(不收敛),只在文档/ADR 注明:承认 Layer 1 的 tier 事实上由超级位驱动(与 posture 高度但非完全重合),M3 缩减为「加一道等价门钉住『超级位 ⊇ posture≥TENANT_ADMIN 的种子等价』+ 注释对齐」,不改行为。这尊重了现有 view-all 委派语义,把 posture-tier 收敛留给未来(需要时再 breaking)。

    选项 C — 折中:tier = posture≥TENANT_ADMIN OR 超级位(即位仍能短路,posture 只补充)。但这弱化了 ADR-0099「单一裁决输入」的初衷,且引入第三种语义,我不推荐。

    我的建议:选项 B。M1/M2 已经拿下 ADR-0099 最有价值的部分(Layer 0 豁免的分叉 bug 类根治——#2946 Finding 2 那条真实漏洞线)。Layer 1 的 tier 收敛(D2)收益是「概念统一」,但代价是压制一个合法且常见的委派模式(private view-all),这个 breaking 值得单独立项、按需触发,而不是作为收尾顺手做。选 B 则 M3 是零风险的「等价门 + 注释对齐」,#3211 干净收口,posture-tier 收敛留 ADR 追踪待未来。

    等你选方向。选 A 我按 M2 流程做等价门 + 出分歧名单;选 B 我做等价门 + 注释,零行为变更即可收口;选 C 我们再议。


    Generated by Claude Code

  5. os-zhuang commented on Jul 19, 2026

    @os-zhuang
    ContributorAuthor

    M3′(P2′ 钉住格)完成 + G2 核查通过 — 全部开发闭环,待 #3245 合并收口

    两轴模型的钉住格已实现并入 PR #3245(现为「ADR 修正 + P2′ 钉住格 + changeset」完整 PR)。

    M3′ 落地(authz-matrix-gate.test.ts,纯测试零行为变更)

    • 种子等价格:种子面「持超级位 ⟺ posture ≥ TENANT_ADMIN」一致;
    • 委派格(LOAD-BEARING):MEMBER 持 invoice_auditor(某 private 对象委派 view/modify all)→ Layer 1 短路(组织内全行)+ Layer 0 墙住(org-1)——审计员模式钉进 CI,防未来「posture 收敛清理」静默删掉它;
    • I7 格:该持有者写侧被墙(pre-image=org-1,非 BYPASS)、供异租户 org_id 的 insert 被拒;
    • 对照格:无委派的 member 在 private 对象被 DENY → 位是真实可授予能力、非条件性死位。

    验证(全绿)

    plugin-security 510 passed · tsc 通过 · check:role-word / check:doc-authoring clean · plugin-security build 绿。

    G2 收尾核查

    进度总览

    所有开发已闭环。ADR-0099 以「边界轴收敛完成(M1/M2 根治 #2946 分叉 bug 类)+ 范围轴显式确认并钉死(M3′)」的完整姿态落地,比勉强做完原 P2 去砍一个合法委派更经得起回看。剩一步纯行政:你 review/merge #3245 后我 close #3211。


    Generated by Claude Code

  6. os-zhuang commented on Jul 19, 2026

    @os-zhuang
    ContributorAuthor

    ✅ 收口 — ADR-0099 P0→P2′ 全链完成,#3245 已合并

    PR #3245 合并进 main,本 tracking 的全部开发闭环。G2 核查通过,关闭 issue。

    交付全景

    里程碑 PR 内容
    M1 — P0 等价门 + ADR Accepted #3218 ✅ 探针提为导出纯函数 + authz-matrix-gate 等价/KNOWN DIVERGENCE/I2/I3/I4 格;产出分歧名单
    G1 — 收窄裁决 — 两条收窄接受((b) 封 #2946 能力拼装后门)+ 三件配套 + 落下一 minor
    M2 — P1 Layer 0 豁免门翻转 #3226 ✅ 豁免门读携带 ctx.posture rung、探针降 resolver-less fallback、读写共用一决策、[authz/ADR-0099] defect 面包屑、过时注释退役
    G-M3 — Layer 1 裁决 — 原 P2 否决 → 两轴模型(四视角:主流平台 / 粒度不可互换 / 委派是低代码日常 / AI 静默 miss 防错)
    M3/M3′ — 两轴 Amendment + P2′ 钉住格 #3245 ✅ ADR 两轴修正 + I7 + 委派格(审计员模式钉进 CI)

    最终模型(两轴,每轴单一权威)

    G2 收尾核查

    遗留(已妥善转出,非本 issue 范围)

    ADR-0099 以「边界轴收敛完成(根治真实分叉 bug)+ 范围轴显式确认并钉死(保住合法委派)」的完整姿态落地。Close。


    Generated by Claude Code

  7. os-zhuang commented on Jul 19, 2026

    @os-zhuang
    ContributorAuthor

    G2 补充证据 — cloud 侧多组织 e2e 实测,M2 零 fallout ✅

    收口时 G2 清单里唯一「待验证」项(cloud 侧企业多组织 e2e)现已本地实测确认。cloud 仓库拉入会话、framework pin(d1d1c40 = #3232,已含 M2 #3226)link 到含 M2/M3′ 的 framework,跑企业授权栈全套。

    结果:@objectstack/security-enterprise — 164 tests / 20 files 全绿

    关键的跨租户隔离 smoke(multi-org-tenant-isolation.integration.test.ts,open-core dogfood 跑不到的企业 shared-DB 路径)4/4 通过:

    1. org admin 持 viewAllRecords 看组织内全行、但被 Layer 0 墙 ✅ —— 这正是 M2 + 两轴模型的真机对照:TENANT_ADMIN 持超级位 = Layer 1 组织内全行(范围轴),posture 定边界 = 不跨 Layer 0(边界轴)。M3′ 委派格的企业版。
    2. member owner-scoped + tenant-isolated ✅
    3. forged organization_id INSERT 被 auto-stamp 覆盖(security(authz): 多组织下伪造 organization_id 的 insert 可越租户墙 — Layer 0 未门控 insert post-image #2937)✅
    4. reassign organization_id UPDATE 不越墙(fix(plugin-security): 堵跨租户 UPDATE 写 + org_admin private 对象越墙(security) #2946/security: 服务端 readonly 字段在 UPDATE 时未强制(可被用户上下文覆盖) #2948)✅ —— 运行时 log 实证:[Security] Layer 0 tenant CHECK FAILED on update — write denied (fail-closed)

    外加 conformance + position-anchor-unit-scope + governance 等其余 160 测试全绿。

    结论:M2 的 Layer 0 豁免门翻转在企业多组织路径零 fallout。 跨租户四个 crown-jewel 不变量端到端保持。ADR-0099 实现链的最后一块验证补齐,G2 完整闭合。

    注:cloud pin(d1d1c40)含 M2 但不含 M3′——符合预期(M3′ 是纯 docs+test 零行为变更,cloud 无需);cloud CI 下次 bump pin 到含 M3′ 的 framework 时预期同样零 fallout。


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions