Skip to content

Commit 5b3b25a

Browse files
matt-aitkenclaude
andcommitted
apiBuilder: require explicit anyResource() / everyResource() at multi-resource sites
Bare RbacResource[] in `authorization.resource` is now a type error. Multi-resource auth must wrap with one of: - anyResource(...): succeed if any element passes (the existing default; used when one record carries multiple identifiers — runs by friendlyId / batch / tags / task — so a JWT scoped to any one grants access) - everyResource(...): succeed only if every element passes (existing helper; used by batch operations and the multi-table query route) The OR-loophole class of bug CodeRabbit caught on api.v1.query — a JWT scoped to one of N detected tables was authorized for the whole multi- table query — was patchable per-route with everyResource. The Symbol marker stayed invisible: future authors would still default to bare arrays. Tightening AuthResource flushed out 11 routes that were silently on the OR path; each is wrapped explicitly now. Also inline the unrelated private `anyResource` helper in internal-packages/rbac/src/ability.ts so the public name is unambiguous. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent da75ac5 commit 5b3b25a

16 files changed

Lines changed: 126 additions & 70 deletions

apps/webapp/app/routes/api.v1.runs.$runId.events.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import { json } from "@remix-run/server-runtime";
22
import { z } from "zod";
33
import { getTaskEventStoreTableForRun } from "~/v3/taskEventStore.server";
4-
import { createLoaderApiRoute } from "~/services/routeBuilders/apiBuilder.server";
4+
import {
5+
anyResource,
6+
createLoaderApiRoute,
7+
} from "~/services/routeBuilders/apiBuilder.server";
58
import { ApiRetrieveRunPresenter } from "~/presenters/v3/ApiRetrieveRunPresenter.server";
69
import { resolveEventRepositoryForStore } from "~/v3/eventRepository/index.server";
710

@@ -30,7 +33,7 @@ export const loader = createLoaderApiRoute(
3033
if (run.batch?.friendlyId) {
3134
resources.push({ type: "batch", id: run.batch.friendlyId });
3235
}
33-
return resources;
36+
return anyResource(resources);
3437
},
3538
},
3639
},

apps/webapp/app/routes/api.v1.runs.$runId.spans.$spanId.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@ import { BatchId } from "@trigger.dev/core/v3/isomorphic";
33
import { z } from "zod";
44
import { $replica } from "~/db.server";
55
import { extractAISpanData } from "~/components/runs/v3/ai";
6-
import { createLoaderApiRoute } from "~/services/routeBuilders/apiBuilder.server";
6+
import {
7+
anyResource,
8+
createLoaderApiRoute,
9+
} from "~/services/routeBuilders/apiBuilder.server";
710
import { resolveEventRepositoryForStore } from "~/v3/eventRepository/index.server";
811
import { getTaskEventStoreTableForRun } from "~/v3/taskEventStore.server";
912

@@ -37,7 +40,7 @@ export const loader = createLoaderApiRoute(
3740
if (run.batchId) {
3841
resources.push({ type: "batch", id: BatchId.toFriendlyId(run.batchId) });
3942
}
40-
return resources;
43+
return anyResource(resources);
4144
},
4245
},
4346
},

apps/webapp/app/routes/api.v1.runs.$runId.trace.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@ import { json } from "@remix-run/server-runtime";
22
import { BatchId } from "@trigger.dev/core/v3/isomorphic";
33
import { z } from "zod";
44
import { $replica } from "~/db.server";
5-
import { createLoaderApiRoute } from "~/services/routeBuilders/apiBuilder.server";
5+
import {
6+
anyResource,
7+
createLoaderApiRoute,
8+
} from "~/services/routeBuilders/apiBuilder.server";
69
import { resolveEventRepositoryForStore } from "~/v3/eventRepository/index.server";
710
import { getTaskEventStoreTableForRun } from "~/v3/taskEventStore.server";
811

@@ -35,7 +38,7 @@ export const loader = createLoaderApiRoute(
3538
if (run.batchId) {
3639
resources.push({ type: "batch", id: BatchId.toFriendlyId(run.batchId) });
3740
}
38-
return resources;
41+
return anyResource(resources);
3942
},
4043
},
4144
},

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

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@ import {
44
ApiRunListSearchParams,
55
} from "~/presenters/v3/ApiRunListPresenter.server";
66
import { logger } from "~/services/logger.server";
7-
import { createLoaderApiRoute } from "~/services/routeBuilders/apiBuilder.server";
7+
import {
8+
anyResource,
9+
createLoaderApiRoute,
10+
} from "~/services/routeBuilders/apiBuilder.server";
811

912
export const loader = createLoaderApiRoute(
1013
{
@@ -15,10 +18,10 @@ export const loader = createLoaderApiRoute(
1518
action: "read",
1619
resource: (_, __, searchParams) => {
1720
const taskFilter = searchParams["filter[taskIdentifier]"] ?? [];
18-
return [
21+
return anyResource([
1922
{ type: "runs" },
2023
...taskFilter.map((id) => ({ type: "tasks", id })),
21-
];
24+
]);
2225
},
2326
},
2427
findResource: async () => 1, // This is a dummy function, we don't need to find a resource

apps/webapp/app/routes/api.v1.sessions.$session.end-and-continue.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,10 @@ import { $replica, prisma } from "~/db.server";
88
import { logger } from "~/services/logger.server";
99
import { swapSessionRun } from "~/services/realtime/sessionRunManager.server";
1010
import { resolveSessionByIdOrExternalId } from "~/services/realtime/sessions.server";
11-
import { createActionApiRoute } from "~/services/routeBuilders/apiBuilder.server";
11+
import {
12+
anyResource,
13+
createActionApiRoute,
14+
} from "~/services/routeBuilders/apiBuilder.server";
1215

1316
const ParamsSchema = z.object({
1417
session: z.string(),
@@ -52,7 +55,7 @@ const { action, loader } = createActionApiRoute(
5255
ids.add(session.friendlyId);
5356
if (session.externalId) ids.add(session.externalId);
5457
}
55-
return [...ids].map((id) => ({ type: "sessions", id }));
58+
return anyResource([...ids].map((id) => ({ type: "sessions", id })));
5659
},
5760
},
5861
},

apps/webapp/app/routes/api.v1.sessions.$session.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
serializeSessionWithFriendlyRunId,
1212
} from "~/services/realtime/sessions.server";
1313
import {
14+
anyResource,
1415
createActionApiRoute,
1516
createLoaderApiRoute,
1617
} from "~/services/routeBuilders/apiBuilder.server";
@@ -35,10 +36,10 @@ export const loader = createLoaderApiRoute(
3536
// / `admin` bypass via the JWT ability's wildcard branches.
3637
resource: (session) =>
3738
session.externalId
38-
? [
39+
? anyResource([
3940
{ type: "sessions", id: session.friendlyId },
4041
{ type: "sessions", id: session.externalId },
41-
]
42+
])
4243
: { type: "sessions", id: session.friendlyId },
4344
},
4445
},

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

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
import { serializeSession } from "~/services/realtime/sessions.server";
2121
import { SessionsRepository } from "~/services/sessionsRepository/sessionsRepository.server";
2222
import {
23+
anyResource,
2324
createActionApiRoute,
2425
createLoaderApiRoute,
2526
} from "~/services/routeBuilders/apiBuilder.server";
@@ -47,10 +48,10 @@ export const loader = createLoaderApiRoute(
4748
// grants access (the array gets OR semantics).
4849
resource: (_, __, searchParams) => {
4950
const taskFilter = asArray(searchParams["filter[taskIdentifier]"]) ?? [];
50-
return [
51+
return anyResource([
5152
...taskFilter.map((id) => ({ type: "tasks" as const, id })),
5253
{ type: "sessions" as const },
53-
];
54+
]);
5455
},
5556
},
5657
findResource: async () => 1,
@@ -135,10 +136,11 @@ const { action } = createActionApiRoute(
135136
// per-task check exactly as before. `admin` / `write:all` bypass
136137
// via the JWT ability's wildcard branches.
137138
action: "write",
138-
resource: (_params, _searchParams, _headers, body) => [
139-
{ type: "tasks", id: body.taskIdentifier },
140-
{ type: "sessions" },
141-
],
139+
resource: (_params, _searchParams, _headers, body) =>
140+
anyResource([
141+
{ type: "tasks", id: body.taskIdentifier },
142+
{ type: "sessions" },
143+
]),
142144
},
143145
corsStrategy: "all",
144146
},

apps/webapp/app/routes/api.v3.runs.$runId.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import { json } from "@remix-run/server-runtime";
22
import { z } from "zod";
33
import { ApiRetrieveRunPresenter } from "~/presenters/v3/ApiRetrieveRunPresenter.server";
4-
import { createLoaderApiRoute } from "~/services/routeBuilders/apiBuilder.server";
4+
import {
5+
anyResource,
6+
createLoaderApiRoute,
7+
} from "~/services/routeBuilders/apiBuilder.server";
58

69
const ParamsSchema = z.object({
710
runId: z.string(),
@@ -27,7 +30,7 @@ export const loader = createLoaderApiRoute(
2730
if (run.batch?.friendlyId) {
2831
resources.push({ type: "batch", id: run.batch.friendlyId });
2932
}
30-
return resources;
33+
return anyResource(resources);
3134
},
3235
},
3336
},

apps/webapp/app/routes/realtime.v1.runs.$runId.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@ import { z } from "zod";
33
import { $replica } from "~/db.server";
44
import { getRequestAbortSignal } from "~/services/httpAsyncStorage.server";
55
import { realtimeClient } from "~/services/realtimeClientGlobal.server";
6-
import { createLoaderApiRoute } from "~/services/routeBuilders/apiBuilder.server";
6+
import {
7+
anyResource,
8+
createLoaderApiRoute,
9+
} from "~/services/routeBuilders/apiBuilder.server";
710

811
const ParamsSchema = z.object({
912
runId: z.string(),
@@ -40,7 +43,7 @@ export const loader = createLoaderApiRoute(
4043
if (run.batch?.friendlyId) {
4144
resources.push({ type: "batch", id: run.batch.friendlyId });
4245
}
43-
return resources;
46+
return anyResource(resources);
4447
},
4548
},
4649
},

apps/webapp/app/routes/realtime.v1.runs.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import { z } from "zod";
22
import { getRequestAbortSignal } from "~/services/httpAsyncStorage.server";
33
import { realtimeClient } from "~/services/realtimeClientGlobal.server";
4-
import { createLoaderApiRoute } from "~/services/routeBuilders/apiBuilder.server";
4+
import {
5+
anyResource,
6+
createLoaderApiRoute,
7+
} from "~/services/routeBuilders/apiBuilder.server";
58

69
const SearchParamsSchema = z.object({
710
tags: z
@@ -21,10 +24,11 @@ export const loader = createLoaderApiRoute(
2124
findResource: async () => 1, // This is a dummy value, it's not used
2225
authorization: {
2326
action: "read",
24-
resource: (_, __, searchParams) => [
25-
{ type: "runs" },
26-
...(searchParams.tags ?? []).map((tag) => ({ type: "tags", id: tag })),
27-
],
27+
resource: (_, __, searchParams) =>
28+
anyResource([
29+
{ type: "runs" },
30+
...(searchParams.tags ?? []).map((tag) => ({ type: "tags", id: tag })),
31+
]),
2832
},
2933
},
3034
async ({ searchParams, authentication, request, apiVersion }) => {

0 commit comments

Comments
 (0)