Skip to content

Decision: may an org-level sharing admin DELETE a platform-global (organization_id = null) sharing rule? #7795

Description

@os-help

Raised from #7761 (PR fixing getRule's by-id org filter). #7761's triage explicitly reserved this second axis for a maintainer ruling rather than a dev's judgment, so it is filed here so the question survives #7761 closing. No behaviour was changed for it#7761's PR implements only the unambiguous half (another organization's row is unreachable by id).

The question

After #7760 and #7761, a platform-global sharing rule — organization_id = null, written by the boot seeder from declared metadata — is deliberately reachable by an org-scoped admin, by name and by id, for symmetry between the two address forms. Reachable currently means all three verbs, because getRule, evaluateRule and deleteRule share one resolution path.

So an admin holding manage_sharing in one organization may delete a rule that belongs to none, and deleteRule purges every sys_record_share grant it materialised — across every tenant, since a null-org rule's criteria query runs unscoped under SYSTEM_CTX.

Should read/evaluate/delete split for the null-org case?

Why this is not a dev call

manage_sharing is declared scope: 'org' in packages/spec/src/security/capabilities.ts:59. The capability vocabulary already distinguishes org from platform authority — manage_platform_settings and manage_metadata are scope: 'platform', and there is a built-in platform_admin position (ADR-0068 D2) — but SharingRuleService.assertCanManageRules accepts manage_sharingormanage_platform_settings interchangeably and never consults scope. Deciding that an org-scoped capability does not authorize a cross-tenant destructive act, and choosing which capability does, is a contract decision about the authority model, not an implementation detail.

Two measured facts that bear on it

1. The delete is impermanent, so it is a revocation rather than a removal.bootstrapDeclaredSharingRules re-seeds declared rules on every boot (sharing-plugin.ts:568), and the boot backfill re-materialises grants. Deleting a seeded rule therefore does not remove it — it revokes every tenant's grants under it until the next restart, then the rule returns under a new id. That is the profile of an outage, not of an administrative change.

2. The safe lever is unavailable while the destructive one is not. An org admin cannot deactivate a platform-global rule: defineRule's existence lookup is deliberately strict ({name, organization_id: orgId}, held that way by #7676 so one org cannot upsert over a row other orgs read), so active: false from an org admin creates a new org-stamped row and leaves the shared one running. The only lever an org admin has over a platform-global rule today is deleting it. The scoped, reversible operation is refused and the cross-tenant, irreversible-until-reboot one is permitted — the inverse of the safe arrangement.

Options

A. Status quo — read, evaluate and delete all permitted on null-org rows. Cheapest; keeps one resolution path. Leaves an org-level capability authorizing a cross-tenant grant purge, and leaves fact 2's inversion in place.

B. Split: read and evaluate permitted, delete refused unless the caller carries platform authority (manage_platform_settings / the platform_admin position). Preserves everything #7760 shipped — listing, inspection, evaluation of seeded rules — and removes only the cross-tenant destructive act from org-scoped callers. Costs a real split in deleteRule, plus a decision on the error surface (403 PERMISSION_DENIED reads more honestly than a 404 here, since the row is deliberately visible).

C. Refuse delete of null-org rows for every non-system caller. Simplest to reason about and matches fact 1 — the row is owned by the deployment's declared metadata, so the API is not where it should be removed. Strictly less flexible than B, and a genuine platform admin then has no API path to remove a seeded rule.

D. B or C, plus give org admins a scoped opt-out — an org-level "suppress this platform-global rule for my organization" that does not touch the shared row. Fixes fact 2 properly instead of only removing the dangerous lever. Materially more design and a new stored concept, so this needs its own business-need judgment.

Analysis on the three axes

Real business need. Measured, not asserted: nothing in this repo deletes a platform-global rule through the API. The only in-repo delete callers are the REST route and tests; the boot seeder only ever defines. The demand for "an org admin removes a shared seeded rule" is not evidenced anywhere in the example apps, and fact 1 shows it would not even work as intended. The demand that is evidenced is #7676's — admins need to see and evaluate seeded rules — and B and C both keep that whole. Under the startup-focus principle this argues against preserving an unexercised destructive capability, and against D until someone actually asks for per-org suppression.

Long-term soundness. B or C aligns with the direction #7676/#7760 already took: scope reads generously, keep writes strictly owned. Option A is the workaround-shaped choice — it leaves the authority model saying one thing (scope: 'org') while the enforcement seam does another, which is the class of drift ADR-0111 D6 exists to prevent.

Making AI-written / metadata-authored apps hard to get wrong. A platform-global rule is a declared metadata artifact. Letting a tenant-level API call delete a declared artifact — impermanently, so it silently returns at the next boot with a new id — is exactly the kind of surface where an automated admin action produces a confusing half-state. Refusing it loudly at the service boundary is the structural prevention.

Recommendation: B, with C as the acceptable simpler ruling. B keeps every capability #7760 deliberately unlocked, removes only the cross-tenant destructive one from org-scoped callers, and expresses the platform/org distinction the capability registry already declares. I would not do D without evidence of demand for per-org suppression. Whatever is chosen, fact 2 is worth recording: if delete is closed off, an org admin has no lever at all over a platform-global rule, which may itself argue for D later.

Filed unassigned.


Generated by Claude Code

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions