Skip to content

Commit 82cb5d3

Browse files
committed
RBAC: migrate apiBuilder to rbac.authenticateBearer + ability.can (TRI-8719 Phase B)
Swap all three apiBuilder call sites (loader, action, multi-method) from authenticateApiRequestWithFailure + checkAuthorization to a single RBAC plugin bridge. 30 route files migrated in lockstep — drop the authorization.superScopes option, convert resource callbacks to return RbacResource or RbacResource[] in the new shape. Infrastructure: - apiBuilder: new authenticateRequestForApiBuilder helper funnels all three builders through rbac.authenticateBearer and follow-up findEnvironmentById to rebuild the legacy ApiAuthenticationResultSuccess shape handlers still expect (no handler-facing changes). - @internal/rbac: re-export RbacAbility, RbacResource from @trigger.dev/plugins so webapp only depends on @trigger.dev/rbac. Route-file changes (Risk mitigations from the ticket): - Custom actions (trigger, batchTrigger, update) unchanged at the route level — the ACTION_ALIASES wrapper from Phase A handles them transparently. - Multi-key runs routes (api.v3.runs.$runId, realtime.v1.runs.$runId, realtime.v1.streams.$runId.$streamId, api.v1.runs.$runId.events / .spans.$spanId / .trace, realtime.v1.streams.$runId.input.$streamId second block, plus the batch-array routes) now return RbacResource[] — any resource match grants access. Undefined batch ids are filtered out to avoid accidentally matching a type-level read:batch scope with no id. - Empty-resource routes (api.v1.batches, api.v1.idempotencyKeys.$key.reset) now return { type: 'runs' } — JWTs with read:runs / write:runs start working where they were previously denied by the legacy empty-resource short-circuit. Intentional improvement, strict superset of today's accept set. - Search-params routes (realtime.v1.runs, api.v1.runs) return an array with a collection-level { type: 'runs' } plus any filtered tag/task entries so JWTs that work today continue to work. Verification: - pnpm run typecheck --filter webapp: clean. - pnpm run test --filter @internal/rbac: 31 unit tests pass (wrapper + array-resource semantics). - E2E suite (test/api-auth.e2e.test.ts): all 31 tests pass on the new code path — the three pre-migration 'behaviours to preserve' tests (type-level write:tasks triggers a task, read:tags:<tag> reaches a run with that tag, read:batch:<id> reaches a run in that batch) are still green post-swap. Packaging: - .changeset/rbac-plugin-array-resources.md: minor bump for @trigger.dev/plugins (array-resource overload on RbacAbility.can). - .server-changes/rbac-apibuilder-migration.md: webapp-only note.
1 parent e154745 commit 82cb5d3

33 files changed

Lines changed: 202 additions & 215 deletions
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@trigger.dev/plugins": minor
3+
---
4+
5+
RBAC plugin: `RbacAbility.can(action, resource)` now accepts either a single `RbacResource` or `RbacResource[]`. Array form means "grant access if any resource in the array passes", matching the multi-key authorisation semantics expected by routes that touch multiple resources (e.g. a run that also carries a batch id, tags, and a task identifier — a JWT scoped to any of them grants access).
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
area: webapp
3+
type: improvement
4+
---
5+
6+
Migrate `apiBuilder.server.ts` to `rbac.authenticateBearer` + `ability.can` (TRI-8719). All 30 API routes that used `authorization.superScopes` now rely on the RBAC plugin's scope-algebra plus an `ACTION_ALIASES` compatibility map (`trigger`/`batchTrigger`/`update` satisfied by `write:*` scopes). Two intentional improvements: empty-resource routes (`/api/v1/batches`, `/api/v1/idempotencyKeys/:key/reset`) now accept JWTs (previously denied by the legacy empty-resource short-circuit); id-scoped `write:tasks:*` scopes now grant `POST /api/v1/tasks/:taskId/trigger` (previously denied by literal superScope mismatch). Both are strict supersets of the current accept set — no JWT regresses.

apps/webapp/app/routes/api.v1.batches.$batchId.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,7 @@ export const loader = createLoaderApiRoute(
2525
},
2626
authorization: {
2727
action: "read",
28-
resource: (batch) => ({ batch: batch.friendlyId }),
29-
superScopes: ["read:runs", "read:all", "admin"],
28+
resource: (batch) => ({ type: "batch", id: batch.friendlyId }),
3029
},
3130
},
3231
async ({ resource: batch }) => {

apps/webapp/app/routes/api.v1.deployments.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,7 @@ export const loader = createLoaderApiRoute(
7272
corsStrategy: "none",
7373
authorization: {
7474
action: "read",
75-
resource: () => ({ deployments: "list" }),
76-
superScopes: ["read:deployments", "read:all", "admin"],
75+
resource: () => ({ type: "deployments", id: "list" }),
7776
},
7877
findResource: async () => 1, // This is a dummy function, we don't need to find a resource
7978
},

apps/webapp/app/routes/api.v1.idempotencyKeys.$key.reset.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,7 @@ export const { action } = createActionApiRoute(
2121
corsStrategy: "all",
2222
authorization: {
2323
action: "write",
24-
resource: () => ({}),
25-
superScopes: ["write:runs", "admin"],
24+
resource: () => ({ type: "runs" }),
2625
},
2726
},
2827
async ({ params, body, authentication }) => {

apps/webapp/app/routes/api.v1.prompts.$slug.override.reactivate.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,7 @@ const { action } = createActionApiRoute(
2222
corsStrategy: "all",
2323
authorization: {
2424
action: "update",
25-
resource: (params) => ({ prompts: params.slug }),
26-
superScopes: ["admin"],
25+
resource: (params) => ({ type: "prompts", id: params.slug }),
2726
},
2827
},
2928
async ({ body, params, authentication }) => {

apps/webapp/app/routes/api.v1.prompts.$slug.override.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,7 @@ const { action, loader } = createMultiMethodApiRoute({
4040
corsStrategy: "all",
4141
authorization: {
4242
action: "update",
43-
resource: (params) => ({ prompts: params.slug }),
44-
superScopes: ["admin"],
43+
resource: (params) => ({ type: "prompts", id: params.slug }),
4544
},
4645
methods: {
4746
POST: {

apps/webapp/app/routes/api.v1.prompts.$slug.promote.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,7 @@ const { action } = createActionApiRoute(
2222
corsStrategy: "all",
2323
authorization: {
2424
action: "update",
25-
resource: (params) => ({ prompts: params.slug }),
26-
superScopes: ["admin"],
25+
resource: (params) => ({ type: "prompts", id: params.slug }),
2726
},
2827
},
2928
async ({ body, params, authentication }) => {

apps/webapp/app/routes/api.v1.prompts.$slug.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,7 @@ export const loader = createLoaderApiRoute(
3737
},
3838
authorization: {
3939
action: "read",
40-
resource: (_resource, params) => ({ prompts: params.slug }),
41-
superScopes: ["read:prompts", "admin"],
40+
resource: (_resource, params) => ({ type: "prompts", id: params.slug }),
4241
},
4342
},
4443
async ({ searchParams, resource: prompt }) => {
@@ -98,8 +97,7 @@ const { action } = createActionApiRoute(
9897
corsStrategy: "all",
9998
authorization: {
10099
action: "read",
101-
resource: (params) => ({ prompts: params.slug }),
102-
superScopes: ["read:prompts", "admin"],
100+
resource: (params) => ({ type: "prompts", id: params.slug }),
103101
},
104102
},
105103
async ({ body, params, authentication }) => {

apps/webapp/app/routes/api.v1.prompts.$slug.versions.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,7 @@ export const loader = createLoaderApiRoute(
2727
},
2828
authorization: {
2929
action: "read",
30-
resource: (_resource, params) => ({ prompts: params.slug }),
31-
superScopes: ["read:prompts", "admin"],
30+
resource: (_resource, params) => ({ type: "prompts", id: params.slug }),
3231
},
3332
},
3433
async ({ resource: prompt }) => {

0 commit comments

Comments
 (0)