forked from boos/BoosAPI
default deny empty downstream key exposure
This commit is contained in:
@@ -1,10 +1,17 @@
|
||||
import type { FastifyReply, FastifyRequest } from 'fastify';
|
||||
import { getProxyAuthContext } from '../../middleware/auth.js';
|
||||
import { isModelAllowedByPolicyOrAllowedRoutes, recordManagedKeyCostUsage } from '../../services/downstreamApiKeyService.js';
|
||||
import { EMPTY_DOWNSTREAM_ROUTING_POLICY, type DownstreamRoutingPolicy } from '../../services/downstreamPolicyTypes.js';
|
||||
import {
|
||||
EMPTY_DOWNSTREAM_ROUTING_POLICY,
|
||||
GLOBAL_UNRESTRICTED_DOWNSTREAM_ROUTING_POLICY,
|
||||
type DownstreamRoutingPolicy,
|
||||
} from '../../services/downstreamPolicyTypes.js';
|
||||
|
||||
export function getDownstreamRoutingPolicy(request: FastifyRequest): DownstreamRoutingPolicy {
|
||||
return getProxyAuthContext(request)?.policy || EMPTY_DOWNSTREAM_ROUTING_POLICY;
|
||||
const authContext = getProxyAuthContext(request);
|
||||
if (!authContext) return EMPTY_DOWNSTREAM_ROUTING_POLICY;
|
||||
if (authContext.keyId === null) return GLOBAL_UNRESTRICTED_DOWNSTREAM_ROUTING_POLICY;
|
||||
return authContext.policy;
|
||||
}
|
||||
|
||||
export async function ensureModelAllowedForDownstreamKey(
|
||||
@@ -14,6 +21,7 @@ export async function ensureModelAllowedForDownstreamKey(
|
||||
): Promise<boolean> {
|
||||
const authContext = getProxyAuthContext(request);
|
||||
if (!authContext) return true;
|
||||
if (authContext.keyId === null) return true;
|
||||
|
||||
if (await isModelAllowedByPolicyOrAllowedRoutes(requestedModel, authContext.policy)) {
|
||||
return true;
|
||||
|
||||
@@ -8,6 +8,7 @@ type DbModule = typeof import('../../db/index.js');
|
||||
type ProxyRouterModule = typeof import('./router.js');
|
||||
type TokenRouterModule = typeof import('../../services/tokenRouter.js');
|
||||
type TokensRoutesModule = typeof import('../api/tokens.js');
|
||||
type ConfigModule = typeof import('../../config.js');
|
||||
|
||||
describe('/v1/models route', () => {
|
||||
let db: DbModule['db'];
|
||||
@@ -15,6 +16,7 @@ describe('/v1/models route', () => {
|
||||
let proxyRoutes: ProxyRouterModule['proxyRoutes'];
|
||||
let tokensRoutes: TokensRoutesModule['tokensRoutes'];
|
||||
let invalidateTokenRouterCache: TokenRouterModule['invalidateTokenRouterCache'];
|
||||
let config: ConfigModule['config'];
|
||||
let app: FastifyInstance;
|
||||
let dataDir = '';
|
||||
|
||||
@@ -27,12 +29,15 @@ describe('/v1/models route', () => {
|
||||
const proxyRouterModule = await import('./router.js');
|
||||
const tokenRouterModule = await import('../../services/tokenRouter.js');
|
||||
const tokensRoutesModule = await import('../api/tokens.js');
|
||||
const configModule = await import('../../config.js');
|
||||
|
||||
db = dbModule.db;
|
||||
schema = dbModule.schema;
|
||||
proxyRoutes = proxyRouterModule.proxyRoutes;
|
||||
tokensRoutes = tokensRoutesModule.tokensRoutes;
|
||||
invalidateTokenRouterCache = tokenRouterModule.invalidateTokenRouterCache;
|
||||
config = configModule.config;
|
||||
config.proxyToken = 'sk-global-proxy-token';
|
||||
|
||||
app = Fastify();
|
||||
await app.register(tokensRoutes);
|
||||
@@ -108,6 +113,7 @@ describe('/v1/models route', () => {
|
||||
name: 'managed-key',
|
||||
key: 'sk-managed-models',
|
||||
enabled: true,
|
||||
supportedModels: JSON.stringify(['routable-model']),
|
||||
}).run();
|
||||
|
||||
const response = await app.inject({
|
||||
@@ -129,6 +135,64 @@ describe('/v1/models route', () => {
|
||||
expect(ids).not.toContain('orphan-model');
|
||||
});
|
||||
|
||||
it('keeps global proxy token unrestricted when no managed key matches', async () => {
|
||||
const site = await db.insert(schema.sites).values({
|
||||
name: 'global-site',
|
||||
url: 'https://global.example.com',
|
||||
platform: 'openai',
|
||||
status: 'active',
|
||||
}).returning().get();
|
||||
|
||||
const account = await db.insert(schema.accounts).values({
|
||||
siteId: site.id,
|
||||
accessToken: 'global-access-token',
|
||||
status: 'active',
|
||||
}).returning().get();
|
||||
|
||||
const token = await db.insert(schema.accountTokens).values({
|
||||
accountId: account.id,
|
||||
name: 'default',
|
||||
token: 'global-api-token',
|
||||
enabled: true,
|
||||
isDefault: true,
|
||||
}).returning().get();
|
||||
|
||||
await db.insert(schema.modelAvailability).values({
|
||||
accountId: account.id,
|
||||
modelName: 'global-routable-model',
|
||||
available: true,
|
||||
}).run();
|
||||
|
||||
const route = await db.insert(schema.tokenRoutes).values({
|
||||
modelPattern: 'global-routable-model',
|
||||
enabled: true,
|
||||
}).returning().get();
|
||||
|
||||
await db.insert(schema.routeChannels).values({
|
||||
routeId: route.id,
|
||||
accountId: account.id,
|
||||
tokenId: token.id,
|
||||
sourceModel: 'global-routable-model',
|
||||
enabled: true,
|
||||
}).run();
|
||||
|
||||
const response = await app.inject({
|
||||
method: 'GET',
|
||||
url: '/v1/models',
|
||||
headers: {
|
||||
authorization: 'Bearer sk-global-proxy-token',
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.statusCode).toBe(200);
|
||||
const body = response.json() as {
|
||||
object: 'list';
|
||||
data: Array<{ id: string }>;
|
||||
};
|
||||
|
||||
expect(body.data.map((item) => item.id)).toContain('global-routable-model');
|
||||
});
|
||||
|
||||
it('returns only whitelist models for managed key with supportedModels policy', async () => {
|
||||
const site = await db.insert(schema.sites).values({
|
||||
name: 'test-site',
|
||||
@@ -290,6 +354,58 @@ describe('/v1/models route', () => {
|
||||
expect(ids).not.toContain('claude-sonnet-4-5');
|
||||
});
|
||||
|
||||
it('returns no models for managed key with empty model and group selections', async () => {
|
||||
const site = await db.insert(schema.sites).values({
|
||||
name: 'deny-all-site',
|
||||
url: 'https://deny-all.example.com',
|
||||
platform: 'openai',
|
||||
status: 'active',
|
||||
}).returning().get();
|
||||
|
||||
const account = await db.insert(schema.accounts).values({
|
||||
siteId: site.id,
|
||||
accessToken: 'deny-all-access-token',
|
||||
status: 'active',
|
||||
}).returning().get();
|
||||
|
||||
await db.insert(schema.modelAvailability).values([
|
||||
{
|
||||
accountId: account.id,
|
||||
modelName: 'gpt-4o-mini',
|
||||
available: true,
|
||||
},
|
||||
{
|
||||
accountId: account.id,
|
||||
modelName: 'claude-opus-4-6',
|
||||
available: true,
|
||||
},
|
||||
]).run();
|
||||
|
||||
await db.insert(schema.downstreamApiKeys).values({
|
||||
name: 'managed-key-deny-all',
|
||||
key: 'sk-managed-deny-all',
|
||||
enabled: true,
|
||||
supportedModels: JSON.stringify([]),
|
||||
allowedRouteIds: JSON.stringify([]),
|
||||
}).run();
|
||||
|
||||
const response = await app.inject({
|
||||
method: 'GET',
|
||||
url: '/v1/models',
|
||||
headers: {
|
||||
authorization: 'Bearer sk-managed-deny-all',
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.statusCode).toBe(200);
|
||||
const body = response.json() as {
|
||||
object: 'list';
|
||||
data: Array<{ id: string }>;
|
||||
};
|
||||
|
||||
expect(body.data).toEqual([]);
|
||||
});
|
||||
|
||||
it('returns only explicit-group public name while hiding source exact routes', async () => {
|
||||
const site = await db.insert(schema.sites).values({
|
||||
name: 'explicit-group-site',
|
||||
@@ -461,6 +577,7 @@ describe('/v1/models route', () => {
|
||||
name: 'search-key',
|
||||
key: 'sk-search-key',
|
||||
enabled: true,
|
||||
supportedModels: JSON.stringify(['gpt-4.1']),
|
||||
}).run();
|
||||
|
||||
const response = await app.inject({
|
||||
|
||||
Reference in New Issue
Block a user