Files
triggerdotdev--trigger.dev/apps/webapp/app/services/environmentVariableApiAccess.server.ts
2026-07-13 13:32:57 +08:00

88 lines
2.9 KiB
TypeScript

import { json } from "@remix-run/server-runtime";
import type { RuntimeEnvironmentType } from "@trigger.dev/database";
import { isUserActorToken } from "@trigger.dev/rbac";
import { rbac } from "~/services/rbac.server";
type EnvironmentScopedResource = "envvars" | "apiKeys";
const RESOURCE_LABELS: Record<EnvironmentScopedResource, string> = {
envvars: "environment variables",
apiKeys: "API keys",
};
/**
* Env-tier RBAC for environment-scoped API routes (env vars, and the endpoints
* that hand out an environment's secret credentials).
*
* Machine credentials (an environment's secret/public API key) are already
* scoped to a single environment, so they pass through unchanged. A personal
* access token (or a delegated user-actor token) carries a user, so enforce
* that user's role for the targeted environment tier — e.g. a Developer can't
* read deployed env vars or API keys via the API, matching the dashboard
* restriction. Blocking the credential read for deployed tiers is also what
* stops a restricted role deploying via the CLI (deploy needs the
* environment's secret key).
*
* Returns a `Response` to short-circuit with when access is denied, or
* `undefined` when the request may proceed.
*/
export async function authorizePatEnvironmentAccess({
request,
authType,
organizationId,
projectId,
envType,
resource,
action,
}: {
request: Request;
authType: "personalAccessToken" | "organizationAccessToken" | "apiKey";
organizationId: string;
projectId: string;
envType: RuntimeEnvironmentType;
resource: EnvironmentScopedResource;
action: "read" | "write";
}): Promise<Response | undefined> {
const bearer = request.headers
.get("Authorization")
?.replace(/^Bearer /, "")
.trim();
const isUat = !!bearer && isUserActorToken(bearer);
// Machine creds (apiKey) and org tokens carry no user role to enforce. A
// user-actor token carries a user just like a PAT, so it's gated too.
if (authType !== "personalAccessToken" && !isUat) {
return undefined;
}
const userAuth = isUat
? await rbac.authenticateUserActor(request, { organizationId, projectId })
: await rbac.authenticatePat(request, { organizationId, projectId });
if (!userAuth.ok) {
return json({ error: userAuth.error }, { status: userAuth.status });
}
if (!userAuth.ability.can(action, { type: resource, envType })) {
return json(
{
error: `You don't have permission to access this environment's ${RESOURCE_LABELS[resource]}.`,
},
{ status: 403 }
);
}
return undefined;
}
/** Env-tier env var access for the env var API routes. */
export function authorizeEnvVarApiRequest(opts: {
request: Request;
authType: "personalAccessToken" | "organizationAccessToken" | "apiKey";
organizationId: string;
projectId: string;
envType: RuntimeEnvironmentType;
action: "read" | "write";
}): Promise<Response | undefined> {
return authorizePatEnvironmentAccess({ ...opts, resource: "envvars" });
}