Repository navigation
security(rest): the external-datasource federation family still admits any authenticated caller, while its declared admin twin now requires a capability #9901
Description
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 |
| anonymous | 401 |
401 |
| authenticated, no capability | 403 PERMISSION_DENIED (#9593) |
served |
authenticated + manage_platform_settings |
served | served |
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.
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/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.
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:
- Which capability, and whether it is the same one for read and write here. security(service-datasource): datasource-admin routes require a platform-settings capability beyond authentication #9593 measured
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. - 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.
Generated by Claude Code
Activity
test(rest): entitle the listRemoteTables twin fixture and pin the new…
os-warren commentedon Aug 19, 2026
Triage: lands in
packages/rest/src/external-datasource-routes.ts⇒domain:cli; routed to the maintainer inbox (needs-user-decision), type Task — a security/permission-boundary question is on the human floor regardless of facet alignment, and the card's own question 3 (is the federation family deliberately the lower-privilege door?) is a product call.listRemoteTablesroute twins diverge on?schema=— the admin spelling drops it #7955 request shape, finding: the external-datasource federation HTTP family mounts outside the enforceAuth seam with no guard of its own — read and write #9686 anonymous axis); the capability axis is the first pinned disagreement — converging shrinks a special case, and the write routes (import,refresh-catalog) creating live metadata behind auth-only is the sharpest edge.remote-tables-twin.equivalence.test.ts; no user has asked for a lower-privilege introspection door — butdatasources.external.*IS published SDK surface, so a new capability requirement is a behaviour change for any real caller presenting a plain credential.resolveExecutionContext+systemPermissions, already read bypackage-routes.ts) — no new mechanism; the decision cost is the SDK-caller break, not the code.Recommendation: A — require a capability on the federation family:
manage_platform_settingsfor the read twins (matching #9593), andmanage_metadatafor the two write routes (import,refresh-catalog), since they create metadata and that is this package's established gate for metadata creation; fold the equivalence-test divergence row back into the agreement block. B — mirror #9593 exactly (manage_platform_settingson everything, no read/write split). C — rule the federation family deliberately lower-privilege and document it as such.Confidence gap: this analysis cannot see real-world
datasources.external.*SDK callers — if any production integration calls these routes with a non-admin credential, A and B both break it, and the migration story (staged warn? version gate?) has not been designed.Generated by Claude Code
os-zhuang commentedon Aug 20, 2026
Maintainer ruling recorded (2026-08-20, live decision-inbox session with the triage seat, session
session_01PjAP6vbcsg2yMtvySPv1Qo)Ruled: tighten the federation family. Provenance: maintainer accepted the batch, verbatim: 「其他接受你的建议。」; this card's presented recommendation:
external/tables,external/tables/:remote/draft) gate onmanage_platform_settings— converging with the security(service-datasource): datasource-admin routes require a platform-settings capability beyond authentication #9593-measured admin twin, restoring the maintained twin-equivalence invariant on the capability axis.external/tables/:remote/import,external/refresh-catalog) gate onmanage_metadata— they create live metadata, matching this package's existing metadata-write gate.remote-tables-twin.equivalence.test.tsknown-gap case is expected to flip — fold the row back into the agreement block per its own header, ⛔ don't delete it.datasources.external.*is published SDK surface — the changeset states the new capability requirement plainly for existing credentialed callers.The seam exists (
resolveExecutionContextalready handed to the registrar;package-routes.tsis the in-package precedent). Clause-②: yes (accept-set narrowing) — contract-review tier. Question 3 of the body (deliberate lower-privilege door?) is answered in the negative by this ruling.State transition in the same stroke:
needs-user-decision→pm:queue.Generated by Claude Code
security(service-datasource): require manage_platform_settings on the…
security(rest): require a capability on the external-datasource feder…
security(rest): require manage_platform_settings on POST /datasources…
docs(qa): correct the federation auth-floor texts against head's capa…
docs(rest): re-anchor the dead tracker citations in packages/rest/src…