From 2f6b5b18e83b1efa7421f451bab56ea6b5606a38 Mon Sep 17 00:00:00 2001 From: fewensa <37804932+fewensa@users.noreply.github.com> Date: Sat, 19 Sep 2026 11:04:50 +0800 Subject: [PATCH] fix(auth): enforce blocked models in all-access mode (#13861) --- src/lib/db/apiKeys.ts | 9 ++++ src/shared/utils/apiKeyPolicy.ts | 3 ++ src/sse/handlers/chat/comboTargetKeyPolicy.ts | 9 +++- tests/unit/api-key-policy.test.ts | 31 ++++++++++++ ...restricted-key-target-policy-12886.test.ts | 47 +++++++++++++++++++ 5 files changed, 98 insertions(+), 1 deletion(-) diff --git a/src/lib/db/apiKeys.ts b/src/lib/db/apiKeys.ts index 037f14f2a7..f3d2a814e2 100644 --- a/src/lib/db/apiKeys.ts +++ b/src/lib/db/apiKeys.ts @@ -375,6 +375,15 @@ async function getModelPermissionCandidates(modelId: string): Promise return Array.from(candidates); } +export async function isModelBlockedByPatterns( + blockedModels: string[] | null | undefined, + modelId: string +): Promise { + if (!blockedModels?.length) return false; + const candidates = await getModelPermissionCandidates(modelId); + return blockedModels.some((pattern) => modelPatternMatches(pattern, candidates)); +} + async function getPublishedModelLookupTarget( modelId: string ): Promise<{ providerId: string; modelId: string } | null> { diff --git a/src/shared/utils/apiKeyPolicy.ts b/src/shared/utils/apiKeyPolicy.ts index b100cbcc7f..713c823184 100644 --- a/src/shared/utils/apiKeyPolicy.ts +++ b/src/shared/utils/apiKeyPolicy.ts @@ -75,6 +75,7 @@ export interface ApiKeyMetadata { name?: string; modelAccessMode?: "all" | "restricted"; allowedModels?: string[]; + blockedModels?: string[]; allowedCombos?: string[]; allowedConnections?: string[]; allowedQuotas?: string[]; @@ -346,6 +347,7 @@ async function validateStandardRoutingTarget( const hasModelRestrictions = apiKeyInfo.modelAccessMode === "restricted" || Boolean(apiKeyInfo.allowedModels?.length) || + Boolean(apiKeyInfo.blockedModels?.length) || apiKeyInfo.disableNonPublicModels === true; if (!requestedComboName && hasModelRestrictions && modelStr.startsWith("auto/")) { requestedComboName = modelStr; @@ -587,6 +589,7 @@ async function validateModelAccess(context: PolicyContext): Promise { + const key = await createKeyWithPolicy({ + modelAccessMode: "all", + allowedModels: [], + blockedModels: ["gpt-6*", "*/gpt-6*"], + }); + const policy = await loadPolicy("all-mode-blocked-models"); + + const blocked = await policy.enforceApiKeyPolicy( + makePolicyRequest(key.key), + "mbrouter/gpt-6-codex" + ); + assert.equal(blocked.rejection.status, 403); + + const allowed = await policy.enforceApiKeyPolicy( + makePolicyRequest(key.key), + "mbrouter/gpt-5.6-sol" + ); + assert.equal(allowed.rejection, null); + + const metadata = await apiKeysDb.getApiKeyMetadata(key.key); + assert.ok(metadata); + const rerouted = await policy.validateApiKeyRoutingTarget( + makePolicyRequest(key.key), + key.key, + metadata, + "gpt-6" + ); + assert.equal(rerouted?.status, 403); +}); + test("enforceApiKeyPolicy returns Anthropic error envelope for /v1/messages model denials", async () => { const restrictedKey = await createKeyWithPolicy({ allowedModels: ["cc/*"], diff --git a/tests/unit/combo-restricted-key-target-policy-12886.test.ts b/tests/unit/combo-restricted-key-target-policy-12886.test.ts index 2c6635e05c..ebe288122f 100644 --- a/tests/unit/combo-restricted-key-target-policy-12886.test.ts +++ b/tests/unit/combo-restricted-key-target-policy-12886.test.ts @@ -77,3 +77,50 @@ test("#12886: unrestricted key skips the gate", async () => { assert.equal(ok, true); assert.equal(called, 0); }); + +test("blockedModels still filters combo targets in all-access mode", async () => { + let called = 0; + const ok = await comboTargetPassesKeyModelPolicy({ + apiKey: KEY, + apiKeyInfo: { + modelAccessMode: "all", + allowedModels: [], + blockedModels: ["deepseek/*"], + }, + requestedModelStr: COMBO, + targetModelStr: INNER, + isModelAllowedForKey: async () => { + called += 1; + return false; + }, + }); + assert.equal(ok, false); + assert.equal(called, 0); +}); + +test("blockedModels takes precedence without disabling allowed combo targets", async () => { + const apiKeyInfo = { + modelAccessMode: "restricted", + allowedModels: [COMBO], + blockedModels: ["anthropic/*"], + }; + const checker = allowListChecker([COMBO]); + + const allowed = await comboTargetPassesKeyModelPolicy({ + apiKey: KEY, + apiKeyInfo, + requestedModelStr: COMBO, + targetModelStr: INNER, + isModelAllowedForKey: checker, + }); + const blocked = await comboTargetPassesKeyModelPolicy({ + apiKey: KEY, + apiKeyInfo, + requestedModelStr: COMBO, + targetModelStr: OTHER, + isModelAllowedForKey: checker, + }); + + assert.equal(allowed, true); + assert.equal(blocked, false); +});