You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
security(rest): the external-datasource federation family still admits any authenticated caller, while its declared admin twin now requires a capability #9901
Found while implementing #9593 (the datasource-admin capability gate). Filed rather than fixed there: it is a different registrar in a different package, and #9593's file surface was ruled to be the datasource-admin family.
Disclosure posture — inherited deliberately
⛔ No reproduction here, in any PR body, or in any commit message.#9391 was revised on 2026-08-17 to remove its request/response table after the maintainer deleted the public reproduction, and #9686 inherited that posture for this same area. What follows is the cause, the source location, and the contract a fix would restore.
What is now true
listRemoteTables and generateObjectDraft are each reachable through two mounted routes, in two packages, and the two doors no longer apply the same admission policy:
operation
admin spelling (@objectstack/service-datasource)
federation spelling (@objectstack/rest)
listRemoteTables
GET /api/v1/datasources/:name/remote-tables
GET /api/v1/datasources/:name/external/tables
generateObjectDraft
POST /api/v1/datasources/:name/object-draft
POST /api/v1/datasources/:name/external/tables/:remote/draft
So an authenticated caller holding no platform capability is refused live remote-schema introspection at one spelling and served it at the other — same operation, same external-datasource service slot, same method, two doors, one gate.
Cause
Both guards are correct for what each was scoped to; the asymmetry is the residue of the two cards being deliberately split.
#9686 made the same call from the other side: "Whether these routes should require a capability beyond authentication is a separate question of the kind #9593 answers for the admin family, and should not be folded in." Both deferrals were right individually; this card is the question they both deferred to, now that #9593 has answered it for the admin half.
Why it matters beyond tidiness
This family's equivalence is a maintained invariant, not a coincidence — #4249 gave the two spellings one failure contract, #7955 one request shape, #9686 one admission answer on the anonymous axis. The capability axis is the first on which they are pinned as disagreeing.
The federation family is also the larger surface: it carries three routes the admin spelling has no twin for, two of which write (POST .../external/tables/:remote/import creates a live runtime-origin federated object; POST .../external/refresh-catalog). Those are not covered by #9593 at all and are on the same authentication-only footing.
Unlike the admin spelling, this family is SDK-expressed (datasources.external.* on ObjectStackClient, per the REST route ledger), so any fix must be checked against those callers presenting a credential — that is the half most likely to break, and the reason this is a decision rather than a mechanical copy.
Where it is currently pinned
packages/rest/src/remote-tables-twin.equivalence.test.ts — the twin-equivalence suite — was updated by #9593's PR to record the divergence explicitly rather than hide it, in a case labelled as a record of a known gap. ⚠️That case is expected to FAIL when this card is fixed, and the fix should fold the row back into the agreement block above it rather than delete it. Read the block header there before changing it.
What a fix would look like (a lead, not a decision)
The federation registrar already receives resolveExecutionContext from direct-mount-composition.ts, and @objectstack/rest's sibling package-routes.ts already reads systemPermissions off that resolved context for its own capability gate — so the seam exists and the shape is established in this package. The open questions are genuinely open and should not be answered by copying:
The SDK callers.datasources.external.* is published surface; a capability requirement is a behaviour change for anyone using it today.
Whether the two spellings should converge at all, or whether the federation family is deliberately the lower-privilege door for a use case the admin family is not. That question is what makes this a card rather than a patch.
Backlinks: #9593 (the admin half, where the capability was measured) · #9686 (this family's authentication guard, which deferred this question) · #9391 (the admin family's authentication guard) · #7955 / #4249 (the twin-equivalence invariants).
Unassigned and unqueued — for the triage seat to level and route.
Found while implementing #9593 (the datasource-admin capability gate). Filed rather than fixed there: it is a different registrar in a different package, and #9593's file surface was ruled to be the datasource-admin family.
Disclosure posture — inherited deliberately
⛔ No reproduction here, in any PR body, or in any commit message.#9391 was revised on 2026-08-17 to remove its request/response table after the maintainer deleted the public reproduction, and #9686 inherited that posture for this same area. What follows is the cause, the source location, and the contract a fix would restore.
What is now true
listRemoteTablesandgenerateObjectDraftare each reachable through two mounted routes, in two packages, and the two doors no longer apply the same admission policy:@objectstack/service-datasource)@objectstack/rest)listRemoteTablesGET /api/v1/datasources/:name/remote-tablesGET /api/v1/datasources/:name/external/tablesgenerateObjectDraftPOST /api/v1/datasources/:name/object-draftPOST /api/v1/datasources/:name/external/tables/:remote/draft401401403 PERMISSION_DENIED(#9593)manage_platform_settingsSo an authenticated caller holding no platform capability is refused live remote-schema introspection at one spelling and served it at the other — same operation, same
external-datasourceservice slot, same method, two doors, one gate.Cause
Both guards are correct for what each was scoped to; the asymmetry is the residue of the two cards being deliberately split.
registerDatasourceAdminRoutes(packages/services/service-datasource/src/admin-routes.ts) applies the shared anonymous floor and, as of security(service-datasource): datasource-admin routes require a platform-settings capability beyond authentication #9593, requiresmanage_platform_settings— matched to what the sibling Setup-admin families gate on.registerExternalDatasourceRoutes(packages/rest/src/external-datasource-routes.ts) applies the shared anonymous floor only. Its own guard doc says so in as many words: "Authentication and nothing more: whether these routes should FURTHER require a capability is the separately-ruled question security(service-datasource): datasource-admin routes require a platform-settings capability beyond authentication #9593 asks of the admin family, and is deliberately absent here."#9686 made the same call from the other side: "Whether these routes should require a capability beyond authentication is a separate question of the kind #9593 answers for the admin family, and should not be folded in." Both deferrals were right individually; this card is the question they both deferred to, now that #9593 has answered it for the admin half.
Why it matters beyond tidiness
This family's equivalence is a maintained invariant, not a coincidence — #4249 gave the two spellings one failure contract, #7955 one request shape, #9686 one admission answer on the anonymous axis. The capability axis is the first on which they are pinned as disagreeing.
The federation family is also the larger surface: it carries three routes the admin spelling has no twin for, two of which write (
POST .../external/tables/:remote/importcreates a live runtime-origin federated object;POST .../external/refresh-catalog). Those are not covered by #9593 at all and are on the same authentication-only footing.Unlike the admin spelling, this family is SDK-expressed (
datasources.external.*onObjectStackClient, per the REST route ledger), so any fix must be checked against those callers presenting a credential — that is the half most likely to break, and the reason this is a decision rather than a mechanical copy.Where it is currently pinned
packages/rest/src/remote-tables-twin.equivalence.test.ts— the twin-equivalence suite — was updated by #9593's PR to record the divergence explicitly rather than hide it, in a case labelled as a record of a known gap.What a fix would look like (a lead, not a decision)
The federation registrar already receives
resolveExecutionContextfromdirect-mount-composition.ts, and@objectstack/rest's siblingpackage-routes.tsalready readssystemPermissionsoff that resolved context for its own capability gate — so the seam exists and the shape is established in this package. The open questions are genuinely open and should not be answered by copying:manage_platform_settingswith no read/write split for the admin family; this family's write routes (import,refresh-catalog) create metadata, which ismanage_metadataterritory in this package's other gate.datasources.external.*is published surface; a capability requirement is a behaviour change for anyone using it today.Backlinks: #9593 (the admin half, where the capability was measured) · #9686 (this family's authentication guard, which deferred this question) · #9391 (the admin family's authentication guard) · #7955 / #4249 (the twin-equivalence invariants).
Unassigned and unqueued — for the triage seat to level and route.
Generated by Claude Code