diff --git a/apps/webapp/app/models/api-key.server.ts b/apps/webapp/app/models/api-key.server.ts index 7dccbb896e5..1f62629a563 100644 --- a/apps/webapp/app/models/api-key.server.ts +++ b/apps/webapp/app/models/api-key.server.ts @@ -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"; @@ -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; rbacController?: Pick; - issuanceAllowed?: (organizationId: string) => Promise; telemetryRecorder?: ApiKeyTelemetry; } = {} ) { @@ -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"); } diff --git a/apps/webapp/app/models/runtimeEnvironment.server.ts b/apps/webapp/app/models/runtimeEnvironment.server.ts index 790576200ec..0fe9f800c22 100644 --- a/apps/webapp/app/models/runtimeEnvironment.server.ts +++ b/apps/webapp/app/models/runtimeEnvironment.server.ts @@ -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 }; @@ -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 @@ -112,8 +111,7 @@ export type ApiKeyEnvironmentResolution = async function resolveEnvironmentByApiKey( apiKey: string, branchName: string | undefined, - tx: PrismaClientOrTransaction, - additionalApiKeyLookupEnabled: () => boolean + tx: PrismaClientOrTransaction ): Promise { const branch = sanitizeBranchName(branchName) ?? undefined; @@ -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 @@ -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 { - const resolution = await resolveEnvironmentByApiKey( - apiKey, - branchName, - tx, - additionalApiKeyLookupEnabled - ); + const resolution = await resolveEnvironmentByApiKey(apiKey, branchName, tx); return resolution.ok ? resolution.environment : null; } @@ -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 { - return resolveEnvironmentByApiKey(apiKey, branchName, tx, additionalApiKeyLookupEnabled); + return resolveEnvironmentByApiKey(apiKey, branchName, tx); } export type PrivateApiKeyRateLimitScope = { diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.apikeys/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.apikeys/route.tsx index 5f17a7d5661..4b3df3f8107 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.apikeys/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.apikeys/route.tsx @@ -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, @@ -135,15 +133,11 @@ 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, @@ -151,9 +145,6 @@ export const loader = dashboardLoader( environmentSlug: params.envParam, showRevoked: searchParams.showRevoked, }), - context.organizationId - ? canIssueAdditionalApiKeys(context.organizationId) - : Promise.resolve(false), rbac.isUsingPlugin(), ]); @@ -176,7 +167,6 @@ export const loader = dashboardLoader( apiKeys: canReadApiKeys ? data.apiKeys : [], canReadApiKeys, canWriteApiKeys, - additionalApiKeyIssuanceEnabled, isRbacPluginAvailable, showRevoked: searchParams.showRevoked ?? false, loadedAt: Date.now(), @@ -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. }, @@ -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, @@ -321,7 +298,6 @@ export default function Page() { apiKeys, canReadApiKeys, canWriteApiKeys, - additionalApiKeyIssuanceEnabled, isRbacPluginAvailable, showRevoked, hasVercelIntegration, @@ -386,15 +362,13 @@ export default function Page() { /> ) : null} - {additionalApiKeyIssuanceEnabled ? ( - - ) : null} + diff --git a/apps/webapp/app/services/additionalApiKeyIssuance.server.ts b/apps/webapp/app/services/additionalApiKeyIssuance.server.ts deleted file mode 100644 index f589069b885..00000000000 --- a/apps/webapp/app/services/additionalApiKeyIssuance.server.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { type PrismaClient } from "@trigger.dev/database"; -import { prisma } from "~/db.server"; -import { resolveAdditionalApiKeyIssuance } from "~/services/additionalApiKeyIssuance"; -import { FEATURE_FLAG } from "~/v3/featureFlags"; - -type IssuancePrismaClient = Pick; - -export async function canIssueAdditionalApiKeys( - organizationId: string, - prismaClient: IssuancePrismaClient = prisma -): Promise { - const [organization, globalFlags] = await Promise.all([ - prismaClient.organization.findFirst({ - where: { id: organizationId }, - select: { featureFlags: true }, - }), - prismaClient.featureFlag.findMany({ - where: { - key: { - in: [FEATURE_FLAG.additionalApiKeysEnabled, FEATURE_FLAG.additionalApiKeyIssuanceEnabled], - }, - }, - select: { key: true, value: true }, - }), - ]); - - if (!organization) { - return false; - } - - return resolveAdditionalApiKeyIssuance( - Object.fromEntries(globalFlags.map((featureFlag) => [featureFlag.key, featureFlag.value])), - (organization.featureFlags as Record | null) ?? undefined - ); -} diff --git a/apps/webapp/app/services/additionalApiKeyIssuance.ts b/apps/webapp/app/services/additionalApiKeyIssuance.ts deleted file mode 100644 index 849a06f59af..00000000000 --- a/apps/webapp/app/services/additionalApiKeyIssuance.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { FEATURE_FLAG, type FeatureFlagCatalog } from "~/v3/featureFlags"; - -export function resolveAdditionalApiKeyIssuance( - globalFlags: Partial | Record | undefined, - organizationFlags: Record | undefined -): boolean { - const issuanceEnabled = globalFlags?.[FEATURE_FLAG.additionalApiKeyIssuanceEnabled]; - if (issuanceEnabled !== undefined && issuanceEnabled !== true) { - return false; - } - - const organizationOverride = organizationFlags?.[FEATURE_FLAG.additionalApiKeysEnabled]; - if (organizationOverride === true || organizationOverride === false) { - return organizationOverride; - } - - const additionalApiKeysEnabled = globalFlags?.[FEATURE_FLAG.additionalApiKeysEnabled]; - return additionalApiKeysEnabled === undefined || additionalApiKeysEnabled === true; -} diff --git a/apps/webapp/app/services/authFeatureControls.server.ts b/apps/webapp/app/services/authFeatureControls.server.ts deleted file mode 100644 index 3e562d608e5..00000000000 --- a/apps/webapp/app/services/authFeatureControls.server.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { resolveAuthFeatureControls } from "~/services/authFeatureControls"; -import { globalFlagsRegistry } from "~/v3/globalFlagsRegistry.server"; - -function currentControls() { - return resolveAuthFeatureControls(globalFlagsRegistry.current()); -} - -export const authFeatureControls = { - additionalApiKeyLookupEnabled: () => currentControls().additionalApiKeyLookupEnabled, -}; diff --git a/apps/webapp/app/services/authFeatureControls.ts b/apps/webapp/app/services/authFeatureControls.ts deleted file mode 100644 index 932f9c34bee..00000000000 --- a/apps/webapp/app/services/authFeatureControls.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { FEATURE_FLAG, type FeatureFlagCatalog } from "~/v3/featureFlags"; - -export type AuthFeatureControls = { - additionalApiKeyLookupEnabled: boolean; -}; - -export function resolveAuthFeatureControls( - flags: Partial | Record | undefined -): AuthFeatureControls { - const additionalApiKeyLookupEnabled = flags?.[FEATURE_FLAG.additionalApiKeyLookupEnabled]; - - return { - additionalApiKeyLookupEnabled: - additionalApiKeyLookupEnabled === undefined || additionalApiKeyLookupEnabled === true, - }; -} diff --git a/apps/webapp/app/services/authTelemetry.server.ts b/apps/webapp/app/services/authTelemetry.server.ts index b19fa6447c2..2b9ddb55f92 100644 --- a/apps/webapp/app/services/authTelemetry.server.ts +++ b/apps/webapp/app/services/authTelemetry.server.ts @@ -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"); @@ -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 }; }); @@ -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; @@ -102,27 +84,22 @@ export async function observeLegacyBearerAuthentication { 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, }); } } diff --git a/apps/webapp/app/services/rbac.server.ts b/apps/webapp/app/services/rbac.server.ts index fc1cfd58cf1..11510c1358f 100644 --- a/apps/webapp/app/services/rbac.server.ts +++ b/apps/webapp/app/services/rbac.server.ts @@ -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 @@ -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, diff --git a/apps/webapp/app/v3/featureFlags.ts b/apps/webapp/app/v3/featureFlags.ts index fa1b09a0fb8..cce2bb4ba9a 100644 --- a/apps/webapp/app/v3/featureFlags.ts +++ b/apps/webapp/app/v3/featureFlags.ts @@ -44,12 +44,6 @@ export const FEATURE_FLAG = { deployBuildPathPreview: "deployBuildPathPreview", deployBuildPathStaging: "deployBuildPathStaging", deployBuildPathProduction: "deployBuildPathProduction", - // Per-organization control for creating additional environment API keys. Defaults on. - additionalApiKeysEnabled: "additionalApiKeysEnabled", - // System-wide kill switch for issuing additional environment API keys. Defaults on. - additionalApiKeyIssuanceEnabled: "additionalApiKeyIssuanceEnabled", - // System-wide kill switch for additional (scoped) environment API-key lookup. Defaults on. - additionalApiKeyLookupEnabled: "additionalApiKeyLookupEnabled", scheduleDefaultWindowEnabled: "scheduleDefaultWindowEnabled", freeScheduleMinimumWindowEnabled: "freeScheduleMinimumWindowEnabled", } as const; @@ -165,10 +159,6 @@ export const FeatureFlagCatalog = { [FEATURE_FLAG.deployBuildPathPreview]: DeployBuildPath, [FEATURE_FLAG.deployBuildPathStaging]: DeployBuildPath, [FEATURE_FLAG.deployBuildPathProduction]: DeployBuildPath, - // Strict booleans prevent stringified values from silently changing API-key behavior. - [FEATURE_FLAG.additionalApiKeysEnabled]: z.boolean(), - [FEATURE_FLAG.additionalApiKeyIssuanceEnabled]: z.boolean(), - [FEATURE_FLAG.additionalApiKeyLookupEnabled]: z.boolean(), [FEATURE_FLAG.scheduleDefaultWindowEnabled]: z.boolean(), [FEATURE_FLAG.freeScheduleMinimumWindowEnabled]: z.boolean(), }; @@ -198,9 +188,6 @@ export const ORG_LOCKED_FLAGS: FeatureFlagKey[] = [ FEATURE_FLAG.taskEventRepository, FEATURE_FLAG.runOpsMintKindPrev, FEATURE_FLAG.runOpsMintKindFlippedAt, - // System-wide only — orgs must not be able to override these kill switches. - FEATURE_FLAG.additionalApiKeyIssuanceEnabled, - FEATURE_FLAG.additionalApiKeyLookupEnabled, // The active mint-shard list is deployment-wide; only the pins are per-org. FEATURE_FLAG.runOpsMintShardSet, FEATURE_FLAG.runOpsMintShardSetPrev, diff --git a/apps/webapp/test/additionalApiKeyIssuance.test.ts b/apps/webapp/test/additionalApiKeyIssuance.test.ts deleted file mode 100644 index 8c9fa64f931..00000000000 --- a/apps/webapp/test/additionalApiKeyIssuance.test.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { resolveAdditionalApiKeyIssuance } from "~/services/additionalApiKeyIssuance"; -import { FEATURE_FLAG, FeatureFlagCatalog, ORG_LOCKED_FLAGS } from "~/v3/featureFlags"; - -describe("additional API key issuance controls", () => { - it("registers strict rollout and system-wide flags", () => { - expect( - FeatureFlagCatalog[FEATURE_FLAG.additionalApiKeysEnabled].safeParse("false").success - ).toBe(false); - expect( - FeatureFlagCatalog[FEATURE_FLAG.additionalApiKeyIssuanceEnabled].safeParse("false").success - ).toBe(false); - expect(ORG_LOCKED_FLAGS).not.toContain(FEATURE_FLAG.additionalApiKeysEnabled); - expect(ORG_LOCKED_FLAGS).toContain(FEATURE_FLAG.additionalApiKeyIssuanceEnabled); - }); - - it("defaults to enabled", () => { - expect(resolveAdditionalApiKeyIssuance(undefined, undefined)).toBe(true); - }); - - it("requires the system-wide issuance gate", () => { - expect( - resolveAdditionalApiKeyIssuance( - { [FEATURE_FLAG.additionalApiKeyIssuanceEnabled]: false }, - { [FEATURE_FLAG.additionalApiKeysEnabled]: true } - ) - ).toBe(false); - }); - - it("allows the global rollout flag to disable issuance", () => { - expect( - resolveAdditionalApiKeyIssuance({ [FEATURE_FLAG.additionalApiKeysEnabled]: false }, undefined) - ).toBe(false); - }); - - it("allows an organization override when issuance is enabled", () => { - expect( - resolveAdditionalApiKeyIssuance( - { [FEATURE_FLAG.additionalApiKeyIssuanceEnabled]: true }, - { [FEATURE_FLAG.additionalApiKeysEnabled]: true } - ) - ).toBe(true); - }); - - it("uses the global rollout value when the organization has no override", () => { - expect( - resolveAdditionalApiKeyIssuance( - { - [FEATURE_FLAG.additionalApiKeysEnabled]: true, - [FEATURE_FLAG.additionalApiKeyIssuanceEnabled]: true, - }, - undefined - ) - ).toBe(true); - }); - - it("allows an organization to opt out of a global rollout", () => { - expect( - resolveAdditionalApiKeyIssuance( - { - [FEATURE_FLAG.additionalApiKeysEnabled]: true, - [FEATURE_FLAG.additionalApiKeyIssuanceEnabled]: true, - }, - { [FEATURE_FLAG.additionalApiKeysEnabled]: false } - ) - ).toBe(false); - }); -}); diff --git a/apps/webapp/test/authFeatureControls.test.ts b/apps/webapp/test/authFeatureControls.test.ts deleted file mode 100644 index 71cc584c5cc..00000000000 --- a/apps/webapp/test/authFeatureControls.test.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { resolveAuthFeatureControls } from "~/services/authFeatureControls"; -import { FEATURE_FLAG, FeatureFlagCatalog, ORG_LOCKED_FLAGS } from "~/v3/featureFlags"; - -describe("auth feature controls", () => { - it("defaults lookup to enabled for a cold or missing snapshot", () => { - expect(resolveAuthFeatureControls(undefined)).toEqual({ - additionalApiKeyLookupEnabled: true, - }); - }); - - it("allows the global flag to disable lookup", () => { - expect( - resolveAuthFeatureControls({ [FEATURE_FLAG.additionalApiKeyLookupEnabled]: false }) - ).toEqual({ additionalApiKeyLookupEnabled: false }); - }); - - it("accepts only strict booleans and locks org overrides", () => { - const flag = FEATURE_FLAG.additionalApiKeyLookupEnabled; - expect(FeatureFlagCatalog[flag].safeParse(true).success).toBe(true); - // Strict z.boolean(): the stringified "false" must not coerce to true. - expect(FeatureFlagCatalog[flag].safeParse("false").success).toBe(false); - expect(ORG_LOCKED_FLAGS).toContain(flag); - }); -}); diff --git a/apps/webapp/test/createEnvironmentApiKey.test.ts b/apps/webapp/test/createEnvironmentApiKey.test.ts index 3ebdc66b382..1ab1290408c 100644 --- a/apps/webapp/test/createEnvironmentApiKey.test.ts +++ b/apps/webapp/test/createEnvironmentApiKey.test.ts @@ -5,7 +5,6 @@ import { expect, vi } from "vitest"; import { MAX_API_KEY_TASK_IDENTIFIERS } from "~/consts"; import { createEnvironmentApiKey, revokeEnvironmentApiKey } from "~/models/api-key.server"; import type { ApiKeyTelemetry } from "~/services/apiKeyTelemetry.server"; -import { FEATURE_FLAG } from "~/v3/featureFlags"; import { createRuntimeEnvironment, createTestOrgProjectWithMember, @@ -29,59 +28,15 @@ function telemetryRecorder(): ApiKeyTelemetry { async function setup(prisma: PrismaClient) { const { organization, project, user } = await createTestOrgProjectWithMember(prisma); - const [environment] = await Promise.all([ - createRuntimeEnvironment(prisma, { - projectId: project.id, - organizationId: organization.id, - type: "PRODUCTION", - slug: uniqueId("prod"), - }), - prisma.organization.update({ - where: { id: organization.id }, - data: { featureFlags: { [FEATURE_FLAG.additionalApiKeysEnabled]: true } }, - }), - prisma.featureFlag.upsert({ - where: { key: FEATURE_FLAG.additionalApiKeyIssuanceEnabled }, - create: { key: FEATURE_FLAG.additionalApiKeyIssuanceEnabled, value: true }, - update: { value: true }, - }), - ]); + const environment = await createRuntimeEnvironment(prisma, { + projectId: project.id, + organizationId: organization.id, + type: "PRODUCTION", + slug: uniqueId("prod"), + }); return { organization, project, user, environment }; } -containerTest( - "rejects creation when the system-wide issuance gate is disabled", - async ({ prisma }) => { - const { user, environment } = await setup(prisma); - await prisma.featureFlag.update({ - where: { key: FEATURE_FLAG.additionalApiKeyIssuanceEnabled }, - data: { value: false }, - }); - const controller = policyController(async () => ({ - ok: true, - policy: { presetId: null, scopes: ["admin"] }, - })); - - await expect( - createEnvironmentApiKey( - { - environmentId: environment.id, - taskEnvironmentId: environment.id, - userId: user.id, - name: "Disabled", - presetId: "FULL_ACCESS", - }, - { prismaClient: prisma, rbacController: controller } - ) - ).rejects.toThrow("Creating additional API keys is not enabled"); - - expect(controller.prepareApiKeyPolicy).not.toHaveBeenCalled(); - await expect( - prisma.apiKey.count({ where: { runtimeEnvironmentId: environment.id } }) - ).resolves.toBe(0); - } -); - containerTest("standalone fallback creates one explicit full-access key", async ({ prisma }) => { const { user, environment } = await setup(prisma); const fallback = rbacPlugin.create({ primary: prisma, replica: prisma }, { forceFallback: true }); diff --git a/apps/webapp/test/rbacFallbackBranch.test.ts b/apps/webapp/test/rbacFallbackBranch.test.ts index 95c3b161da8..6f9a277736c 100644 --- a/apps/webapp/test/rbacFallbackBranch.test.ts +++ b/apps/webapp/test/rbacFallbackBranch.test.ts @@ -14,11 +14,8 @@ vi.setConfig({ testTimeout: 60_000 }); // mirrors findEnvironmentByApiKey, but is a separate implementation, so it // needs its own coverage. forceFallback skips loading the closed-source plugin // and uses the in-repo fallback directly. -function makeController(prisma: PrismaClient, additionalApiKeyLookupEnabled?: () => boolean) { - return plugin.create( - { primary: prisma, replica: prisma }, - { forceFallback: true, additionalApiKeyLookupEnabled } - ); +function makeController(prisma: PrismaClient) { + return plugin.create({ primary: prisma, replica: prisma }, { forceFallback: true }); } function bearerRequest(apiKey: string, branch?: string) { @@ -171,30 +168,6 @@ describe("RBAC fallback — DEVELOPMENT branch pivot", () => { }); describe("RBAC fallback — additional keys", () => { - it("rejects a disabled additional-key lookup without querying", async () => { - const runtimeEnvironmentFind = vi.fn(); - const revokedApiKeyFind = vi.fn(); - const apiKeyFind = vi.fn(); - const prisma = { - runtimeEnvironment: { findFirst: runtimeEnvironmentFind }, - revokedApiKey: { findFirst: revokedApiKeyFind }, - apiKey: { findFirst: apiKeyFind }, - } as unknown as PrismaClient; - const rbac = makeController(prisma, () => false); - const key = "tr_prod_sk_0123456789abcdefghijklmn"; - - await expect(rbac.authenticateBearer(bearerRequest(key))).resolves.toMatchObject({ - ok: false, - resolution: { - credentialKind: "additional_api_key", - lookupPath: "additional_skipped", - }, - }); - expect(runtimeEnvironmentFind).not.toHaveBeenCalled(); - expect(revokedApiKeyFind).not.toHaveBeenCalled(); - expect(apiKeyFind).not.toHaveBeenCalled(); - }); - postgresTest("rejects revoked and expired additional keys", async ({ prisma }) => { const { organization, project, orgMember, user } = await createTestOrgProjectWithMember(prisma); const rbac = makeController(prisma); diff --git a/internal-packages/rbac/src/bearerCredentials.ts b/internal-packages/rbac/src/bearerCredentials.ts index d1e4685c8ec..ff92d6a693b 100644 --- a/internal-packages/rbac/src/bearerCredentials.ts +++ b/internal-packages/rbac/src/bearerCredentials.ts @@ -29,7 +29,6 @@ export type BearerLookupPath = | "root_current" | "root_rotated" | "additional" - | "additional_skipped" | "jwt_current" | "jwt_rotated" | "legacy_public" @@ -71,10 +70,7 @@ export class BearerCredentialResolver { private readonly prisma: PrismaClient; private readonly replica: PrismaClient; - constructor( - clients: BearerCredentialClients, - private readonly additionalApiKeyLookupEnabled: () => boolean = () => true - ) { + constructor(clients: BearerCredentialClients) { this.prisma = clients.primary; this.replica = clients.replica; } @@ -210,18 +206,6 @@ export class BearerCredentialResolver { const branchName = sanitizeBranchName(request.headers.get("x-trigger-branch")); if (isAdditionalApiKey(rawToken)) { - if (!this.additionalApiKeyLookupEnabled()) { - return { - ok: false, - status: 401, - error: "Invalid API key", - resolution: { - credentialKind: "additional_api_key", - lookupPath: "additional_skipped", - }, - }; - } - return this.resolveAdditionalKey(rawToken, branchName, options?.allowPreviewParent); } diff --git a/internal-packages/rbac/src/fallback.ts b/internal-packages/rbac/src/fallback.ts index 5a7cdddd83a..61c3c24e1b7 100644 --- a/internal-packages/rbac/src/fallback.ts +++ b/internal-packages/rbac/src/fallback.ts @@ -50,7 +50,6 @@ function resolvePrismaClients(input: PrismaInput): FallbackPrismaClients { export type FallbackOptions = { // Platform secret for verifying delegated user-actor tokens (tr_uat_). userActorSecret?: string; - additionalApiKeyLookupEnabled?: () => boolean; }; export class RoleBaseAccessFallback { @@ -79,7 +78,7 @@ class RoleBaseAccessFallbackController implements RoleBaseAccessController { this.prisma = clients.primary; this.replica = clients.replica; this.userActorSecret = options?.userActorSecret; - this.bearer = new BearerCredentialResolver(clients, options?.additionalApiKeyLookupEnabled); + this.bearer = new BearerCredentialResolver(clients); } async isUsingPlugin(): Promise { diff --git a/internal-packages/rbac/src/index.ts b/internal-packages/rbac/src/index.ts index 5ee4bcd761c..cf46c5d0907 100644 --- a/internal-packages/rbac/src/index.ts +++ b/internal-packages/rbac/src/index.ts @@ -80,9 +80,6 @@ export type RbacCreateOptions = { // follows the host's writer/replica topology. The fallback ignores this — // it queries through the Prisma clients passed as `RbacPrismaInput`. database?: RbacDatabaseConfig; - // Synchronous host-owned rollout control. Defaults to enabled for non-webapp - // consumers; the webapp passes its cold-safe global flag reader. - additionalApiKeyLookupEnabled?: () => boolean; }; // Route actions that historically authorised via the legacy checkAuthorization's @@ -146,8 +143,7 @@ class LazyController implements RoleBaseAccessController { constructor(prisma: RbacPrismaInput, options?: RbacCreateOptions) { this._hostCredentialResolver = new BearerCredentialResolver( - "primary" in prisma ? prisma : { primary: prisma, replica: prisma }, - options?.additionalApiKeyLookupEnabled + "primary" in prisma ? prisma : { primary: prisma, replica: prisma } ); this._init = this.load(prisma, options); // load() runs eagerly but the result is awaited lazily on first method @@ -166,7 +162,6 @@ class LazyController implements RoleBaseAccessController { if (options?.forceFallback) { return new RoleBaseAccessFallback(prisma, { userActorSecret: options?.userActorSecret, - additionalApiKeyLookupEnabled: options?.additionalApiKeyLookupEnabled, }).create(); } const moduleName = "@triggerdotdev/plugins/rbac"; @@ -226,7 +221,6 @@ class LazyController implements RoleBaseAccessController { return new RoleBaseAccessFallback(prisma, { userActorSecret: options?.userActorSecret, - additionalApiKeyLookupEnabled: options?.additionalApiKeyLookupEnabled, }).create(); } } diff --git a/test-timings.json b/test-timings.json index 7f45aa17099..9852da212ee 100644 --- a/test-timings.json +++ b/test-timings.json @@ -106,7 +106,6 @@ "apps/webapp/test/GCRARateLimiter.test.ts": 4553, "apps/webapp/test/SpanPresenter.readthrough.test.ts": 7526, "apps/webapp/test/activitySeries.server.test.ts": 4, - "apps/webapp/test/additionalApiKeyIssuance.test.ts": 3, "apps/webapp/test/aiTitleRateLimiter.test.ts": 165, "apps/webapp/test/api-auth.e2e.test.ts": 20090, "apps/webapp/test/api.v1.waitpoints.tokens.complete.crossSeamGuard.test.ts": 93816, @@ -127,7 +126,6 @@ "apps/webapp/test/apiRunResultPresenter.readthrough.test.ts": 7573, "apps/webapp/test/apiWaitpointListPresenter.readroute.test.ts": 3367, "apps/webapp/test/apiWaitpointPresenter.readthrough.test.ts": 10909, - "apps/webapp/test/authFeatureControls.test.ts": 3, "apps/webapp/test/authorizationCodeConsent.test.ts": 9295, "apps/webapp/test/authorizationRateLimitMiddleware.test.ts": 1, "apps/webapp/test/authorizationRateLimitMiddlewareBypass.test.ts": 221,