Skip to content

cloud-connection: the marketplace-install route supplies no tenancyPosture to resolveAuthzContext — an ex-member's org-stamped API key is admitted #15353

Description

@hotlong

Censused under #15256 (maintainer ruling 2026-09-04, item 5). That card repaired the REST single-kernel seam; this is the same hole through another door.

This site

packages/cloud-connection/src/marketplace-install-local-plugin.ts:1656

const authz = await resolveAuthzContext({ ql, headers, getSession });

Real request headers, so x-api-key is accepted. No posture.

⚠️ Note for triage: this module already imports resolveTenancyPosture and postureEnforcesWall for its own install-time gating, so a posture is derivable in this file — but the value it derives there is the install path's, and wiring it into the admission seam still has to honour decision 1 option A's registered-and-failing case rather than reuse a fallback that resolves to the requested posture.

The mechanism, unchanged from #15256

resolveAuthzContext gates BOTH posture-conditional API-key refusals on a tenancyPosture its caller supplies:

  • organization_requiredpackages/core/src/security/api-key.ts, if (!tenantId && tenancyPosture)
  • organization_membership_endedpackages/core/src/security/resolve-authz-context.ts, if (keyPrincipal?.tenantId && input.tenancyPosture)

No posture supplied means neither guard runs. The key's tenantId is sys_api_key.active_organization_id copied verbatim — the caller's own stored claim, never vetted against current membership. So under a wall-enforcing posture (isolated), an API key stamped with an organization its owner has left is admitted, carrying that organization as its tenant.

The census this came from

Of the eight non-test resolveAuthzContext callers on main, exactly two supply a posture:

The other six do not. This issue is one of them; the ruling directed one issue per site.

Why #15256's PR did not fix it inline

Ruling item 5 says fix in that PR if it is one line per site. It is not. A correct derivation has to carry decision-1-option-A's classification (#13906): a tenancy service that was never registered is branded and resolves quietly to "no posture", while one that was registered and FAILED to build must raise AuthzStoreUnavailableError (503). A naive try { ... } catch { undefined } at this seam re-introduces precisely the permissive-on-failure defect #13906 was filed to repair — a failure reading as "this check does not apply".

Refs: #15256 (the ruling and the repaired REST seam) · #15163 (the framework measurement) · #13906 (decision 1 option A) · ADR-0105 D2/D3 · ADR-0123 D2.

Blocked-by: #15256

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions