Skip to content

feat: project membership RBAC (stacked on service auth) - #1559

Closed
Josh Duffney (duffney) wants to merge 6 commits into
duffney-service-auth-run-tokenfrom
duffney-rbac-v2-l2
Closed

Josh Duffney (duffney) wants to merge 6 commits into
duffney-service-auth-run-tokenfrom
duffney-rbac-v2-l2

Conversation

@duffney

Copy link
Copy Markdown
Collaborator

Summary

Project membership RBAC, stacked on the service-auth / run-token layer (#1558). This PR is #1532 rebased onto #1558, plus one commit that wires RBAC enforcement to #1558's run and service principals. Refs #1557.

Stack: main ← #1531 ← #1558 (service auth / run tokens) ← this PR.

Fallback: #1532 (stacked directly on #1531) is unchanged and stays the fallback if the run-token approach in #1558 is rejected.

Commits:

Commit Description
54718a4 Add project membership RBAC and operator-managed access (#1532 d7759c42, conflicts resolved against #1558)
33abaf3 fix: wait for access story navigation before selecting projects (#1532 a248e76d)
98ff677 fix: scope AI prompt context lookups to the authorized project (#1532 b7439535)
189f8f3 feat(api): authorize run-token and service-key callers (new)
d3d933a test(api): cover run-token and service-key authorization (new)
7989ceb docs: describe run-token and service-key authorization (new)

Conflict resolution (cherry-picks)

Guard wiring (apps/api/src/auth/authorization.ts)

decide() branches on req.principal, which #1558 sets for run and service callers. Those callers have req.user = ANONYMOUS_USER and no req.auth.

Run principal

  • Allowed only on routes declared runToken: true (14 GET routes), only for GET/HEAD, and only when the route policy is project. Otherwise it returns 403 run_token_not_allowed.
  • The project is resolved the same way as for users (query, point document, requestId, references). It must equal the run's project and must not be deleted. Otherwise it returns 404 not_found, the same non-disclosing contract a non-member user gets.
  • On success, req.authorizedProjectId is set.

Service principal

  • Allowed only on routes granted by its scopes:
    • agents:read: GET/HEAD /api/v1/agents
    • agents:register: POST /api/v1/agents and /api/v1/agents/:id/versions
    • models:sync: POST /api/v1/models/sync
  • Anything else returns 403 insufficient_scope.

Other changes

Known gap

The post-processor's /reports/trigger call and the report-generator tools don't send run tokens yet. Report generation is disabled by policy in this stack, so these paths aren't exercised. They are left for a follow-up.

Demos

N/A. This is API authorization behavior; no Portal/CLI UI change beyond #1532's.

Before

N/A

After

N/A

Testing

Commands and results:

  • pnpm exec vitest run apps/api packages/shared apps/judge packages/model-scanning scripts apps/workers: passed (148 files, 2458 tests).
  • pnpm exec vitest run apps/cli apps/portal: passed. The full unit set across api, shared, judge, model-scanning, scripts, workers, cli and portal was 251 files and 3499 tests.
  • Integration (isolated Mongo/Redis): passed (6 files, 61 tests). Command:
    AUTH_TEST_MONGO_URI=… AUTH_TEST_REDIS_PORT=… pnpm exec vitest run --config vitest.integration.config.ts apps/api/src/auth/ apps/api/src/routes/project-membership.integration.test.ts apps/api/src/routes/task-prompts.integration.test.ts
    
    • This includes the new caller-authorization.integration.test.ts. 5 of its 7 tests fail without the guard-wiring commit.
  • Typecheck passed for:
    • apps/api, apps/judge, packages/shared, apps/cli, packages/model-scanning (tsc --noEmit)
    • Portal (pnpm --filter portal exec tsc -b)
    • workers, except coder-acp-copilot-windows, which fails only because a sibling package's dist isn't built locally. That is unrelated to this change.
  • OpenAPI snapshot regenerated (website/src/openapi/scope-openapi.json).
  • Lint: not run (eslint is not installed in this workspace).

Local end-to-end (Docker Compose, SCOPE_AUTH_ENABLED=true, entra-local, isolated port offset/DB):

Setup:

  • Service keys were set for register-agents and model-scanner.
  • Admin was bootstrapped with operator-access.js bootstrap-admin (preview, then --apply).
  • Two projects, X and Y, were created.

Service and run callers:

  • ✅ register-agents exited 0 and registered both agents and their dev versions.
  • ✅ model-scanner-copilot exited 0 (+38 models synced).
  • ✅ A run submitted in project X completed with outcome succeeded (1/1 criteria passed).
    • The worker and judge used the run token; the judge fetched criteria with it.
    • The worker, judge, post-processor, scheduler, report-generator, scanner and register-agents logs had no 401/403.
    • Report generation is disabled by policy, so no report was produced.
  • ✅ Service key out of scope → 403 insufficient_scope:
    • register-agents on GET /agents, POST /models/sync and GET /criteria
    • model-scanner on GET /criteria
  • ✅ Service key with a wrong value → 401 invalid_service_key.
  • ✅ Service headers sent together with a bearer → 400 ambiguous_credentials.

Run token for project X (minted for a separate synthetic processing request):

  • ✅ Flagged GET in X (/criteria, /criteria/:id) → 200.
  • ✅ Flagged GET for project Y (/criteria?projectId=Y, /criteria/<Y criterion>?projectId=Y) → 404.
  • ✅ Non-flagged GETs (/requests, /projects) and POST /criteria → 403 run_token_not_allowed.
  • ✅ /users/me → 401.
  • ✅ Lifecycle: older than max age → 401 run_token_expired; run no longer processing → 401 run_not_active; tampered token → 401 invalid_run_token.

User principals:

  • ✅ A user who isn't a member of X gets 404 for both reading and submitting in X; members are unaffected.

Not verified end to end:

  • The post-processor /reports/trigger and report-generator run-token paths (see known gap).
  • The judge default model was overridden locally to one available to the test account.

Documentation and compatibility

Checklist

Enforce fixed project capabilities and canonical authorization across API, CLI and Portal. Add durable access operations, operator-only bootstrap and repair, project-admin membership discovery, migration 031 and coordinated report shutdown/auth mode.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 4257b230-1926-4101-9c45-498e16e5fa51
Cover deferred all-project navigation so story interactions cannot query the scoped view before React commits the route transition.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 4257b230-1926-4101-9c45-498e16e5fa51
Task-prompt generation and feature extraction read prompts and feature
definitions across all projects, sending other projects' data to the LLM
and persisting foreign features on the current prompt.

- task-prompts/generate filters recent prompts by the authorized projectId
- task-prompts/:id/extract-features uses only the prompt's project features
- prompt-features generate-prompt/extract-from-text and
  criteria/generate-prompt require projectId and always filter by it
- criteria/mdp task-prompt lookup is scoped to its project
- Portal AI calls now send the active projectId (previously 400)
- Add unit and two-project integration regression tests
- Regenerate OpenAPI snapshot/spec; document the rule in app-design.md

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: e871494e-c9b6-4a6f-8665-96a0b6336218
Branch route authorization on the caller principal attached by the auth
middleware:

- Run tokens are accepted only on routes flagged runToken: true, for
  GET/HEAD, and only for the run's own non-deleted project. Other projects
  look absent (404); unflagged routes and writes return 403
  run_token_not_allowed. req.authorizedProjectId is set as for users.
- Service keys are accepted only on the routes their scopes grant
  (agents:read, agents:register, models:sync); everything else is 403
  insufficient_scope.
- Run and service callers skip the IdP JWT lease parse and cannot resolve
  to a user (currentUser returns 403 user_required).
- The global /api/v1 gate admits verified run and service callers so the
  route guard can decide.

User principals keep the existing membership behavior.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Unit tests for the guard's caller branches: run tokens pass on flagged
GET/HEAD routes in their own project without a user read or JWT lease,
look absent (404) for other or deleted projects, and get 403 on unflagged
routes, writes, metadata, platform and catalog routes; service keys pass
in scope and get 403 insufficient_scope otherwise; callers never resolve
to a user record.

Auth-on integration tests against native Mongo through the full API: a
run token reads criteria in its project, is rejected on another
project's route, on unflagged routes and on POST; expired, finished and
forged tokens get 401; the model scanner and register-agents keys pass
in scope and get 403 out of scope.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Document how the route guard limits run tokens (flagged GET/HEAD routes in
the run's project, 403/404 otherwise, no JWT lease) and service keys
(scope-to-route table, 403 insufficient_scope), update the internal-caller
cutover table, note the report trigger/tools gap and add the new
integration suite to the release gates.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@github-actions github-actions Bot added type: documentation Documentation additions, corrections, and improvements. area: worker Coding agent worker runtime, task execution, and lifecycle. area: cli Scope command-line interface and terminal workflows. type: dependencies Dependency additions, removals, and version updates. area: infrastructure Cloud resources, hosting, networking, and shared infrastructure. language: javascript Work involving JavaScript code, tooling, or dependencies. area: portal Scope web portal, pages, and user-facing components. area: post-processing ATIF generation and the run-output post-processing pipeline. area: reporting Generation and delivery of benchmark reports. topic: security Security protections, vulnerabilities, and risk reduction. topic: testing Test coverage, test infrastructure, and validation quality. topic: ui Portal, visual presentation, layout, and interface components. labels Oct 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: cli Scope command-line interface and terminal workflows. area: infrastructure Cloud resources, hosting, networking, and shared infrastructure. area: portal Scope web portal, pages, and user-facing components. area: post-processing ATIF generation and the run-output post-processing pipeline. area: reporting Generation and delivery of benchmark reports. area: worker Coding agent worker runtime, task execution, and lifecycle. language: javascript Work involving JavaScript code, tooling, or dependencies. topic: security Security protections, vulnerabilities, and risk reduction. topic: testing Test coverage, test infrastructure, and validation quality. topic: ui Portal, visual presentation, layout, and interface components. type: dependencies Dependency additions, removals, and version updates. type: documentation Documentation additions, corrections, and improvements.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant