Skip to content

Commit c37622e

Browse files
authored
Realtime: improve scope access to runs with tags and batches (#1511)
* JWT scopes for tags and batches can now access runs that have the tag or are in the batch - useTaskTrigger can now submit options - auto-generated batch trigger public access tokens no longer need each individual run ID scope * Add changeset * Added task scopes to work like tags and batches Also removed scopes for tags when auto-generating a public access token as that could be dangerous.
1 parent d67023a commit c37622e

23 files changed

+262
-199
lines changed

.changeset/purple-snakes-divide.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@trigger.dev/react-hooks": patch
3+
"@trigger.dev/sdk": patch
4+
---
5+
6+
Public access token scopes with just tags or just a batch can now access runs that have those tags or are in the batch. Previously, the only way to access a run was to have a specific scope for that exact run.

apps/webapp/app/presenters/v3/ApiRetrieveBatchPresenter.server.ts

Lines changed: 0 additions & 32 deletions
This file was deleted.

apps/webapp/app/presenters/v3/ApiRetrieveRunPresenter.server.ts

Lines changed: 36 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import assertNever from "assert-never";
1515
import { AuthenticatedEnvironment } from "~/services/apiAuth.server";
1616
import { generatePresignedUrl } from "~/v3/r2.server";
1717
import { BasePresenter } from "./basePresenter.server";
18-
import { prisma } from "~/db.server";
18+
import { $replica, prisma } from "~/db.server";
1919

2020
// Build 'select' object
2121
const commonRunSelect = {
@@ -59,48 +59,46 @@ type CommonRelatedRun = Prisma.Result<
5959
"findFirstOrThrow"
6060
>;
6161

62+
type FoundRun = NonNullable<Awaited<ReturnType<typeof ApiRetrieveRunPresenter.findRun>>>;
63+
6264
export class ApiRetrieveRunPresenter extends BasePresenter {
63-
public async call(
64-
friendlyId: string,
65-
env: AuthenticatedEnvironment
66-
): Promise<RetrieveRunResponse | undefined> {
67-
return this.traceWithEnv("call", env, async (span) => {
68-
const taskRun = await this._replica.taskRun.findFirst({
69-
where: {
70-
friendlyId,
71-
runtimeEnvironmentId: env.id,
72-
},
73-
include: {
74-
attempts: true,
75-
lockedToVersion: true,
76-
schedule: true,
77-
tags: true,
78-
batch: {
79-
select: {
80-
id: true,
81-
friendlyId: true,
82-
},
65+
public static async findRun(friendlyId: string, env: AuthenticatedEnvironment) {
66+
return $replica.taskRun.findFirst({
67+
where: {
68+
friendlyId,
69+
runtimeEnvironmentId: env.id,
70+
},
71+
include: {
72+
attempts: true,
73+
lockedToVersion: true,
74+
schedule: true,
75+
tags: true,
76+
batch: {
77+
select: {
78+
id: true,
79+
friendlyId: true,
8380
},
84-
parentTaskRun: {
85-
select: commonRunSelect,
86-
},
87-
rootTaskRun: {
88-
select: commonRunSelect,
89-
},
90-
childRuns: {
91-
select: {
92-
...commonRunSelect,
93-
},
81+
},
82+
parentTaskRun: {
83+
select: commonRunSelect,
84+
},
85+
rootTaskRun: {
86+
select: commonRunSelect,
87+
},
88+
childRuns: {
89+
select: {
90+
...commonRunSelect,
9491
},
9592
},
96-
});
97-
98-
if (!taskRun) {
99-
logger.debug("Task run not found", { friendlyId, envId: env.id });
100-
101-
return undefined;
102-
}
93+
},
94+
});
95+
}
10396

97+
public async call(
98+
taskRun: FoundRun,
99+
env: AuthenticatedEnvironment
100+
): Promise<RetrieveRunResponse | undefined> {
101+
return this.traceWithEnv("call", env, async (span) => {
104102
let $payload: any;
105103
let $payloadPresignedUrl: string | undefined;
106104
let $output: any;
Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { json } from "@remix-run/server-runtime";
22
import { z } from "zod";
3-
import { ApiRetrieveBatchPresenter } from "~/presenters/v3/ApiRetrieveBatchPresenter.server";
3+
import { $replica } from "~/db.server";
44
import { createLoaderApiRoute } from "~/services/routeBuilders/apiBuilder.server";
55

66
const ParamsSchema = z.object({
@@ -12,20 +12,28 @@ export const loader = createLoaderApiRoute(
1212
params: ParamsSchema,
1313
allowJWT: true,
1414
corsStrategy: "all",
15+
findResource: (params, auth) => {
16+
return $replica.batchTaskRun.findFirst({
17+
where: {
18+
friendlyId: params.batchId,
19+
runtimeEnvironmentId: auth.environment.id,
20+
},
21+
});
22+
},
1523
authorization: {
1624
action: "read",
17-
resource: (params) => ({ batch: params.batchId }),
25+
resource: (batch) => ({ batch: batch.friendlyId }),
1826
superScopes: ["read:runs", "read:all", "admin"],
1927
},
2028
},
21-
async ({ params, authentication }) => {
22-
const presenter = new ApiRetrieveBatchPresenter();
23-
const result = await presenter.call(params.batchId, authentication.environment);
24-
25-
if (!result) {
26-
return json({ error: "Batch not found" }, { status: 404 });
27-
}
28-
29-
return json(result);
29+
async ({ resource: batch }) => {
30+
return json({
31+
id: batch.friendlyId,
32+
status: batch.status,
33+
idempotencyKey: batch.idempotencyKey ?? undefined,
34+
createdAt: batch.createdAt,
35+
updatedAt: batch.updatedAt,
36+
runCount: batch.runCount,
37+
});
3038
}
3139
);

apps/webapp/app/routes/api.v1.packets.$.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ export const loader = createLoaderApiRoute(
4545
params: ParamsSchema,
4646
allowJWT: true,
4747
corsStrategy: "all",
48+
findResource: async () => 1, // This is a dummy function, we don't need to find a resource
4849
},
4950
async ({ params, authentication }) => {
5051
const filename = params["*"];

apps/webapp/app/routes/api.v1.runs.$runParam.reschedule.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,17 @@ export async function action({ request, params }: ActionFunctionArgs) {
6161
return json({ error: "An unknown error occurred" }, { status: 500 });
6262
}
6363

64+
const run = await ApiRetrieveRunPresenter.findRun(
65+
updatedRun.friendlyId,
66+
authenticationResult.environment
67+
);
68+
69+
if (!run) {
70+
return json({ error: "Run not found" }, { status: 404 });
71+
}
72+
6473
const presenter = new ApiRetrieveRunPresenter();
65-
const result = await presenter.call(updatedRun.friendlyId, authenticationResult.environment);
74+
const result = await presenter.call(run, authenticationResult.environment);
6675

6776
if (!result) {
6877
return json({ error: "Run not found" }, { status: 404 });

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,10 @@ export const loader = createLoaderApiRoute(
1212
corsStrategy: "all",
1313
authorization: {
1414
action: "read",
15-
resource: (_, searchParams) => ({ tasks: searchParams["filter[taskIdentifier]"] }),
15+
resource: (_, __, searchParams) => ({ tasks: searchParams["filter[taskIdentifier]"] }),
1616
superScopes: ["read:runs", "read:all", "admin"],
1717
},
18+
findResource: async () => 1, // This is a dummy function, we don't need to find a resource
1819
},
1920
async ({ searchParams, authentication }) => {
2021
const presenter = new ApiRunListPresenter();

apps/webapp/app/routes/api.v1.tasks.batch.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ async function responseHeaders(
133133
const claims = {
134134
sub: environment.id,
135135
pub: true,
136-
scopes: [`read:batch:${batch.id}`].concat(batch.runs.map((r) => `read:runs:${r.id}`)),
136+
scopes: [`read:batch:${batch.id}`],
137137
};
138138

139139
const jwt = await generateJWT({

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

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,23 @@ export const loader = createLoaderApiRoute(
1212
params: ParamsSchema,
1313
allowJWT: true,
1414
corsStrategy: "all",
15+
findResource: (params, auth) => {
16+
return ApiRetrieveRunPresenter.findRun(params.runId, auth.environment);
17+
},
1518
authorization: {
1619
action: "read",
17-
resource: (params) => ({ runs: params.runId }),
20+
resource: (run) => ({
21+
runs: run.friendlyId,
22+
tags: run.runTags,
23+
batch: run.batch?.friendlyId,
24+
tasks: run.taskIdentifier,
25+
}),
1826
superScopes: ["read:runs", "read:all", "admin"],
1927
},
2028
},
21-
async ({ params, authentication }) => {
29+
async ({ authentication, resource }) => {
2230
const presenter = new ApiRetrieveRunPresenter();
23-
const result = await presenter.call(params.runId, authentication.environment);
31+
const result = await presenter.call(resource, authentication.environment);
2432

2533
if (!result) {
2634
return json(

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

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { json } from "@remix-run/server-runtime";
21
import { z } from "zod";
32
import { $replica } from "~/db.server";
43
import { realtimeClient } from "~/services/realtimeClientGlobal.server";
@@ -13,24 +12,21 @@ export const loader = createLoaderApiRoute(
1312
params: ParamsSchema,
1413
allowJWT: true,
1514
corsStrategy: "all",
15+
findResource: (params, auth) => {
16+
return $replica.batchTaskRun.findFirst({
17+
where: {
18+
friendlyId: params.batchId,
19+
runtimeEnvironmentId: auth.environment.id,
20+
},
21+
});
22+
},
1623
authorization: {
1724
action: "read",
18-
resource: (params) => ({ batch: params.batchId }),
25+
resource: (batch) => ({ batch: batch.friendlyId }),
1926
superScopes: ["read:runs", "read:all", "admin"],
2027
},
2128
},
22-
async ({ params, authentication, request }) => {
23-
const batchRun = await $replica.batchTaskRun.findFirst({
24-
where: {
25-
friendlyId: params.batchId,
26-
runtimeEnvironmentId: authentication.environment.id,
27-
},
28-
});
29-
30-
if (!batchRun) {
31-
return json({ error: "Batch not found" }, { status: 404 });
32-
}
33-
29+
async ({ authentication, request, resource: batchRun }) => {
3430
return realtimeClient.streamBatch(
3531
request.url,
3632
authentication.environment,

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

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -13,24 +13,33 @@ export const loader = createLoaderApiRoute(
1313
params: ParamsSchema,
1414
allowJWT: true,
1515
corsStrategy: "all",
16+
findResource: async (params, authentication) => {
17+
return $replica.taskRun.findFirst({
18+
where: {
19+
friendlyId: params.runId,
20+
runtimeEnvironmentId: authentication.environment.id,
21+
},
22+
include: {
23+
batch: {
24+
select: {
25+
friendlyId: true,
26+
},
27+
},
28+
},
29+
});
30+
},
1631
authorization: {
1732
action: "read",
18-
resource: (params) => ({ runs: params.runId }),
33+
resource: (run) => ({
34+
runs: run.friendlyId,
35+
tags: run.runTags,
36+
batch: run.batch?.friendlyId,
37+
tasks: run.taskIdentifier,
38+
}),
1939
superScopes: ["read:runs", "read:all", "admin"],
2040
},
2141
},
22-
async ({ params, authentication, request }) => {
23-
const run = await $replica.taskRun.findFirst({
24-
where: {
25-
friendlyId: params.runId,
26-
runtimeEnvironmentId: authentication.environment.id,
27-
},
28-
});
29-
30-
if (!run) {
31-
return json({ error: "Run not found" }, { status: 404 });
32-
}
33-
42+
async ({ authentication, request, resource: run }) => {
3443
return realtimeClient.streamRun(
3544
request.url,
3645
authentication.environment,

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,10 @@ export const loader = createLoaderApiRoute(
1616
searchParams: SearchParamsSchema,
1717
allowJWT: true,
1818
corsStrategy: "all",
19+
findResource: async () => 1, // This is a dummy value, it's not used
1920
authorization: {
2021
action: "read",
21-
resource: (_, searchParams) => searchParams,
22+
resource: (_, __, searchParams) => searchParams,
2223
superScopes: ["read:runs", "read:all", "admin"],
2324
},
2425
},

0 commit comments

Comments
 (0)