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 = { 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 { 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 { return authorizePatEnvironmentAccess({ ...opts, resource: "envvars" }); }