Skip to content

security(rest): require manage_platform_settings on POST /datasources/:name/external/validate (#10255) - #10538

Merged
os-elon merged 1 commit into
mainfrom
claude/issue-10255-external-validate-capability
Aug 21, 2026
Merged

security(rest): require manage_platform_settings on POST /datasources/:name/external/validate (#10255)#10538
os-elon merged 1 commit into
mainfrom
claude/issue-10255-external-validate-capability

Conversation

@os-elon

Copy link
Copy Markdown
Collaborator

Fixes#10255

What

POST /datasources/:name/external/validate — the one route of the external-datasource federation family the #9901 ruling did not name — now requires the family's read capability, manage_platform_settings, above the #9686 authentication floor. The now-unused authenticated guard kind is removed rather than left as a spare lower door a future un-ruled route could silently adopt. Ruled on #10255 (2026-08-20, maintainer accepted option A; the ruling comment records the verbatim acceptance).

Why the read capability — established from the service source, not the handler name

packages/services/service-datasource/src/external-datasource-service.ts:

  • No mutation.validateAll reaches only reads: listObjects, per-object getObject / getDatasource, and introspect. It persists nothing, writes no credential, caches no result — unlike refreshCatalog, which persists the snapshot and is gated as a write.
  • It is a read of the already-gated surface. Per-object validation drives introspect(datasource) — the same live remote-schema introspection primitive behind the gated GET /:name/external/tables — and its report names remote columns and types (schema diffs) and carries driver error strings for unreachable remotes. An unentitled caller refused at the tables route could previously still trigger live remote introspection through this route and read what it found.
  • Reachability before this PR: any authenticated caller — better-auth session or sys_api_key, no org narrowing — because the resolver admits every credential kind the platform admits.

Refusal shape is the family's existing one: 403 with the standard catalog code PERMISSION_DENIED, the message naming the missing capability; the anonymous floor stays 401 UNAUTHENTICATED.

Breaking-change surface (clause-②)

This is published SDK surface: datasources.external.validate on ObjectStackClient and the CLI's os datasource validate reach exactly this route. An integration presenting a valid credential withoutmanage_platform_settings was served before and is refused now — the same migration as PR #10254's read routes, smaller scale; the changeset documents it in that changeset's shape. No in-repo caller breaks: examples/dogfood do not call this route, the client test mocks fetch, and the auth-guard suite's entitled fixtures already hold the read capability where they must.

Test changes — the flip is deliberate

  • The auth-guard table's explicit capability: null row for validate now carries READ_CAPABILITY — the loud table edit the previous pin was designed to force.
  • The case pinning "still served holding nothing" is flipped to its opposite (403 PERMISSION_DENIED naming the capability, service never dispatched) with the ruling recorded in the case body, plus a companion case pinning that manage_platform_settings alone clears it.
  • The "holding NOTHING" refusal loop now runs all five routes (no null row left to filter), and the crossed read/write-split cases pick validate up in the read set.
  • Envelope-conformance stub comment updated (five of five gated; stub already held both capabilities, so no functional change there).

Verification (all at head de8fac4, quoting each runner's own verdict line)

  • Full @objectstack/rest suite + typecheck: Test Files 131 passed (131) · Tests 2142 passed (2142); tsc --noEmit exit 0.
  • Reverse verification (route file reverted to origin/main, new tests kept — direction predicted before the run): Tests 3 failed | 8 passed (11) — exactly the three predicted cases (the all-five refusal loop, the flipped [#10255] refusal case, and the manage_metadata-alone cross case), while both success-direction cases stayed green. Fix restored byte-identical from the commit afterwards (clean git status).
  • Gate set re-derived at head with node scripts/pm/dispatch-gates.mjs (no path args): all 11 path-matched families green plus the 5 convention-triggered test-file families — including check-type-check-coverage --re-measure: OK — 33 ledger entr(ies) re-measured … none above its recorded number, slot-lookup ratchet holds … none new, where-matcher conformance holds … none new, check-engine-double-contract: OK, check-nul-bytes: OK, ✓ This diff introduces no major bump.
  • Not applicable: dist-rebuild ablation discipline — the suites here import the subject relatively from src, so no dist resolution is involved in either verification leg.

Out-of-scope observation filed as #10537 (validate fans out to every federated datasource and post-filters; #10537 is not addressed here).


Generated by Claude Code

…/:name/external/validate
The one federation route the #9901 ruling did not name joins the family's
read gate: validateAll drives the same live remote-schema introspection the
gated read twins expose, so it answers to the same capability. The
now-unused 'authenticated' guard kind is removed. Ruled on #10255
(2026-08-20, option A).
Fixes#10255
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019bmVFqoQPq63zhKrxdYG1r
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

5 anchor(s) derived from 1 changed package(s); no hand-written page names any of them. ✅

What this run could not see
  • the SDK route bridge reached 45 of 221 client-bound route-ledger rows — the other 176 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run: node scripts/docs-audit/affected-docs.mjs --bridge-coverage

Coarse fallback — 13 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 359f5956d7910aed7ae9f8fccc9fbb988b3e4882packageMentionDocs.

Which tree this was computed on

This run read content/docs from 9b85c4b25fc02bd5d6566a7c9034528863a5e489 — the merge of head de8fac4c2b2fb5f11cb3b4d7eb82f5253349a91e into base 359f5956d7910aed7ae9f8fccc9fbb988b3e4882, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 9b85c4b25fc02bd5d6566a7c9034528863a5e489 && git checkout 9b85c4b25fc02bd5d6566a7c9034528863a5e489
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 359f5956d7910aed7ae9f8fccc9fbb988b3e4882 de8fac4c2b2fb5f11cb3b4d7eb82f5253349a91e && git checkout -B drift-repro 359f5956d7910aed7ae9f8fccc9fbb988b3e4882 && git merge --no-ff de8fac4c2b2fb5f11cb3b4d7eb82f5253349a91e
node scripts/docs-audit/affected-docs.mjs --json 359f5956d7910aed7ae9f8fccc9fbb988b3e4882

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tests tooling labels Aug 21, 2026
@os-elon
os-elon marked this pull request as ready for review August 21, 2026 06:16
@os-elon
os-elon added this pull request to the merge queueAug 21, 2026
Merged via the queue into main with commit 6ce58a7Aug 21, 2026
32 checks passed
@os-elon
os-elon deleted the claude/issue-10255-external-validate-capability branch August 21, 2026 06:30
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

2 participants

@os-elon@claude