default deny empty downstream key exposure

This commit is contained in:
cita
2026-03-20 00:22:17 +08:00
parent ee6eeaddeb
commit 5403a946c6
9 changed files with 311 additions and 22 deletions
@@ -136,6 +136,17 @@ describe('downstreamApiKeyService', () => {
expect(await service.isModelAllowedByPolicyOrAllowedRoutes('gemini-2.0-flash', policy)).toBe(false);
});
it('denies all models when both supportedModels and allowedRouteIds are empty', async () => {
const policy = {
supportedModels: [],
allowedRouteIds: [],
siteWeightMultipliers: {},
};
expect(await service.isModelAllowedByPolicyOrAllowedRoutes('gpt-4o-mini', policy)).toBe(false);
expect(await service.isModelAllowedByPolicyOrAllowedRoutes('claude-opus-4-6', policy)).toBe(false);
});
it('authorizes by selected group model pattern only, not arbitrary internal models', async () => {
const virtualModelGroup = await db.insert(schema.tokenRoutes).values({
modelPattern: 'claude-opus-4-6',
@@ -234,7 +234,7 @@ export async function isModelAllowedByPolicyOrAllowedRoutes(model: string, polic
const hasPatternRules = patterns.length > 0;
const hasRouteRules = allowedRouteIds.length > 0;
if (!hasPatternRules && !hasRouteRules) return true;
if (!hasPatternRules && !hasRouteRules) return false;
if (hasPatternRules && patterns.some((pattern) => matchesDownstreamModelPattern(model, pattern))) {
return true;
@@ -9,3 +9,9 @@ export const EMPTY_DOWNSTREAM_ROUTING_POLICY: DownstreamRoutingPolicy = {
allowedRouteIds: [],
siteWeightMultipliers: {},
};
export const GLOBAL_UNRESTRICTED_DOWNSTREAM_ROUTING_POLICY: DownstreamRoutingPolicy = {
supportedModels: ['*'],
allowedRouteIds: [],
siteWeightMultipliers: {},
};
@@ -102,6 +102,46 @@ describe('TokenRouter downstream policy', () => {
expect(blockedPick).toBeNull();
});
it('rejects route selection when both supportedModels and allowedRouteIds are empty', async () => {
const site = await db.insert(schema.sites).values({
name: 'site-deny-all',
url: 'https://deny-all.example.com',
platform: 'new-api',
status: 'active',
}).returning().get();
const account = await db.insert(schema.accounts).values({
siteId: site.id,
username: 'user-deny-all',
accessToken: 'access-deny-all',
apiToken: 'sk-deny-all',
status: 'active',
}).returning().get();
const route = await db.insert(schema.tokenRoutes).values({
modelPattern: 'gpt-4o-mini',
enabled: true,
}).returning().get();
await db.insert(schema.routeChannels).values({
routeId: route.id,
accountId: account.id,
tokenId: null,
priority: 0,
weight: 10,
enabled: true,
}).run();
const router = new TokenRouter();
const deniedPick = await router.selectChannel('gpt-4o-mini', {
allowedRouteIds: [],
supportedModels: [],
siteWeightMultipliers: {},
});
expect(deniedPick).toBeNull();
});
it('applies site weight multipliers to probability explanation', async () => {
const siteHigh = await db.insert(schema.sites).values({
name: 'high-site',
+12 -9
View File
@@ -531,15 +531,18 @@ function channelSupportsRequestedModel(channelSourceModel: string | null | undef
return false;
}
function isModelAllowedByDownstreamPolicy(requestedModel: string, policy: DownstreamRoutingPolicy): boolean {
const supportedPatterns = Array.isArray(policy.supportedModels)
? policy.supportedModels
: [];
const matchedSupportedPattern = supportedPatterns.some((pattern) => matchesModelPattern(requestedModel, pattern));
if (matchedSupportedPattern) return true;
if (policy.allowedRouteIds.length > 0) return true;
return supportedPatterns.length === 0;
}
function isModelAllowedByDownstreamPolicy(requestedModel: string, policy: DownstreamRoutingPolicy): boolean {
const supportedPatterns = Array.isArray(policy.supportedModels)
? policy.supportedModels
: [];
const hasSupportedPatterns = supportedPatterns.length > 0;
const hasAllowedRoutes = policy.allowedRouteIds.length > 0;
if (!hasSupportedPatterns && !hasAllowedRoutes) return false;
const matchedSupportedPattern = supportedPatterns.some((pattern) => matchesModelPattern(requestedModel, pattern));
if (matchedSupportedPattern) return true;
if (hasAllowedRoutes) return true;
return false;
}
function resolveMappedModel(requestedModel: string, modelMapping?: string | null): string {
if (!modelMapping) return requestedModel;