Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 1 addition & 14 deletions apps/webapp/app/models/api-key.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import { customAlphabet } from "nanoid";
import { MAX_API_KEY_TASK_IDENTIFIERS } from "~/consts";
import { $transaction, boundedIn, prisma } from "~/db.server";
import { RuntimeEnvironmentType } from "~/database-types";
import { canIssueAdditionalApiKeys } from "~/services/additionalApiKeyIssuance.server";
import { apiKeyTelemetry, type ApiKeyTelemetry } from "~/services/apiKeyTelemetry.server";
import { rbac } from "~/services/rbac.server";
import { generateAdditionalApiKey, generateRootApiKey } from "~/utils/apiKeys";
Expand Down Expand Up @@ -225,15 +224,10 @@ export async function createEnvironmentApiKey(
{
prismaClient = prisma,
rbacController = rbac,
issuanceAllowed,
telemetryRecorder = apiKeyTelemetry,
}: {
prismaClient?: Pick<
PrismaClient,
"apiKey" | "featureFlag" | "organization" | "runtimeEnvironment" | "taskIdentifier"
>;
prismaClient?: Pick<PrismaClient, "apiKey" | "runtimeEnvironment" | "taskIdentifier">;
rbacController?: Pick<HostRbacController, "prepareApiKeyPolicy">;
issuanceAllowed?: (organizationId: string) => Promise<boolean>;
telemetryRecorder?: ApiKeyTelemetry;
} = {}
) {
Expand All @@ -249,13 +243,6 @@ export async function createEnvironmentApiKey(
throw new Error("Environment not found");
}

const canIssue =
issuanceAllowed ??
((organizationId) => canIssueAdditionalApiKeys(organizationId, prismaClient));
if (!(await canIssue(environment.organizationId))) {
throw new Error("Creating additional API keys is not enabled.");
}

if (expiresAt && expiresAt.getTime() <= Date.now()) {
throw new Error("Expiration must be in the future");
}
Expand Down
24 changes: 6 additions & 18 deletions apps/webapp/app/models/runtimeEnvironment.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import { BuildRuntime } from "@trigger.dev/core/v3";
import { isAdditionalApiKey } from "@trigger.dev/core/v3/apiKeys";
import { isDefaultDevBranch, sanitizeBranchName } from "@trigger.dev/core/v3/utils/gitBranch";
import { scopesGrantFullAccess } from "@trigger.dev/rbac";
import { authFeatureControls } from "~/services/authFeatureControls.server";

export type { RuntimeEnvironment };

Expand Down Expand Up @@ -101,7 +100,7 @@ export function toAuthenticated(

export type ApiKeyEnvironmentResolution =
| { ok: true; environment: AuthenticatedEnvironment }
| { ok: false; reason: "not-found" | "restricted" | "disabled" };
| { ok: false; reason: "not-found" | "restricted" };

/**
* Resolve an environment from a raw API key for legacy routes that do not
Expand All @@ -112,8 +111,7 @@ export type ApiKeyEnvironmentResolution =
async function resolveEnvironmentByApiKey(
apiKey: string,
branchName: string | undefined,
tx: PrismaClientOrTransaction,
additionalApiKeyLookupEnabled: () => boolean
tx: PrismaClientOrTransaction
): Promise<ApiKeyEnvironmentResolution> {
const branch = sanitizeBranchName(branchName) ?? undefined;

Expand All @@ -131,9 +129,6 @@ async function resolveEnvironmentByApiKey(

const now = new Date();
const routesToAdditionalKey = isAdditionalApiKey(apiKey);
if (routesToAdditionalKey && !additionalApiKeyLookupEnabled()) {
return { ok: false, reason: "disabled" };
}

let rootEnvironment = routesToAdditionalKey
? null
Expand Down Expand Up @@ -277,15 +272,9 @@ async function resolveEnvironmentByApiKey(
export async function findEnvironmentByApiKey(
apiKey: string,
branchName: string | undefined,
tx: PrismaClientOrTransaction = $replica,
additionalApiKeyLookupEnabled = authFeatureControls.additionalApiKeyLookupEnabled
tx: PrismaClientOrTransaction = $replica
): Promise<AuthenticatedEnvironment | null> {
const resolution = await resolveEnvironmentByApiKey(
apiKey,
branchName,
tx,
additionalApiKeyLookupEnabled
);
const resolution = await resolveEnvironmentByApiKey(apiKey, branchName, tx);
return resolution.ok ? resolution.environment : null;
}

Expand All @@ -297,10 +286,9 @@ export async function findEnvironmentByApiKey(
export async function findEnvironmentByApiKeyWithResolution(
apiKey: string,
branchName: string | undefined,
tx: PrismaClientOrTransaction = $replica,
additionalApiKeyLookupEnabled = authFeatureControls.additionalApiKeyLookupEnabled
tx: PrismaClientOrTransaction = $replica
): Promise<ApiKeyEnvironmentResolution> {
return resolveEnvironmentByApiKey(apiKey, branchName, tx, additionalApiKeyLookupEnabled);
return resolveEnvironmentByApiKey(apiKey, branchName, tx);
}

export type PrivateApiKeyRateLimitScope = {
Expand Down

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 Missing server release note

Additional API keys become available to every organization, but the PR adds no required .server-changes/ entry.

Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Original file line number Diff line number Diff line change
Expand Up @@ -64,14 +64,12 @@ import {
typedJsonWithErrorMessage,
typedJsonWithSuccessMessage,
} from "~/models/message.server";
import { resolveOrgIdFromSlug } from "~/models/organization.server";
import { findProjectBySlug } from "~/models/project.server";
import { findEnvironmentBySlug } from "~/models/runtimeEnvironment.server";
import { ApiKeysPresenter } from "~/presenters/v3/ApiKeysPresenter.server";
import { useFeatures } from "~/hooks/useFeatures";
import { useOrganization } from "~/hooks/useOrganizations";
import { useShowSelfServe } from "~/hooks/useShowSelfServe";
import { canIssueAdditionalApiKeys } from "~/services/additionalApiKeyIssuance.server";
import {
validateCreateApiKeyPreset,
type ApiKeyPreset,
Expand Down Expand Up @@ -135,25 +133,18 @@ export const loader = dashboardLoader(
{
params: EnvironmentParamSchema,
searchParams: ApiKeySearchParams,
context: async (params) => {
const organizationId = await resolveOrgIdFromSlug(params.organizationSlug);
return organizationId ? { organizationId } : {};
},
},
async ({ params, searchParams, user, ability, context }) => {
async ({ params, searchParams, user, ability }) => {
try {
const presenter = new ApiKeysPresenter();
const [data, additionalApiKeyIssuanceEnabled, isRbacPluginAvailable] = await Promise.all([
const [data, isRbacPluginAvailable] = await Promise.all([
presenter.call({
userId: user.id,
organizationSlug: params.organizationSlug,
projectSlug: params.projectParam,
environmentSlug: params.envParam,
showRevoked: searchParams.showRevoked,
}),
context.organizationId
? canIssueAdditionalApiKeys(context.organizationId)
: Promise.resolve(false),
rbac.isUsingPlugin(),
]);

Expand All @@ -176,7 +167,6 @@ export const loader = dashboardLoader(
apiKeys: canReadApiKeys ? data.apiKeys : [],
canReadApiKeys,
canWriteApiKeys,
additionalApiKeyIssuanceEnabled,
isRbacPluginAvailable,
showRevoked: searchParams.showRevoked ?? false,
loadedAt: Date.now(),
Expand All @@ -194,10 +184,6 @@ export const loader = dashboardLoader(
export const action = dashboardAction(
{
params: EnvironmentParamSchema,
context: async (params) => {
const organizationId = await resolveOrgIdFromSlug(params.organizationSlug);
return organizationId ? { organizationId } : {};
},
// The environment tier is only known after resolving the route params,
// so write:apiKeys is enforced in the handler before any mutation.
},
Expand Down Expand Up @@ -241,15 +227,6 @@ export const action = dashboardAction(
try {
switch (submission.data.action) {
case "create": {
if (!(await canIssueAdditionalApiKeys(project.organizationId))) {
const message = "Creating additional API keys is not enabled.";
return typedJsonWithErrorMessage(
{ ok: false as const, error: message },
request,
message
);
}

const presets = await rbac.apiKeyPresets(project.organizationId);
const preset = validateCreateApiKeyPreset({
presets,
Expand Down Expand Up @@ -321,7 +298,6 @@ export default function Page() {
apiKeys,
canReadApiKeys,
canWriteApiKeys,
additionalApiKeyIssuanceEnabled,
isRbacPluginAvailable,
showRevoked,
hasVercelIntegration,
Expand Down Expand Up @@ -386,15 +362,13 @@ export default function Page() {
/>
) : null}
<RevokedFilter checked={showRevoked} />
{additionalApiKeyIssuanceEnabled ? (
<NewApiKeyDialog
canWrite={canWriteApiKeys}
availableTasks={availableTasks}
presets={presets}
isRbacPluginAvailable={isRbacPluginAvailable}
environment={apiKeyEnvironmentLabel}
/>
) : null}
<NewApiKeyDialog
canWrite={canWriteApiKeys}
availableTasks={availableTasks}
presets={presets}
isRbacPluginAvailable={isRbacPluginAvailable}
environment={apiKeyEnvironmentLabel}
/>
</div>
</div>

Expand Down
35 changes: 0 additions & 35 deletions apps/webapp/app/services/additionalApiKeyIssuance.server.ts

This file was deleted.

19 changes: 0 additions & 19 deletions apps/webapp/app/services/additionalApiKeyIssuance.ts

This file was deleted.

10 changes: 0 additions & 10 deletions apps/webapp/app/services/authFeatureControls.server.ts

This file was deleted.

16 changes: 0 additions & 16 deletions apps/webapp/app/services/authFeatureControls.ts

This file was deleted.

35 changes: 6 additions & 29 deletions apps/webapp/app/services/authTelemetry.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,10 @@ import type {
HostBearerAuthResult,
RbacResource,
} from "@trigger.dev/rbac";
import { authFeatureControls } from "~/services/authFeatureControls.server";
import { rbac } from "~/services/rbac.server";
import { singleton } from "~/utils/singleton";

type ApiAuthResult = "success" | "invalid" | "forbidden" | "disabled" | "error";
type ApiAuthResult = "success" | "invalid" | "forbidden" | "error";

const telemetry = singleton("apiAuthTelemetry", () => {
const meter = getMeter("api-auth");
Expand All @@ -24,17 +23,6 @@ const telemetry = singleton("apiAuthTelemetry", () => {
unit: "ms",
});

meter
.createObservableGauge("api_auth.rollout_mode", {
description: "Active API authentication rollout modes",
})
.addCallback((result) => {
result.observe(1, {
control: "additional_key_lookup",
mode: authFeatureControls.additionalApiKeyLookupEnabled() ? "enabled" : "disabled",
});
});

return { attempts, duration };
});

Expand All @@ -54,13 +42,7 @@ export async function authenticateBearerWithTelemetry(
final = {
credentialKind: resolution.credentialKind,
lookupPath: resolution.lookupPath,
result: result.ok
? "success"
: resolution.lookupPath === "additional_skipped"
? "disabled"
: result.status === 403
? "forbidden"
: "invalid",
result: result.ok ? "success" : result.status === 403 ? "forbidden" : "invalid",
};
recordAuthAttempt("rbac", final.credentialKind, final.lookupPath, final.result);
return result;
Expand Down Expand Up @@ -102,27 +84,22 @@ export async function observeLegacyBearerAuthentication<T extends { ok: boolean
): Promise<T> {
const startedAt = performance.now();
const classified = classifyCredential(request, true);
const lookupPath: BearerLookupPath =
classified.credentialKind === "additional_api_key" &&
!authFeatureControls.additionalApiKeyLookupEnabled()
? "additional_skipped"
: classified.lookupPath;
let result: ApiAuthResult = "error";

try {
const value = await operation();
result = value?.ok ? "success" : lookupPath === "additional_skipped" ? "disabled" : "invalid";
recordAuthAttempt("legacy", classified.credentialKind, lookupPath, result);
result = value?.ok ? "success" : "invalid";
recordAuthAttempt("legacy", classified.credentialKind, classified.lookupPath, result);
return value;
} catch (error) {
recordAuthAttempt("legacy", classified.credentialKind, lookupPath, result);
recordAuthAttempt("legacy", classified.credentialKind, classified.lookupPath, result);
throw error;
} finally {
telemetry.duration.record(performance.now() - startedAt, {
resolver: "legacy",
credential_kind: classified.credentialKind,
result,
lookup_path: lookupPath,
lookup_path: classified.lookupPath,
});
}
}
Expand Down
2 changes: 0 additions & 2 deletions apps/webapp/app/services/rbac.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { $replica, prisma } from "~/db.server";
import type { PrismaClient } from "@trigger.dev/database";
import plugin from "@trigger.dev/rbac";
import { env } from "~/env.server";
import { authFeatureControls } from "~/services/authFeatureControls.server";

// plugin.create() is synchronous — returns a lazy controller that resolves
// any installed RBAC plugin on first call. Top-level await is not used
Expand Down Expand Up @@ -31,7 +30,6 @@ export const rbac = plugin.create(
{
forceFallback: env.RBAC_FORCE_FALLBACK,
userActorSecret: env.SESSION_SECRET,
additionalApiKeyLookupEnabled: authFeatureControls.additionalApiKeyLookupEnabled,
// A plugin that owns its own database client gets the same
// writer/replica topology the webapp's Prisma clients use (see
// getClient/getReplicaClient in db.server.ts): control-plane URLs win,
Expand Down
Loading
Loading