Skip to content

fix(auth): gate the four unauthenticated run/policy sub-resource GET routes - #18

Merged
iaj6 merged 1 commit into
mainfrom
fix/route-auth
Jul 12, 2026
Merged

iaj6 merged 1 commit into
mainfrom
fix/route-auth

Conversation

@iaj6

@iaj6 iaj6 commented Jul 12, 2026

Copy link
Copy Markdown
Owner

Summary

The July audit found four GET routes with no requireUser and no ownership check, while every sibling route enforced both. Any request — including one with a garbage Bearer token — could read cross-tenant data:

  • GET /api/runs/[id]/metrics — any run's cost/token metrics
  • GET /api/runs/[id]/agents — any run's full event timeline (up to 500 events incl. payloads)
  • GET /api/runs/[id]/policies — any run's policy results
  • GET /api/policies/[id]/results — policy results across all users' runs

These were missed by the June auth sweep because they have no UI callers, so nothing exercised them.

Changes

  • The three run sub-resources now apply exactly the rules of GET /api/runs/[id]: authenticated; members see only their own runs; pre-auth (userId = null) runs are admin-only; non-owners get 404, not 403, so run IDs can't be enumerated.
  • GET /api/policies/[id]/results requires auth and scopes results to the member's own runs — getPolicyResultsForPolicy gained an optional ownedByUserId parameter that inner-joins the runs table. Admins keep the cross-user view (matching resolveViewScope semantics elsewhere).

Tests

auth-gaps.test.ts extended with 8 cases: anon → 401, non-owner → 404, owner → 200, admin → 200, and null-owner-is-admin-only for each run route, plus member-vs-admin scoping of policy results. Full workspace suite green (1,278 tests) and web lint clean.

🤖 Generated with Claude Code

…routes

GET /api/runs/[id]/agents, /api/runs/[id]/metrics, /api/runs/[id]/policies,
and /api/policies/[id]/results shipped with no requireUser and no ownership
check while every sibling route enforced both — any request with any (even
invalid) credential could read any run's cost metrics, full event timeline,
and policy results across all users.

- The three run sub-resources now apply the same rules as GET /api/runs/[id]:
  authenticated, members see only their own runs, pre-auth (userId=null)
  runs are admin-only, and non-owners get 404 (not 403) so run IDs can't
  be enumerated.
- /api/policies/[id]/results now requires auth and scopes results to the
  member's own runs via a join against the runs table
  (getPolicyResultsForPolicy gained an optional ownedByUserId filter);
  admins keep the cross-user view.
- auth-gaps.test.ts extended with 8 cases covering anon/non-owner/owner/
  admin and null-owner behavior for all four routes — these were exactly
  the routes the earlier auth sweep missed because they had no UI callers.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@iaj6
iaj6 merged commit 584c249 into main Jul 12, 2026
3 checks passed
@iaj6
iaj6 deleted the fix/route-auth branch July 12, 2026 03:45
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.

1 participant