Skip to content

ISecurityService gains a fail-closed authored-row-write verdict (contract + plugin-security implementation) — #5493 ruling, step 1 #6735

Description

@os-zhuang

Part of #5493 (maintainer ruling 2026-08-08, recorded in #5493 comment 5226377985 and the chat ruling it links: Q1 = A, Q2 = A1).

Why (one measurement carries the whole card)

#5493's fix requires the sharing middleware to defer, before hard-refusing a by-id write, to "an app-authored RLS update-widener admits this row by declaration". No reachable surface answers that today, and the cheap proxy is measurably a security hole: probe E-A (#5493 comment 5226364929) shows a creator-but-no-longer-owner is admitted by the platform created_by floor and refused by sharing with an envelope byte-identical to #5493's — a deferral keyed on "composed RLS admits" would hand transferred records back to their former creators. Separating the two requires policy provenance (isPlatformOwnershipFloorPolicy), which is deliberately private to plugin-security (platform-ownership-policies.ts, ADR-0105 D3 keying).

What (ruled shape — Q1 = A, Q2 = A1)

packages/spec/src/contracts/security-service.ts (ISecurityService) gains a verdict-shaped, by-id method:

  • checkAuthoredRowWrite(object, recordId, operation, context) returning 'admit' | 'abstain' (exact name/signature per contract conventions — implementer's call, trade-offs in the PR body);
  • admit iff at least one applicable non-floor (app-authored) RLS policy matches the row for this operation;
  • abstain in every other case — including a missing method, an unresolvable probe, or a throw. Fail-closed: a deployment without this method behaves byte-for-byte as today. (Same discipline feat(sharing): ISharingService 的每行写判定补三态(放行/不表态/拒绝)(#6428) #6564 pinned for SharingWriteVerdict: a failure is never a widening.)
  • packages/plugins/plugin-security/src/security-plugin.ts implements it on the registered service, reusing the private provenance split fix(plugin-security): enforce both declared write-wideners; the platform baseline becomes explicit-allow (#5492, #5491) #6684 landed (computeRlsFilter internals / RlsFilterOptions.dropPlatformOwnershipFloor / platform-ownership-policies.ts) — no second implementation of RLS evaluation anywhere.
  • Semantic pin the implementation must carry (the E-A hole, as a test): a principal matching only the platform floor (creator of a since-transferred row, no authored policy) gets abstain, never admit.
  • Q2 note, ruled: verdict-shaped by-id ONLY. The bulk-path half of the family is deliberately excluded and tracked separately (see the sibling card filed alongside this one); do not widen this surface toward filters.

Ordering (contract-first, the #6428 chain shape)

This card is the head: this lands → #5493 step 2 (plugin-sharing consumption + the control-pin flip in row-write-widener-composition.test.ts) dispatches in the identity lane. #5493 carries Blocked-by: pointing here.

Boundaries

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions