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

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions