Skip to content

[security] plugin-auth's two id-shaped platform-admin judges disagree with resolve-authz-context on three axes — two of them load-bearing on live gates #10949

Description

@os-warren

Measured by the domain:services seat while taking #10348's measurement-only dispatch. Filed by the PM because the measuring seat hit API rate limit already exceeded on every attempt and declined to file blind without a duplicate search — that search has now been run (nothing open).

Disclosure discipline: conclusions and their in-repo basis only. No reproduction recipe. (#9391/#9653/#9756/#9714/#10069 family.)

What was compared

#10348 asked whether three near-identical id-shaped platform-admin reads should be consolidated. Two survive (isOrgOrPlatformAdmin was deleted by #10390 today). The card asserted "all three do the same two reads".

That is overturned. The two plugin-auth spellings agree with each other byte-for-byte, and disagree with the declared authoritypackages/core/src/security/resolve-authz-context.ts, whose own header states that every entry point must resolve authorization through it and never re-read sys_*_permission_set itself — on three axes. Two of those axes are live.

The two live axes

Both auth-manager.ts judges — isPlatformAdminUserId (:4420, which authorizes /sso/register and, via applyPlatformAdminImpersonation, /admin/impersonate-user) and the customSession closure (:2961) —

① apply no ADR-0091 validity window (isGrantActive) and no ADR-0049 active check (isRowActive) to the grant or to the permission-set row. The declared authority applies both before any derivation.
An expired or deactivated grant still authorizes those gates.

② resolve admin_full_access by matching a name over an unfiltered, unordered find('sys_permission_set', { limit: 50 }) page.
An environment with more permission sets than that page holds can lose the row and demote every platform admin at once.

Why this is a defect and not a style difference — the corroboration is all in-repo

None of this rests on an outside standard. This repository already does it correctly, in four places, one of them inside the same package:

plugin-auth's own last-admin-guard.tsalready imports isGrantActive / isRowActive from @objectstack/corefor the same table and the same permission-set name
ensure-default-organization.tsalready spells the same lookup as { where: { name: 'admin_full_access' }, limit: 1 } — targeted, not a page scan
last-admin-guard.ts's scan()deliberately reads maxScan + 1 and refuses rather than truncating — the posture axis ② is missing
packages/qa/dogfood/test/authz-conformance.matrix.ts:305enumerates "every reader that turns a window-carrying row into authority"

That last row is the sharpest part of the finding: auth-manager.ts's judge is a reader of exactly that kind and is absent from the enumeration. So the conformance matrix that exists to catch this class does not contain the instance — the gap is invisible to the very inventory built to see it. That makes the enumeration incomplete too, independently of the two judges.

Direction of the repair

Tightening — fewer principals authorized (an expired or deactivated grant stops authorizing; a targeted lookup stops silently missing the row). It is not a loosening and does not need the maintainer floor on that account.

⚠️ But it does change callers' verdicts, which #10348's dispatch placed behind a hard stop, so it is filed rather than fixed.

Relationship to #10348

#10348 is the consolidation card and its thesis was confirmed, not refuted: N copies of an authorization predicate drift, and the drift is invisible while the copies exist. This finding is what the drift turned out to be. The two are best taken together — consolidating onto resolve-authz-context.ts (option C on #10348) subsumes this defect, because the authority already applies both checks and already spells the lookup targeted.

⚠️ Consolidation also carries a separate open question that is a genuine loosening and is the maintainer's: see #10348's measurement of the withSystemReadContext wrapper on the customSession site.

Refs

Filed unassigned, security + finding, no pm:queue — grading is triage's field.

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions