Skip to content

fix(sharing)!: remove the full access level — it promised delete/transfer/share and granted edit (#3865) - #3901

Merged
os-zhuang merged 3 commits into
mainfrom
claude/full-access-permission-eval-8ja8wl
Jul 28, 2026
Merged

fix(sharing)!: remove the full access level — it promised delete/transfer/share and granted edit (#3865)#3901
os-zhuang merged 3 commits into
mainfrom
claude/full-access-permission-eval-8ja8wl

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Closes #3865.

问题

sys_sharing_rule.access_level / sys_record_share.access_level 提供三档,第三档注释写着 Full Access (Transfer, Share, Delete)。但没有任何一处代码因为 full 而授予删除、转移或再共享——两处仅有的 enforcement 都是 access_level in ('edit','full')fulledit 逐字节等价。管理员在 Setup 选「完全访问」,被告知授予了删除权,实际什么都没多给。这是 ADR-0078 / ADR-0049 所禁的 declared-but-unenforced,与此前摘除 queue recipient 是同一种缺陷。

issue 里的实测显示,full 接收人的 delete 被拒且 decidedBy=object_crud——对象级 CRUD 门在轮到 sharing 之前就拒了

为什么选择摘除而不是实现

对标主流平台后,这不是"缺一个功能",而是模型本就如此:共享放宽的是能碰哪些行,从不放宽能做哪些动作

平台 共享档位 删除 / 转移从哪来
Salesforce 共享规则与手工共享只有 Read-Only / Read-Write Full Access 是系统保留档(所有者 / 角色层级 / Modify All),规则无法授出
Dataverse 单记录共享的 AccessRights 是权限掩码 与安全角色 AND——角色无 Delete 特权,共享给了也删不了
ServiceNow / Odoo 无"完全访问"共享档 动词由 ACL / 独立的 unlink 权限决定

full 想解决的每个真实场景都已有更正确的通道:接手离职同事的客户 → 转移所有者;主管删下属记录 → ADR-0057 的 write DEPTH;管理员清理 → admin scope;跨部门协作 → 保留的 read/edit 两档。这解释了它为什么一直没长出真逻辑。

完整对标见 issue 评论

改动:authoring 拒绝、runtime 容忍、数据归一(ADR-0090 D4 的模式)

  • specSharingLevelShareAccessLevel 收窄为 read | edit;两个对象的 Field.select 同步,Setup 下拉不再出现误导选项(元数据驱动,objectui 无需改动)。
  • 校验SharingService.grant() / SharingRuleService.defineRule() 补上此前完全缺失的 accessLevel 校验。原先任何值都被原样持久化,一个拼错的档位就成了没有任何门会匹配的共享行——同一个 inert-metadata 缺陷的下一层。现在 full 归一为 edit,未知值抛 VALIDATION_FAILED(REST 映射 400)。
  • enforcement 保持更宽 — 读写门仍匹配 edit/full。收窄它会静默撤销所有尚未迁移的授权;authoring 的严与 enforcement 的宽是两件事。
  • 存量数据 — boot backfill 归一两张表的 full 行(以 isSystem 写入,避免 provenance hook 把 package 规则误标 customized);sharing-rule-access-level-full-to-edit conversion 在 load 时重写声明式 stack。
  • explain 面保留 'full' — 那是对已存在行的呈现面,legacy 行必须仍可解释,而不是让面板崩溃。

迁移

无需消费者操作。 两档本就行为等价,所以重写不可能改变任何访问判定——这与 ADR-0090 D4 摘除 OWD sharingModel: 'full' 不同(那次改变了 posture,只能交给作者判断)。仍在声明 accessLevel: 'full' 的 stack 在 load 时带 deprecation notice 转换;存量行在下次启动归一。将 ShareAccessLevel 类型钉在 'full' 的代码不再编译,改用 'edit'

验证

  • pnpm build 71/71 通过(含类型)。
  • 单测:spec 6839 项、objectql 1163 项、plugin-security 673 项、plugin-sharing 134 项、plugin-audit 46 项、rest 179 项全通过。新增 access-level.test.ts 与 backfill 幂等/失败降级用例。
  • i18n:4 个 locale 经 os i18n extract 重新生成,check:i18n-bundles / check:i18n-coverage 通过。
  • 运行时实测(真实 CRM 后端):live 元数据只剩 read/edit;grant full → 201 且落库为 edit;grant admin → 400;直接写入 SQLite 的 legacy full 行重启后变为 edit,且 managed_by / customized 未被改动。

全仓 pnpm test 在本容器中有 3 个包(plugin-audit / objectql / plugin-security)因并行资源争用出现 worker 退出与超时;逐个单独复跑全部通过,非本改动导致。

后续(不在本 PR 范围)

评估过程中另外发现两处,建议单独跟进:

  1. edit 级共享目前已允许删除 —— sharing-plugin.tsupdate/delete 走同一个 canEdit 门。Salesforce 的 Read-Write 共享是明确不能删除的。收紧属破坏性变更,需独立 ADR。
  2. 共享端点缺授权检查 —— POST /data/:object/:id/shares 只做 enforceAuth(登录态),随后以 SYSTEM_CTX 写入共享行,没有"调用者是否有权共享这条记录"的判断,revoke 同样。可能构成提权,将另开 issue。

真要做 per-record 授删,应按 Dataverse 模式设计:权限掩码而非线性档位、与对象 CRUD 做 AND、先建共享管理权、同步收紧 edit——那是独立的 ADR 级项目,不应与"消除界面误导"的修复捆绑。


Generated by Claude Code

…ansfer/share and granted `edit` (#3865)

`sys_sharing_rule.access_level` / `sys_record_share.access_level` offered three
levels, the third documented as **Full Access (Transfer, Share, Delete)**. No
code path granted transfer, re-share, or delete because of it: both enforcement
sites matched `access_level in ('edit','full')`, so `full` was byte-equivalent
to `edit`. An admin picking "Full Access" in Setup was told they had granted
delete rights and had not — declared-but-unenforced metadata (ADR-0078,
ADR-0049), the same defect that retired the `queue` recipient before it.

Measured on showcase, a `full` recipient got read/update allowed and delete
DENIED with `decidedBy=object_crud` — the object-level CRUD gate rejected the
delete *before* sharing was consulted at all. That is the model working, not an
oversight to patch around. Record sharing widens WHICH ROWS a principal
reaches, never WHICH VERBS they may use: Salesforce sharing rules stop at
Read-Only / Read-Write (its Full Access is owner / hierarchy / Modify All only,
never grantable by a rule), and Dataverse ANDs every shared access right
against the security role's own privilege. Delete and transfer belong to
ownership, the ADR-0057 DEPTH scopes, and admin scope.

Authoring rejects, enforcement tolerates, data normalises (the ADR-0090 D4
idiom):

- `SharingLevel` (spec/security) and `ShareAccessLevel` (spec/contracts) narrow
  to `read | edit`; the `Field.select` on both objects offers the same two, so
  the Setup dropdown no longer shows the misleading option.
- `SharingService.grant()` and `SharingRuleService.defineRule()` gain the
  access-level validation they never had. Previously ANY value was persisted
  verbatim, so a typo'd level became a grant no gate would ever match — the
  same inert-metadata bug one layer down. `full` normalises to `edit`; anything
  unrecognised is a `VALIDATION_FAILED` the REST layer maps to 400.
- The read/write gates keep matching `edit`/`full` on purpose. Narrowing them
  would silently REVOKE every not-yet-migrated grant; authoring narrowness and
  enforcement tolerance are different jobs.
- A boot backfill normalises stored `full` rows on both tables (writing with
  `isSystem` so the provenance hook does not mark a package-seeded rule
  `customized`), and the `sharing-rule-access-level-full-to-edit` conversion
  rewrites declarative stacks at load.

Lossless by construction: the two levels were already equivalent, so the
rewrite cannot change an access decision — unlike the OWD `sharingModel:
'full'` alias retired in ADR-0090 D4, which changed posture and had to be
delegated to the author. The explain surfaces keep accepting `'full'`: they
REPORT stored rows, and a legacy row must stay explainable rather than crash
the panel.

Verified on a running CRM backend: live metadata offers only read/edit; a grant
of `full` returns 201 persisted as `edit`; a bogus level returns 400; and
legacy `full` rows seeded directly into SQLite came back `edit` after reboot
with `managed_by`/`customized` untouched.

Reviving a real per-record delete grant is a separate design — a capability
mask ANDed with object CRUD, plus the share-administration model that would
have to authorise re-sharing — not a fourth enum member.

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

vercel Bot commented Jul 28, 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 Jul 28, 2026 3:44pm

Request Review

Conflict: `CONVERSIONS_BY_MAJOR` — main opened protocol 17 with three
alias-removal conversions while this branch had filed the `full` → `edit`
rewrite under 16.

Resolved by moving this branch's entry into 17, which is the correct major
regardless of the conflict: 16 has already SHIPPED (`PROTOCOL_VERSION`
16.0.0), and a conversion retires at `toMajor + 1`, so leaving it at 16 would
have given a stack still authoring `accessLevel: 'full'` no acceptance window
at all on the very next release. The migration-chain entry moves from step16 to
step17 with it.

Unlike its three step-17 siblings, this entry is NOT `retiredFromLoadPath`:
those are already-deprecated keys whose schemas tombstone them with a fix-it
error, whereas `full` carried no prior deprecation and a removed enum VALUE
yields only a generic zod message. It therefore keeps the ADR-0087 D2 default
one-major window — zero-risk here precisely because the rewrite is
behaviour-preserving.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017346r3TMNqpbTLkT5d49uQ
@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling labels Jul 28, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 3 package(s): @objectstack/plugin-security, @objectstack/plugin-sharing, @objectstack/spec.

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

  • content/docs/ai/agents.mdx (via @objectstack/spec)
  • content/docs/ai/skills-reference.mdx (via @objectstack/spec)
  • content/docs/ai/skills.mdx (via @objectstack/spec)
  • content/docs/api/client-sdk.mdx (via @objectstack/spec)
  • content/docs/api/environment-routing.mdx (via @objectstack/spec)
  • content/docs/api/error-catalog.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-client.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-server.mdx (via @objectstack/spec)
  • content/docs/api/index.mdx (via @objectstack/spec)
  • content/docs/automation/approvals.mdx (via packages/spec)
  • content/docs/automation/flows.mdx (via @objectstack/spec)
  • content/docs/automation/hook-bodies.mdx (via packages/spec)
  • content/docs/automation/hooks.mdx (via @objectstack/spec)
  • content/docs/automation/index.mdx (via @objectstack/spec)
  • content/docs/automation/webhooks.mdx (via @objectstack/spec)
  • content/docs/automation/workflows.mdx (via @objectstack/spec)
  • content/docs/concepts/architecture.mdx (via @objectstack/spec)
  • content/docs/concepts/design-principles.mdx (via packages/spec)
  • content/docs/concepts/index.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-driven.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-lifecycle.mdx (via packages/spec)
  • content/docs/concepts/north-star.mdx (via packages/spec)
  • content/docs/data-modeling/analytics.mdx (via @objectstack/spec)
  • content/docs/data-modeling/drivers.mdx (via @objectstack/spec)
  • content/docs/data-modeling/external-datasources.mdx (via @objectstack/spec)
  • content/docs/data-modeling/field-types.mdx (via @objectstack/spec)
  • content/docs/data-modeling/fields.mdx (via @objectstack/spec)
  • content/docs/data-modeling/formulas.mdx (via @objectstack/spec)
  • content/docs/data-modeling/index.mdx (via @objectstack/spec)
  • content/docs/data-modeling/objects.mdx (via @objectstack/spec)
  • content/docs/data-modeling/queries.mdx (via @objectstack/spec)
  • content/docs/data-modeling/schema-design.mdx (via @objectstack/spec)
  • content/docs/data-modeling/seed-data.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation-rules.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation.mdx (via @objectstack/spec)
  • content/docs/deployment/cli.mdx (via @objectstack/plugin-security, @objectstack/spec)
  • content/docs/deployment/troubleshooting.mdx (via @objectstack/spec)
  • content/docs/deployment/validating-metadata.mdx (via @objectstack/spec)
  • content/docs/getting-started/build-with-claude-code.mdx (via @objectstack/spec)
  • content/docs/getting-started/common-patterns.mdx (via @objectstack/spec)
  • content/docs/getting-started/examples.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-reference.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-start.mdx (via @objectstack/spec)
  • content/docs/getting-started/your-first-project.mdx (via @objectstack/spec)
  • content/docs/kernel/cluster.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/auth-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/cache-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/data-engine.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/index.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/metadata-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/storage-service.mdx (via packages/spec)
  • content/docs/kernel/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/email-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/queue-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/sharing-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/sms-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/storage-service.mdx (via packages/spec)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/spec)
  • content/docs/permissions/access-recipes.mdx (via packages/plugins/plugin-security)
  • content/docs/permissions/authorization.mdx (via @objectstack/plugin-security, packages/plugins/plugin-sharing, @objectstack/spec)
  • content/docs/permissions/explain.mdx (via @objectstack/plugin-security)
  • content/docs/permissions/permission-sets.mdx (via @objectstack/spec)
  • content/docs/permissions/permissions-matrix.mdx (via packages/plugins/plugin-security, packages/plugins/plugin-sharing, @objectstack/spec)
  • content/docs/permissions/positions.mdx (via @objectstack/spec)
  • content/docs/permissions/rls.mdx (via @objectstack/spec)
  • content/docs/permissions/sharing-rules.mdx (via @objectstack/plugin-security, @objectstack/spec)
  • content/docs/plugins/adding-a-metadata-type.mdx (via @objectstack/spec)
  • content/docs/plugins/development.mdx (via @objectstack/spec)
  • content/docs/plugins/index.mdx (via @objectstack/plugin-security, @objectstack/spec)
  • content/docs/plugins/packages.mdx (via @objectstack/plugin-security, @objectstack/plugin-sharing, @objectstack/spec)
  • content/docs/protocol/backward-compatibility.mdx (via @objectstack/spec)
  • content/docs/protocol/diagram.mdx (via packages/spec)
  • content/docs/protocol/kernel/config-resolution.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/i18n-standard.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/lifecycle.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/plugin-spec.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/runtime-capabilities.mdx (via @objectstack/spec)
  • content/docs/protocol/knowledge.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/query-syntax.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/schema.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/security.mdx (via packages/plugins/plugin-sharing, packages/spec)
  • content/docs/protocol/objectql/state-machine.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/actions.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/concept.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/layout-dsl.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/record-alert.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/widget-contract.mdx (via @objectstack/spec)
  • content/docs/releases/implementation-status.mdx (via @objectstack/plugin-security, @objectstack/spec)
  • content/docs/releases/index.mdx (via @objectstack/spec)
  • content/docs/releases/v12.mdx (via @objectstack/spec)
  • content/docs/releases/v13.mdx (via @objectstack/spec)
  • content/docs/releases/v16.mdx (via @objectstack/spec)
  • content/docs/releases/v9.mdx (via @objectstack/spec)
  • content/docs/ui/actions.mdx (via @objectstack/spec)
  • content/docs/ui/audience-based-interfaces.mdx (via packages/plugins/plugin-security)
  • content/docs/ui/create-vs-edit-form.mdx (via @objectstack/spec)
  • content/docs/ui/dashboards.mdx (via @objectstack/plugin-security, @objectstack/spec)
  • content/docs/ui/forms.mdx (via @objectstack/spec)
  • content/docs/ui/index.mdx (via @objectstack/spec)
  • content/docs/ui/public-data-collection.mdx (via @objectstack/spec)
  • content/docs/ui/setup-app.mdx (via @objectstack/spec)
  • content/docs/ui/translations.mdx (via @objectstack/spec)
  • content/docs/ui/views.mdx (via @objectstack/spec)

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.

… sharing level

`content/docs/references/` is generated from the zod schemas and is checked in
CI (`@objectstack/spec check:docs`); narrowing `SharingLevel` left it stale.

- `security/sharing.mdx` — `accessLevel` is now `read | edit` and the enum
  member list drops `full`.
- `security/explain.mdx` — `grants` deliberately still lists `full`; only its
  description changes, since explain reports stored rows and a legacy row must
  stay explainable until the boot backfill normalises it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017346r3TMNqpbTLkT5d49uQ
@os-zhuang
os-zhuang marked this pull request as ready for review July 28, 2026 16:01
@os-zhuang
os-zhuang merged commit f00d8d4 into main Jul 28, 2026
17 checks passed
@os-zhuang
os-zhuang deleted the claude/full-access-permission-eval-8ja8wl branch July 28, 2026 16:02
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

Development

Successfully merging this pull request may close these issues.

fix(sharing): 「完全访问」声明了删除/转移/共享,实现上与「编辑」等价

2 participants