Skip to content

Commit b43672d

Browse files
pullfrog[bot]Trigger.dev RepoOps
authored andcommitted
fix(webapp): align worker route access checks
Mono-RevId: f71e922acd772a423b323249a1c75e4fe008e997
1 parent 9dac91f commit b43672d

2 files changed

Lines changed: 128 additions & 1 deletion

File tree

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import { createHash, randomBytes } from "node:crypto";
2+
import { deflateSync } from "node:zlib";
3+
import { afterAll, beforeAll, describe, expect, it } from "vitest";
4+
import { startTestServer, type TestServer } from "@internal/testcontainers/webapp";
5+
import { seedTestUserProject } from "../../test/helpers/seedTestUserProject";
6+
7+
let server: TestServer;
8+
let fixture: Awaited<ReturnType<typeof seedWorkerSource>>;
9+
10+
beforeAll(async () => {
11+
server = await startTestServer();
12+
fixture = await seedWorkerSource();
13+
}, 180_000);
14+
15+
afterAll(async () => {
16+
await server?.stop();
17+
}, 120_000);
18+
19+
async function seedWorkerSource() {
20+
const seed = await seedTestUserProject(server.prisma);
21+
const member = await server.prisma.orgMember.findFirstOrThrow({
22+
where: { organizationId: seed.organization.id, userId: seed.user.id },
23+
});
24+
await server.prisma.runtimeEnvironment.update({
25+
where: { id: seed.environment.id },
26+
data: { orgMemberId: member.id },
27+
});
28+
29+
const oat = `tr_oat_${randomBytes(20).toString("hex")}`;
30+
await server.prisma.organizationAccessToken.create({
31+
data: {
32+
name: "worker-source-test",
33+
organizationId: seed.organization.id,
34+
hashedToken: createHash("sha256").update(oat).digest("hex"),
35+
},
36+
});
37+
38+
const source = 'export const task = "worker source";';
39+
const worker = await server.prisma.backgroundWorker.create({
40+
data: {
41+
friendlyId: `worker_${randomBytes(8).toString("hex")}`,
42+
engine: "V2",
43+
projectId: seed.project.id,
44+
runtimeEnvironmentId: seed.environment.id,
45+
version: "20261009.1",
46+
contentHash: "worker-source-hash",
47+
metadata: {},
48+
files: {
49+
create: {
50+
friendlyId: `file_${randomBytes(8).toString("hex")}`,
51+
projectId: seed.project.id,
52+
filePath: "src/task.ts",
53+
contentHash: "file-source-hash",
54+
contents: Buffer.from(deflateSync(source).toString("base64")),
55+
},
56+
},
57+
},
58+
});
59+
60+
return {
61+
...seed,
62+
oat,
63+
worker,
64+
source,
65+
path: `/api/v1/projects/${seed.project.externalRef}/background-workers/dev/${worker.version}`,
66+
};
67+
}
68+
69+
// This harness uses real OSS authentication; restricted PAT roles require the enterprise plugin.
70+
describe("background worker source authorization", () => {
71+
it.each(["PAT", "OAT", "API key"] as const)(
72+
"returns decompressed source to an authorized %s",
73+
async (credential) => {
74+
const token =
75+
credential === "PAT"
76+
? fixture.pat.token
77+
: credential === "OAT"
78+
? fixture.oat
79+
: fixture.environment.apiKey;
80+
const response = await server.webapp.fetch(fixture.path, {
81+
headers: { Authorization: `Bearer ${token}` },
82+
});
83+
84+
expect(response.status).toBe(200);
85+
expect(await response.json()).toMatchObject({
86+
id: fixture.worker.friendlyId,
87+
version: fixture.worker.version,
88+
files: [{ contents: fixture.source, filePath: "src/task.ts" }],
89+
});
90+
}
91+
);
92+
93+
it("rejects missing credentials", async () => {
94+
const response = await server.webapp.fetch(fixture.path);
95+
96+
expect(response.status).toBe(401);
97+
});
98+
99+
it("does not expose source to a PAT from another organization", async () => {
100+
const other = await seedTestUserProject(server.prisma);
101+
const response = await server.webapp.fetch(fixture.path, {
102+
headers: { Authorization: `Bearer ${other.pat.token}` },
103+
});
104+
105+
expect(response.status).toBe(404);
106+
expect(await response.text()).not.toContain(fixture.source);
107+
});
108+
});

‎apps/webapp/app/routes/api.v1.projects.$projectRef.background-workers.$envSlug.$version.ts‎

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
authenticatedEnvironmentForAuthentication,
88
branchNameFromRequest,
99
} from "~/services/apiAuth.server";
10+
import { authorizePatEnvironmentAccess } from "~/services/environmentVariableApiAccess.server";
1011
import { logger } from "~/services/logger.server";
1112
import zlib from "node:zlib";
1213

@@ -24,7 +25,11 @@ export async function loader({ params, request }: LoaderFunctionArgs) {
2425
}
2526

2627
try {
27-
const authenticationResult = await authenticateRequest(request);
28+
const authenticationResult = await authenticateRequest(request, {
29+
personalAccessToken: true,
30+
organizationAccessToken: true,
31+
apiKey: true,
32+
});
2833

2934
if (!authenticationResult) {
3035
return json({ error: "Invalid or Missing API key" }, { status: 401 });
@@ -37,6 +42,20 @@ export async function loader({ params, request }: LoaderFunctionArgs) {
3742
branchNameFromRequest(request)
3843
);
3944

45+
// Legacy API-key authentication already scopes the caller to its environment.
46+
if (authenticationResult.type !== "apiKey") {
47+
const denied = await authorizePatEnvironmentAccess({
48+
request,
49+
authType: authenticationResult.type,
50+
organizationId: environment.organizationId,
51+
projectId: environment.project.id,
52+
envType: environment.type,
53+
resource: "deployments",
54+
action: "read",
55+
});
56+
if (denied) return denied;
57+
}
58+
4059
// Find the background worker and tasks and files
4160
const backgroundWorker = await prisma.backgroundWorker.findFirst({
4261
where: {

0 commit comments

Comments
 (0)