Skip to content

Commit 010c896

Browse files
committed
feat(webapp): enforce env var permissions on the API routes
The environment variable API routes now apply the caller's role to the targeted environment tier when authenticated with a personal access token, so a restricted role can't read or write deployed env vars via the API. Environment API keys are scoped to a single environment already, so they are unaffected.
1 parent 98408ef commit 010c896

4 files changed

Lines changed: 127 additions & 5 deletions

apps/webapp/app/routes/api.v1.projects.$projectRef.envvars.$slug.$name.ts

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
branchNameFromRequest,
99
} from "~/services/apiAuth.server";
1010
import { logger } from "~/services/logger.server";
11+
import { authorizeEnvVarApiRequest } from "~/services/environmentVariableApiAccess.server";
1112
import { EnvironmentVariablesRepository } from "~/v3/environmentVariables/environmentVariablesRepository.server";
1213

1314
const ParamsSchema = z.object({
@@ -24,7 +25,11 @@ export async function action({ params, request }: ActionFunctionArgs) {
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,16 @@ export async function action({ params, request }: ActionFunctionArgs) {
3742
branchNameFromRequest(request)
3843
);
3944

45+
const denied = await authorizeEnvVarApiRequest({
46+
request,
47+
authType: authenticationResult.type,
48+
organizationId: environment.organizationId,
49+
projectId: environment.project.id,
50+
envType: environment.type,
51+
action: "write",
52+
});
53+
if (denied) return denied;
54+
4055
// Find the environment variable
4156
const variable = await prisma.environmentVariable.findFirst({
4257
where: {
@@ -110,7 +125,11 @@ export async function loader({ params, request }: LoaderFunctionArgs) {
110125
}
111126

112127
try {
113-
const authenticationResult = await authenticateRequest(request);
128+
const authenticationResult = await authenticateRequest(request, {
129+
personalAccessToken: true,
130+
organizationAccessToken: true,
131+
apiKey: true,
132+
});
114133

115134
if (!authenticationResult) {
116135
return json({ error: "Invalid or Missing API key" }, { status: 401 });
@@ -123,6 +142,16 @@ export async function loader({ params, request }: LoaderFunctionArgs) {
123142
branchNameFromRequest(request)
124143
);
125144

145+
const denied = await authorizeEnvVarApiRequest({
146+
request,
147+
authType: authenticationResult.type,
148+
organizationId: environment.organizationId,
149+
projectId: environment.project.id,
150+
envType: environment.type,
151+
action: "read",
152+
});
153+
if (denied) return denied;
154+
126155
// Find the environment variable
127156
const variable = await prisma.environmentVariable.findFirst({
128157
where: {

apps/webapp/app/routes/api.v1.projects.$projectRef.envvars.$slug.import.ts

Lines changed: 16 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 { authorizeEnvVarApiRequest } from "~/services/environmentVariableApiAccess.server";
1011
import { EnvironmentVariablesRepository } from "~/v3/environmentVariables/environmentVariablesRepository.server";
1112

1213
const ParamsSchema = z.object({
@@ -21,7 +22,11 @@ export async function action({ params, request }: ActionFunctionArgs) {
2122
return json({ error: "Invalid params" }, { status: 400 });
2223
}
2324

24-
const authenticationResult = await authenticateRequest(request);
25+
const authenticationResult = await authenticateRequest(request, {
26+
personalAccessToken: true,
27+
organizationAccessToken: true,
28+
apiKey: true,
29+
});
2530

2631
if (!authenticationResult) {
2732
return json({ error: "Invalid or Missing API key" }, { status: 401 });
@@ -34,6 +39,16 @@ export async function action({ params, request }: ActionFunctionArgs) {
3439
branchNameFromRequest(request)
3540
);
3641

42+
const denied = await authorizeEnvVarApiRequest({
43+
request,
44+
authType: authenticationResult.type,
45+
organizationId: environment.organizationId,
46+
projectId: environment.project.id,
47+
envType: environment.type,
48+
action: "write",
49+
});
50+
if (denied) return denied;
51+
3752
const repository = new EnvironmentVariablesRepository();
3853

3954
const body = await parseImportBody(request);

apps/webapp/app/routes/api.v1.projects.$projectRef.envvars.$slug.ts

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
authenticatedEnvironmentForAuthentication,
77
branchNameFromRequest,
88
} from "~/services/apiAuth.server";
9+
import { authorizeEnvVarApiRequest } from "~/services/environmentVariableApiAccess.server";
910
import { EnvironmentVariablesRepository } from "~/v3/environmentVariables/environmentVariablesRepository.server";
1011

1112
const ParamsSchema = z.object({
@@ -20,7 +21,11 @@ export async function action({ params, request }: ActionFunctionArgs) {
2021
return json({ error: "Invalid params" }, { status: 400 });
2122
}
2223

23-
const authenticationResult = await authenticateRequest(request);
24+
const authenticationResult = await authenticateRequest(request, {
25+
personalAccessToken: true,
26+
organizationAccessToken: true,
27+
apiKey: true,
28+
});
2429

2530
if (!authenticationResult) {
2631
return json({ error: "Invalid or Missing API key" }, { status: 401 });
@@ -33,6 +38,16 @@ export async function action({ params, request }: ActionFunctionArgs) {
3338
branchNameFromRequest(request)
3439
);
3540

41+
const denied = await authorizeEnvVarApiRequest({
42+
request,
43+
authType: authenticationResult.type,
44+
organizationId: environment.organizationId,
45+
projectId: environment.project.id,
46+
envType: environment.type,
47+
action: "write",
48+
});
49+
if (denied) return denied;
50+
3651
const jsonBody = await request.json();
3752

3853
const body = CreateEnvironmentVariableRequestBody.safeParse(jsonBody);
@@ -68,7 +83,11 @@ export async function loader({ params, request }: LoaderFunctionArgs) {
6883
return json({ error: "Invalid params" }, { status: 400 });
6984
}
7085

71-
const authenticationResult = await authenticateRequest(request);
86+
const authenticationResult = await authenticateRequest(request, {
87+
personalAccessToken: true,
88+
organizationAccessToken: true,
89+
apiKey: true,
90+
});
7291

7392
if (!authenticationResult) {
7493
return json({ error: "Invalid or Missing API key" }, { status: 401 });
@@ -81,6 +100,16 @@ export async function loader({ params, request }: LoaderFunctionArgs) {
81100
branchNameFromRequest(request)
82101
);
83102

103+
const denied = await authorizeEnvVarApiRequest({
104+
request,
105+
authType: authenticationResult.type,
106+
organizationId: environment.organizationId,
107+
projectId: environment.project.id,
108+
envType: environment.type,
109+
action: "read",
110+
});
111+
if (denied) return denied;
112+
84113
const repository = new EnvironmentVariablesRepository();
85114

86115
const variables = await repository.getEnvironmentWithRedactedSecrets(
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { json } from "@remix-run/server-runtime";
2+
import type { RuntimeEnvironmentType } from "@trigger.dev/database";
3+
import { rbac } from "~/services/rbac.server";
4+
5+
/**
6+
* Env-tier RBAC for the environment-variable API routes.
7+
*
8+
* Machine credentials (an environment's secret/public API key) are already
9+
* scoped to a single environment, so they pass through unchanged. A personal
10+
* access token carries a user, so enforce that user's role for the targeted
11+
* environment tier — e.g. a Developer can't read or write deployed env vars
12+
* via the API, matching the dashboard restriction.
13+
*
14+
* Returns a `Response` to short-circuit with when access is denied, or
15+
* `undefined` when the request may proceed.
16+
*/
17+
export async function authorizeEnvVarApiRequest({
18+
request,
19+
authType,
20+
organizationId,
21+
projectId,
22+
envType,
23+
action,
24+
}: {
25+
request: Request;
26+
authType: "personalAccessToken" | "organizationAccessToken" | "apiKey";
27+
organizationId: string;
28+
projectId: string;
29+
envType: RuntimeEnvironmentType;
30+
action: "read" | "write";
31+
}): Promise<Response | undefined> {
32+
if (authType !== "personalAccessToken") {
33+
return undefined;
34+
}
35+
36+
const patAuth = await rbac.authenticatePat(request, { organizationId, projectId });
37+
if (!patAuth.ok) {
38+
return json({ error: patAuth.error }, { status: patAuth.status });
39+
}
40+
41+
if (!patAuth.ability.can(action, { type: "envvars", envType })) {
42+
return json(
43+
{ error: "You don't have permission to manage environment variables in this environment." },
44+
{ status: 403 }
45+
);
46+
}
47+
48+
return undefined;
49+
}

0 commit comments

Comments
 (0)