Skip to content

Commit 77e19a2

Browse files
fix(service-datasource): the admin routes derive the tenancy posture — an ex-member's org-stamped API key is no longer admitted (#16011)
* fix(service-datasource): derive the tenancy posture at the admin-routes admission seam The datasource ADMIN routes accept real request headers, so `x-api-key` is admitted there — but `requireDatasourceAdmin` called `resolveAuthzContext` with no `tenancyPosture`, and both posture-conditional API-key refusals are gated on the caller supplying one. Neither ran, so under a wall-enforcing posture an API key stamped with an organization its owner has left was admitted and the family then gated it on `authz.systemPermissions` alone. The posture is classified per #13906 decision 1 option A: a `tenancy` service that was NEVER REGISTERED is branded and resolves quietly to "no posture"; one that was registered and FAILED to build raises `AuthzStoreUnavailableError`, which `requireDatasourceAdmin`'s existing catch re-raises rather than laundering into a denial (#13279). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ARYe3yQTQCUFm5qPYNgKaJ * test(service-datasource): the admin-routes API-key admission matrix, its permanent wiring ablation and the two option-A arms 29 cases on one registrar: controls in both directions (a current member reads and writes, an anonymous caller is refused, an entitled-less key gets the capability refusal), the ex-member and organization-less subject rows under `isolated`, and a permanently-held ablation whose handle is the wiring this package shipped before this card — a PluginContext with no `getKernel`. Writes are read back FROM THE DATASOURCE STORE, never from the response body. `group` is MEASURED rather than assumed: the ex-member is refused there too (`postureEnforcesWall('group')`), while the organization-less key is admitted (union scope). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ARYe3yQTQCUFm5qPYNgKaJ * chore(changeset): @objectstack/service-datasource patch; pin the outage class rather than its digits Sized as #15365 was: a fail-closed bug fix, no accept set widens. The outage arm now asserts the outage CLASS. Measured on this tree, the family answers 500 INTERNAL_ERROR rather than the 503 SERVICE_UNAVAILABLE the brand carries — a pre-existing relay gap this card did not open (the same escape has existed for the `ql` permission-store outage since #13279) and does not repair. Asserting the class keeps the pin on the security property. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ARYe3yQTQCUFm5qPYNgKaJ * test(service-datasource): type the harness app off the adapter's own accessor `tsc --noEmit` reds on a hand-written `{ fetch: (req: Request) => Promise<Response> }`: Hono's `fetch` takes an env and an execution context after the request, so the narrower signature is a type error rather than a simplification. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ARYe3yQTQCUFm5qPYNgKaJ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent c91feb2 commit 77e19a2

3 files changed

Lines changed: 733 additions & 1 deletion

File tree

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
'@objectstack/service-datasource': patch
3+
---
4+
5+
The datasource admin routes derive the tenancy posture before resolving the caller
6+
7+
`requireDatasourceAdmin` resolved the request with `resolveAuthzContext({ ql, headers, getSession })` and supplied no `tenancyPosture`. Both posture-conditional API-key refusals are gated on the caller supplying one — `organization_required` and `organization_membership_ended` — so neither ran on this family, and an API key stamped with an organization its owner had left was admitted; the routes then gated it on `authz.systemPermissions` alone. Because this family gates on system capabilities rather than on organization-scoped rows, the consequence was an admitted principal rather than a cross-organization row read.
8+
9+
The posture is now read off the kernel's `tenancy` service and classified rather than swallowed: a service that was never registered stays quiet (`undefined` — the supported no-tenancy composition, unchanged behaviour), while one that was registered and failed to build raises `AuthzStoreUnavailableError` instead of degrading to "no posture". Patch rather than minor: no accept set widens, and a declared guard returns to enforced.

0 commit comments

Comments
 (0)