Skip to content

Commit d5f0d7d

Browse files
carderneTrigger.dev RepoOps
authored andcommitted
fix(webapp): reject cross-environment waitpoint registration
Waitpoint registration now verifies that the target run belongs to the authenticated environment. Mono-RevId: 0998236ba421350cd59c5c90fa09e918df0074cb
1 parent 6ae543a commit d5f0d7d

3 files changed

Lines changed: 243 additions & 3 deletions

File tree

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
area: webapp
3+
type: fix
4+
---
5+
6+
Reject waitpoint registrations that target a run outside the authenticated environment

‎apps/webapp/app/routes/engine.v1.runs.$runFriendlyId.waitpoints.tokens.$waitpointFriendlyId.wait.ts‎

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,12 @@ import { json } from "@remix-run/server-runtime";
22
import { type WaitForWaitpointTokenResponseBody } from "@trigger.dev/core/v3";
33
import { RunId, WaitpointId } from "@trigger.dev/core/v3/isomorphic";
44
import { z } from "zod";
5-
import type { PrismaReplicaClient } from "~/db.server";
5+
import { prisma, type PrismaReplicaClient } from "~/db.server";
66
import { resolveWaitpointThroughReadThrough } from "~/runEngine/concerns/resolveWaitpointThroughReadThrough.server";
77
import { logger } from "~/services/logger.server";
88
import { createActionApiRoute } from "~/services/routeBuilders/apiBuilder.server";
99
import { engine } from "~/v3/runEngine.server";
10+
import { runStore } from "~/v3/runStore.server";
1011

1112
const { action } = createActionApiRoute(
1213
{
@@ -23,7 +24,19 @@ const { action } = createActionApiRoute(
2324
const runId = RunId.toId(params.runFriendlyId);
2425

2526
try {
26-
//check permissions
27+
const run = await runStore.findRun(
28+
{
29+
id: runId,
30+
runtimeEnvironmentId: authentication.environment.id,
31+
},
32+
{ select: { id: true } },
33+
prisma
34+
);
35+
36+
if (!run) {
37+
throw new Response("You don't have permissions for this run", { status: 401 });
38+
}
39+
2740
const waitpoint = await resolveWaitpointThroughReadThrough({
2841
waitpointId,
2942
environmentId: authentication.environment.id,
@@ -49,7 +62,7 @@ const { action } = createActionApiRoute(
4962
}
5063

5164
const _result = await engine.blockRunWithWaitpoint({
52-
runId,
65+
runId: run.id,
5366
waitpoints: [waitpointId],
5467
projectId: authentication.environment.project.id,
5568
organizationId: authentication.environment.organization.id,
Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
import { PostgresRunStore } from "@internal/run-store";
2+
import { postgresTest } from "@internal/testcontainers";
3+
import type { PrismaClient } from "@trigger.dev/database";
4+
import { RunId, WaitpointId } from "@trigger.dev/core/v3/isomorphic";
5+
import { beforeEach, describe, expect, vi } from "vitest";
6+
import { seedTestEnvironment } from "./helpers/seedTestEnvironment";
7+
8+
vi.setConfig({ testTimeout: 60_000, hookTimeout: 60_000 });
9+
10+
const H = vi.hoisted(() => ({
11+
handlers: [] as Array<{ config: any; handler: any }>,
12+
store: undefined as any,
13+
prisma: undefined as any,
14+
engineCalls: [] as Array<Record<string, unknown>>,
15+
}));
16+
17+
vi.mock("~/db.server", () => {
18+
const prisma = new Proxy(
19+
{},
20+
{
21+
get(_target, property) {
22+
const value = H.prisma[property];
23+
return typeof value === "object" && value !== null
24+
? new Proxy(value, { get: (_delegate, method) => H.prisma[property][method] })
25+
: value;
26+
},
27+
}
28+
);
29+
30+
return {
31+
prisma,
32+
$replica: prisma,
33+
runOpsLegacyReplica: prisma,
34+
runOpsNewPrisma: prisma,
35+
runOpsNewReplica: prisma,
36+
runOpsSplitReadEnabled: false,
37+
};
38+
});
39+
40+
vi.mock("~/env.server", () => ({
41+
env: {
42+
RUN_OPS_SPLIT_ENABLED: false,
43+
RUN_OPS_SHARDS: [],
44+
},
45+
}));
46+
47+
vi.mock("~/v3/runOpsMigration/shardHandles.server", () => ({
48+
runOpsShardReplicas: new Map(),
49+
runOpsShardWriters: new Map(),
50+
}));
51+
52+
vi.mock("~/v3/runStore.server", () => ({
53+
runStore: new Proxy(
54+
{},
55+
{
56+
get(_target, property) {
57+
const value = H.store[property];
58+
return typeof value === "function" ? value.bind(H.store) : value;
59+
},
60+
}
61+
),
62+
}));
63+
64+
vi.mock("~/services/routeBuilders/apiBuilder.server", () => ({
65+
createActionApiRoute: (config: any, handler: any) => {
66+
H.handlers.push({ config, handler });
67+
return { action: vi.fn() };
68+
},
69+
}));
70+
71+
vi.mock("~/v3/runEngine.server", () => ({
72+
engine: {
73+
blockRunWithWaitpoint: vi.fn(async (args: Record<string, unknown>) => {
74+
H.engineCalls.push(args);
75+
return {};
76+
}),
77+
},
78+
}));
79+
80+
vi.mock("~/services/logger.server", () => ({
81+
logger: { error: vi.fn() },
82+
}));
83+
84+
async function routeHandler() {
85+
await import("~/routes/engine.v1.runs.$runFriendlyId.waitpoints.tokens.$waitpointFriendlyId.wait");
86+
const entry = H.handlers.find(
87+
({ config }) =>
88+
config?.params?.shape?.runFriendlyId && config?.params?.shape?.waitpointFriendlyId
89+
);
90+
if (!entry) throw new Error("waitpoint wait route handler was not captured");
91+
return entry.handler as (args: any) => Promise<Response>;
92+
}
93+
94+
async function seedRun(
95+
prisma: PrismaClient,
96+
environment: { id: string; projectId: string; organizationId: string }
97+
) {
98+
const run = RunId.generate();
99+
await prisma.taskRun.create({
100+
data: {
101+
id: run.id,
102+
friendlyId: run.friendlyId,
103+
engine: "V2",
104+
taskIdentifier: "authorization-test",
105+
payload: "{}",
106+
traceId: `trace_${run.id}`,
107+
spanId: `span_${run.id}`,
108+
queue: "task/authorization-test",
109+
projectId: environment.projectId,
110+
runtimeEnvironmentId: environment.id,
111+
organizationId: environment.organizationId,
112+
environmentType: "DEVELOPMENT",
113+
runTags: [],
114+
},
115+
});
116+
return run;
117+
}
118+
119+
async function seedWaitpoint(prisma: PrismaClient, environmentId: string, projectId: string) {
120+
const waitpoint = WaitpointId.generate();
121+
await prisma.waitpoint.create({
122+
data: {
123+
id: waitpoint.id,
124+
friendlyId: waitpoint.friendlyId,
125+
type: "MANUAL",
126+
status: "PENDING",
127+
idempotencyKey: waitpoint.id,
128+
userProvidedIdempotencyKey: false,
129+
environmentId,
130+
projectId,
131+
},
132+
});
133+
return waitpoint;
134+
}
135+
136+
beforeEach(() => {
137+
H.engineCalls = [];
138+
});
139+
140+
describe("waitpoint-token wait run authorization", () => {
141+
postgresTest("rejects a run from another runtime environment", async ({ prisma }) => {
142+
const seed = await seedTestEnvironment(prisma);
143+
const foreignEnvironment = await prisma.runtimeEnvironment.create({
144+
data: {
145+
slug: "staging",
146+
type: "DEVELOPMENT",
147+
apiKey: `tr_dev_foreign_${seed.environment.id}`,
148+
pkApiKey: `pk_dev_foreign_${seed.environment.id}`,
149+
shortcode: `foreign_${seed.environment.id}`,
150+
projectId: seed.project.id,
151+
organizationId: seed.organization.id,
152+
},
153+
});
154+
const run = await seedRun(prisma, foreignEnvironment);
155+
const waitpoint = await seedWaitpoint(prisma, seed.environment.id, seed.project.id);
156+
157+
H.prisma = prisma;
158+
H.store = new PostgresRunStore({ prisma, readOnlyPrisma: prisma });
159+
const handler = await routeHandler();
160+
161+
const response = await handler({
162+
authentication: {
163+
environment: {
164+
...seed.environment,
165+
project: seed.project,
166+
organization: seed.organization,
167+
},
168+
},
169+
params: {
170+
runFriendlyId: run.friendlyId,
171+
waitpointFriendlyId: waitpoint.friendlyId,
172+
},
173+
}).then(
174+
(result) => result,
175+
(error) => error
176+
);
177+
178+
expect(response).toBeInstanceOf(Response);
179+
expect(response.status).toBe(401);
180+
expect(await response.text()).toBe("You don't have permissions for this run");
181+
expect(H.engineCalls).toHaveLength(0);
182+
});
183+
184+
postgresTest(
185+
"attaches a waitpoint to a run in the authenticated environment",
186+
async ({ prisma }) => {
187+
const seed = await seedTestEnvironment(prisma);
188+
const run = await seedRun(prisma, seed.environment);
189+
const waitpoint = await seedWaitpoint(prisma, seed.environment.id, seed.project.id);
190+
191+
H.prisma = prisma;
192+
H.store = new PostgresRunStore({ prisma, readOnlyPrisma: prisma });
193+
const handler = await routeHandler();
194+
195+
const response = await handler({
196+
authentication: {
197+
environment: {
198+
...seed.environment,
199+
project: seed.project,
200+
organization: seed.organization,
201+
},
202+
},
203+
params: {
204+
runFriendlyId: run.friendlyId,
205+
waitpointFriendlyId: waitpoint.friendlyId,
206+
},
207+
});
208+
209+
expect(response.status).toBe(200);
210+
expect(await response.json()).toEqual({ success: true });
211+
expect(H.engineCalls).toEqual([
212+
{
213+
runId: run.id,
214+
waitpoints: [waitpoint.id],
215+
projectId: seed.project.id,
216+
organizationId: seed.organization.id,
217+
},
218+
]);
219+
}
220+
);
221+
});

0 commit comments

Comments
 (0)