Skip to content

fix(plugin-sharing): share-link enforcement 吃完整授权信封,修复 group 姿态建链恒 403 (#6206) - #6552

Merged
baozhoutao merged 1 commit into
mainfrom
claude/issue-6206-sharelink-full-context
Aug 8, 2026
Merged

fix(plugin-sharing): share-link enforcement 吃完整授权信封,修复 group 姿态建链恒 403 (#6206)#6552
baozhoutao merged 1 commit into
mainfrom
claude/issue-6206-sharelink-full-context

Conversation

@baozhoutao

Copy link
Copy Markdown
Contributor

Fixes #6206

裁决 A 案(维护者 2026-08-07,comment 5219847367)的消费半边。契约半边已随 PR #6511(d7e0b4212,#6430)落 main,本 PR 按落地后的契约形状消费。

问题

share-link 路由在 resolveAuthzContext 之后重新拼了一个四字段对象:

return { userId: authz.userId, tenantId: authz.tenantId, positions: authz.positions, permissions: authz.permissions };

这个对象不是路由自己用的身份 —— 它被原样交给 ShareLinkService.createLink,而后者用它去 engine.find 复读目标记录([Finding-2]「只能为自己看得见的记录建链接」)。于是 accessible_org_idsorg_user_idssystemPermissionsposturetabPermissions 在进 enforcement 的路上被丢掉

group 租户姿态下 accessible_org_ids 就是 Layer 0 那堵墙(ADR-0105 D2),集合缺席即判否(fail closed)⇒ 可见性校验查不到任何行 ⇒ 路由抛 403。是可用性缺陷,不是泄露。

改法

信封整个透传,不再逐字段挑选:

return { ...authz, isSystem: false };

逐字段挑选正是这条缝出问题的方式 —— 换一份「这次写全了」的字段清单,只是把同一颗雷推迟到 ResolvedAuthzContext 下次长出新维度的那天。posture 因此随上下文流动,不在 enforcement 处重推(ADR-0095 D2)。isSystem: false 与两个兄弟传输面(rest-server.ts / runtime/src/security/resolve-execution-context.ts)写法一致,也就是 ADR-0118 D2 的「absence is never system」显式化。

窄类型 ShareLinkExecutionContext 按裁决保留,但只服务路由自己的 401 判定:新增的 isAuthenticated(ctx) 是它在本仓库唯一的消费点 —— 在读处收窄,而不是在生产处收窄,这正是裁决的要点。share-link-service.ts 三个裁决方法与 canManageShares 探针的形参随 #6511 收成完整 ExecutionContext

before-red:分诊的 repro 义务已兑现(复现成立)

分诊 caveat 说得很明白:方向系代码读出、未跑复现,接手第一件事是先复现。已跑,方向属实。

新增 packages/plugins/plugin-security/src/share-link-tenant-wall.test.ts —— 放在 plugin-security 是因为判否的那一步在这里算(computeTenantLayer0Filter),而该包本来就 devDepend 了 plugin-sharing(controlled-by-parent-sharing.test.ts 等先例),反向依赖则会成环。测试里真实的部分是:被 boot 的真插件(所以跑的是生产那条闭包)、真 share-link service、真墙;替身只有存储 —— 且 RLS_DENY_FILTER 是靠「匹配不到任何行」判否的,和生产同一机制,不是特判。

把裁剪恢复回去、重建 dist 后:

× mints a link for a record the caller can read ... AssertionError: expected 403 to be 201
× reaches records across EVERY organization the caller belongs to (MOAC union) ... expected 403 to be 201
  Test Files  1 failed (1)   Tests  2 failed | 2 passed (4)

修后 4/4 绿。两条在两个世界里都绿的用例是防自欺的:single 姿态 201 不变(墙在该姿态惰性),以及调用方不属于记录所在组织时依然 403 —— 变宽的是信封,不是权限。

回归钉

packages/plugins/plugin-sharing/src/share-link-enforcement-context.test.ts 不写死字段清单 —— 写死清单正是坏掉的那件工件。它拿 #5859 的 seam kit(bootRequestContext,走真 resolveAuthzContext)为同一 principal 造一份参照信封,断言 enforcement 拿到的 context 不缺任何一个键。将来新增授权维度两边同时覆盖;有人再裁一刀,报错会把丢掉的键名列出来:

+   "systemPermissions", "org_user_ids", "accessible_org_ids", "accessToken", "email", "posture", "isSystem",

外加卡片点名的四个键按名断言、401 逻辑不变(且 401 在任何 enforcement 之前判定)、以及看不见的记录仍旧 403。

验证

  • pnpm --filter @objectstack/plugin-sharing test → 14 files / 363 tests 全绿
  • pnpm --filter @objectstack/plugin-security test → 36 files / 773 tests 全绿
  • pnpm --filter @objectstack/runtime test(消费半径:dispatcher 面的 share-links domain)→ 110 files / 1606 tests 全绿
  • typecheck:plugin-sharing / plugin-security / runtime 三包 Done
  • 门禁:check:engine-double-contract(两个新替身的 update/delete 均以 assertEngineUpdateDispatch / assertEngineDeleteDispatch 开场)、check:authz-resolvercheck:route-envelopecheck:adr-anchorscheck:slot-lookupcheck:error-code-casingcheck:nul-bytes 全绿

范围外发现

⛔ 未触碰 sharing-plugin.ts :857 一带的 FORBIDDEN 抛点(#5493 领地);未动 spec。


Generated by Claude Code

…elope (#6206)

The share-link routes rebuilt a four-field object out of the
`resolveAuthzContext` result (`userId`/`tenantId`/`positions`/`permissions`)
and handed it straight to `engine.find` as the [Finding-2] visibility check's
context. `accessible_org_ids`, `org_user_ids`, `systemPermissions`, `posture`
and `tabPermissions` were dropped on the way into enforcement.

Under the `group` tenancy posture `accessible_org_ids` IS the Layer 0 wall
(ADR-0105 D2) and an absent set denies, so the check failed closed and link
creation answered 403 for records the caller reads fine elsewhere — reproduced
here, not only read from the code.

The envelope is now passed through whole (`{ ...authz, isSystem: false }`),
per the maintainer's option-A ruling on #6206 and the contract half that landed
with #6511. `posture` travels with the context and is never re-derived at the
enforcement site (ADR-0095 D2). `ShareLinkExecutionContext` survives as the
routes' own 401 vocabulary, consumed only by the new `isAuthenticated` gate.

Tests: a seam-parity pin in plugin-sharing (the enforcement context must carry
every key the real resolver produced — re-trimming fails by naming the dropped
keys) and the behavioural `group`-posture repro in plugin-security, which owns
`computeTenantLayer0Filter` and can therefore drive the real wall.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JwwiU9bjhwy2SWj13ho8uv
@vercel

vercel Bot commented Aug 8, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 8, 2026 4:25am

Request Review

@github-actions github-actions Bot added the size/l label Aug 8, 2026
@github-actions

github-actions Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/plugin-sharing.

8 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/kernel/runtime-services/examples.mdx (via @objectstack/plugin-sharing)
  • content/docs/kernel/runtime-services/sharing-service.mdx (via @objectstack/plugin-sharing)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/plugin-sharing)
  • content/docs/permissions/authorization.mdx (via packages/plugins/plugin-sharing)
  • content/docs/permissions/permissions-matrix.mdx (via packages/plugins/plugin-sharing)
  • content/docs/plugins/packages.mdx (via @objectstack/plugin-sharing)
  • content/docs/protocol/objectql/security.mdx (via packages/plugins/plugin-sharing)
  • content/docs/releases/implementation-status.mdx (via @objectstack/plugin-sharing)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling labels Aug 8, 2026
@baozhoutao
baozhoutao marked this pull request as ready for review August 8, 2026 04:36
@baozhoutao
baozhoutao added this pull request to the merge queue Aug 8, 2026
Merged via the queue into main with commit 8e13ca8 Aug 8, 2026
25 checks passed
@baozhoutao
baozhoutao deleted the claude/issue-6206-sharelink-full-context branch August 8, 2026 04:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/l tests tooling

Projects

None yet

2 participants