Skip to content

fix(sharing)!: an edit-level share no longer grants delete (ADR-0111 D3, the verb boundary) - #3975

Merged
os-zhuang merged 1 commit into
mainfrom
claude/sharing-auth-permission-issue-ux7sdd
Jul 30, 2026
Merged

fix(sharing)!: an edit-level share no longer grants delete (ADR-0111 D3, the verb boundary)#3975
os-zhuang merged 1 commit into
mainfrom
claude/sharing-auth-permission-issue-ux7sdd

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

What this is

The P1 implementation of ADR-0111D3, the verb boundary — fixing #3902's second finding. Follows the P0 authorization face (#3965, merged).

update and delete shared one canEdit gate, and canEdit accepts an edit-level share — so one "edit" grant silently conferred delete (the mirror of the retired full level, which promised delete and gave nothing: here we gave more than we said). Every comparable platform keeps these distinct: Salesforce Read/Write cannot delete; Dataverse Delete is an independent privilege; Odoo splits write/unlink.

The change

  • ISharingService.canDelete(object, recordId, context) — ownership (widened by write DEPTH) or the modifyAllRecords super-user bypass (which reaches the gate as __writeScope === 'org') only. An edit or legacy full share does not confer it. canEdit is unchanged (the update gate, share included). The shared ownership/DEPTH fast-path is extracted into one matchesOwnerScope helper so the two gates cannot drift.
  • SharingService.buildWriteFilter(object, context, verb) — a bulk delete({multi:true}) scopes to the owner/DEPTH set alone (no share OR-branch); a bulk update keeps the edit-share widening.
  • Middleware routes delete through canDelete / buildWriteFilter(…, 'delete'), and logs a specific fail-closed reason on a delete denial (ADR-0111 D10 — the tightening is greppable, not a mystery 403).
  • /security/explain consults canDelete for a delete operation (new canDeleteRecord binding), so the record-level explanation matches enforcement by construction.

No new delete access level. Consistent with access-level.ts's stated principle and #3865's direction — a share widens rows, never verbs. A future per-record delete grant would be a capability mask AND-ed with object CRUD (the Dataverse shape), not a fourth access_level.

Breaking

A caller who could delete a record only through an edit-level share (and holds object-level delete CRUD) can no longer delete it — delete now requires ownership, write depth, or Modify All Data. Migration is direct/fail-closed per ADR-0111 D10: deny-log + /security/explain + this changeset's breaking entry, no warn-only window, no escape valve.

Testing

  • canDelete unit matrix: system/public/owner-less allow (matches canEdit); owner yes; non-owner no; edit share → canEdit true but canDelete false; legacy full share likewise; Modify All (__writeScope=org) yes; principal-less no.
  • Middleware: edit-share allows update but rejects delete (403 FORBIDDEN); owner deletes; Modify All deletes a non-owned record; bulk delete filter has no share widening while bulk update does.
  • Full suites green: plugin-sharing 190, plugin-security 673, rest 440, spec 6856. Builds + spec generated-artifact checks (skill-docs / spec-changes / upgrade-guide / authorable-surface / api-surface) all pass; changed files lint clean.

Docs

services.sharing runtime-service page (new canDelete + verb-boundary section), kernel services index, protocol/objectql/security sharing-level note (now precise: the canDelete gate is distinct from canEdit).

Remaining ADR-0111 scope

D8 (share-link re-share rulings) and the DEPTH management extension (D1 D-future) — separate follow-up PRs per the ADR's rollout.

🤖 Generated with Claude Code

https://claude.ai/code/session_012P3RFCpjjiSQ7WuamSDW7W


Generated by Claude Code

…D3, the verb boundary)
update and delete shared one canEdit gate, and canEdit accepts an
edit-level share — so one "edit" grant silently conferred delete (the
opposite error from the retired `full` level, which promised delete and
gave nothing). A share widens which rows a principal reaches, never which
verbs they may use.
- ISharingService.canDelete: ownership (widened by write DEPTH) or the
modifyAllRecords bypass ONLY; an edit/full share does not confer it.
canEdit is unchanged (the update gate).
- SharingService.buildWriteFilter takes a verb param: a bulk delete scopes
to owned rows alone (no share widening), a bulk update keeps it.
- The sharing middleware routes delete through canDelete and logs a
specific fail-closed reason on denial (D10).
- /security/explain consults canDelete for a delete operation, so the
record-level explanation matches enforcement.
Breaking: a caller who could delete only through an edit share can no
longer delete — delete requires ownership, write depth, or Modify All.
No new delete level is introduced.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012P3RFCpjjiSQ7WuamSDW7W
@vercel

vercelBot commented Jul 30, 2026

Copy link
Copy Markdown

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

1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
objectstackIgnoredIgnoredJul 30, 2026 12:31am

Request Review

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tests tooling labels Jul 30, 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.

108 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 @objectstack/plugin-security, 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/v17.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.

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

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@os-zhuang@claude