Skip to content

finding: the ADR-0106 D4 read-exemption set and #6603's write-capability gate are different sets, so "whoever can write a schema can see all of it" is not actually enforced #7020

Description

@os-project-manager

Filed from the #6603 implementation. #6603's ruling (5225531464) justifies the gate by making an invariant out of a coincidence — "whoever can write a schema is whoever can see the full schema". The gate it names demands manage_metadata. This card records the measurement that those are two different sets, so the gate as ruled does not by itself establish that invariant.

⛔ This is a record of a mismatch, not a proposal to change #6603's gate. The maintainer's mechanism was ruled twice and is independently justified by its second reason (closing the pre-existing "any authenticated session can clobber a schema" hole). Which way to align the two sets — if at all — is the maintainer's call, and this card deliberately does not choose.

The three measured facts

packages/metadata-core/src/object-schema-fls.ts:72:

export const OBJECT_SCHEMA_MASK_EXEMPT_CAPABILITIES: readonly string[] = ['studio.access', 'setup.access'];

manage_metadata is not in it. Consequences, all measured against the shipped permission sets in packages/plugins/plugin-security/src/objects/default-permission-sets.ts:

  1. organization_admin is D4-exempt but gate-refused. It carries ['manage_org_users', 'setup.access', 'setup.write'] (:190) — setup.access makes it exempt from the mask, so its GET was never masked and it never had the round-trip hazard finding: after ADR-0106, a restricted caller's GET → edit → PUT of an object schema DELETES the fields that were masked out of their read #6603 exists to close; yet it holds no manage_metadata, so finding: after ADR-0106, a restricted caller's GET → edit → PUT of an object schema DELETES the fields that were masked out of their read #6603's gate refuses its writes. (Arguably correct on its own terms — the set's own comment says a tenant does not mutate shared metadata — but it is refused for a reason unrelated to the ruling's stated rationale.)

  2. A manage_metadata-only caller passes the gate and still reads masked. Nothing in the D4 exemption reads manage_metadata, so such a caller's GET is projected — and their GET → edit → PUT still deletes the fields they could not see. The hazard finding: after ADR-0106, a restricted caller's GET → edit → PUT of an object schema DELETES the fields that were masked out of their read #6603 closes for everyone else stays open for exactly the callers the gate lets through. Pinned as measured behaviour in packages/rest/src/meta-item-save-capability-gate.test.ts (the it.each capability matrix).

  3. The two sets meet only on admin_full_access, which carries manage_metadata + studio.access + setup.access (:120-129) and is the only shipped set holding studio.access. That co-occurrence — not the gate — is what makes the invariant hold in a default deployment. A hand-authored permission set granting one without the other separates them immediately, in either direction.

Why this is finding-class rather than a defect

Nothing a user hits on a default deployment today: the shipped sets do not separate the two capabilities on any path that reaches a metadata-authoring surface (verified in the #6603 caller survey — every in-repo PUT /meta/:type/:name caller runs under Studio, which requires studio.access, or as the dev admin). It is filed because the stated rationale of a merged ruling is not what the merged code enforces, and that gap is invisible to anyone reading either one alone. Severity graded at filing time is unreliable in both directions — please re-grade at triage.

Related: #6603 and its ruling 5225531464; ADR-0106 D4; ADR-0066 D1 (manage_metadata); the sibling card on the doors #6603's gate leaves open.

Filing note: the duplicate search over open issues was run once and then blocked by a GitHub API rate limit, so the usual sweep is incomplete. Race-close if this twins an existing card.

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions