Skip to content

test(scope): guard the /api/v1 GET surface against unscoped routes (#1178) - #1351

Open
Sriram-PR wants to merge 2 commits into
smart-mcp-proxy:mainfrom
Sriram-PR:test/route-scope-guard
Open

Sriram-PR wants to merge 2 commits into
smart-mcp-proxy:mainfrom
Sriram-PR:test/route-scope-guard

Conversation

@Sriram-PR

Copy link
Copy Markdown

Refs #1178.

Fixing #1166/#1167 took three passes because the /api/v1 GET surface was enumerated by hand and each pass turned up another door that wasn't scope-gated. This adds a test that walks the production chi router instead of trusting a hand-kept list.

What it does:

  • walks s.Router() and collects every GET under /api/v1
  • requires each route to be classified as refused, filtered, or open, with a written reason next to it
  • a new GET route added without a classification fails the test, so a door can't ship unclassified
  • drives each route with a real read-only agent token scoped to one server and checks the verdict holds: refused routes answer the fixed 403 (and carry the scope-layer denial body, so an unrelated 403 doesn't count), filtered and open routes stay reachable
  • floors the route count so a walk that matches nothing (say after a router refactor moves the prefix) can't pass silently

There's already a similar walk for the server edition (tenant_allowlist_walk_test.go, //go:build server) covering session principals. This is the personal-edition agent-token counterpart, which is the axis #1166/#1167 were about and didn't have a guard.

Also adds a Doctor method to the shared scope-test management mock so /diagnostics runs through the same path production uses, plus TestDiagnostics_ScopedCallerDoesNotSeeHiddenServer backing that route's classification.

Verification:

  • go test -race ./internal/httpapi/
  • golangci-lint run --config .github/.golangci.yml ./internal/httpapi/ (0 issues)

…mart-mcp-proxy#1178)

Fixing smart-mcp-proxy#1166/smart-mcp-proxy#1167 took three passes because the GET surface was
enumerated by hand and each pass missed a door. This walks the production
chi router instead, collects every GET under /api/v1, and requires each
route to be classified as refused, filtered, or open with a written
reason. A new GET route added without an entry fails the test until
someone classifies it, and a floor on the route count stops a walk that
matches nothing from passing.

Adds a Doctor method to the shared scope-test management mock, mirroring
the production service including its scope gate, so /diagnostics is
exercised through the same path production takes.
Comment thread internal/httpapi/scope_route_table_guard_test.go Outdated
Comment thread internal/httpapi/scope_route_table_guard_test.go Outdated
@Sriram-PR

Copy link
Copy Markdown
Author

Good catches, both. You're right that "not 403" was doing almost nothing for the rows that never reach the handler, I confirmed the same 11: the scan subroutes and the /{id} detail routes 404 on the synthetic id before canSeeServer, connect 503s (not wired in the fixture), and index/search 400s with no query.

Reworked it: routes that actually reach the handler now assert 2xx, and the pre-gate ones are in an explicit shortCircuitCodes map with a reason each, so a route that starts reaching the handler trips it. Also fixed the routeParamSubstitutions comment, which was just wrong about the scan routes.

On diagnostics, yes, the old test only exercised the mock's own filter. I swapped it to run the httpapi handler with withManagement:false so it drives the real visibleServers path in the legacy branch, which had no coverage before. Removing that call now fails the test. For the management path I'm citing TestDoctor_ScopedTokenSeesOnlyAllowedServers in the map instead of pretending this file covers it.

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants