mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
fix(quota): block qtSd models for keys with no quota-pool allocation (Check 2.9)
E2E testing on the VPS showed a normal key (empty allowedQuotas) could call a qtSd/<group>/<provider>/<model> virtual model and route through a shared quota pool — because the quota-exclusive enforcement (Check 3) only ran when allowedQuotas was non-empty, so an unallocated key fell through to the normal model checks and qtSd was served. This is the "empty allowedQuotas = all pools" gap from the redesign. Add Check 2.9 in enforceApiKeyPolicy: if the requested model is a qtSd model and the key is NOT allocated to any quota pool (allowedQuotas empty), reject 403 QUOTA_NOT_ALLOCATED. Allocated keys are unchanged (Check 3 still validates scope). This matches the owner's rule: only a key selected in a pool may use its qtSd models. Normal (non-qtSd) model access for normal keys is unchanged. Test: tests/unit/apikeypolicy-quota-only.test.ts — new case asserts a non-quota key is blocked from qtSd (QUOTA_NOT_ALLOCATED) yet still uses normal models.
This commit is contained in:
@@ -321,6 +321,33 @@ export async function enforceApiKeyPolicy(
|
||||
}
|
||||
}
|
||||
|
||||
// ── Check 2.9: qtSd models require a quota-pool allocation ──
|
||||
//
|
||||
// quotaShared-* (qtSd/<group>/<provider>/<model>) virtual models are pool-gated:
|
||||
// a key that is NOT allocated to any quota pool (empty allowedQuotas) must not be
|
||||
// able to call them — otherwise an ordinary key could route through someone
|
||||
// else's shared quota. Only allocated keys (allowedQuotas non-empty, further
|
||||
// validated against their pool scope in Check 3 below) may use qtSd models.
|
||||
if (
|
||||
modelStr &&
|
||||
isQuotaModelName(modelStr) &&
|
||||
!(Array.isArray(apiKeyInfo.allowedQuotas) && apiKeyInfo.allowedQuotas.length > 0)
|
||||
) {
|
||||
const notAllocatedBody = buildErrorBody(
|
||||
HTTP_STATUS.FORBIDDEN,
|
||||
`Model "${modelStr}" requires a quota-pool allocation; this API key is not allocated to any quota pool`
|
||||
);
|
||||
notAllocatedBody.error.code = "QUOTA_NOT_ALLOCATED";
|
||||
return {
|
||||
apiKey,
|
||||
apiKeyInfo,
|
||||
rejection: new Response(JSON.stringify(notAllocatedBody), {
|
||||
status: HTTP_STATUS.FORBIDDEN,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
// ── Check 3: Quota-exclusive enforcement (Phase B4) ──
|
||||
//
|
||||
// When a key has allowedQuotas its access is governed exclusively by the
|
||||
|
||||
@@ -236,6 +236,26 @@ test("key with empty allowedQuotas is subject to normal model restriction checks
|
||||
assert.notEqual(body.error.code, "QUOTA_ONLY", "normal key rejection must NOT use QUOTA_ONLY code");
|
||||
});
|
||||
|
||||
test("non-quota key (empty allowedQuotas) requesting a qtSd model is rejected 403 QUOTA_NOT_ALLOCATED", async () => {
|
||||
// A normal key with NO quota allocation must NOT route through a shared quota pool.
|
||||
const created = await apiKeysDb.createApiKey("Normal Key No Quota", "machine-b4-noalloc");
|
||||
// (no allowedQuotas set → empty array)
|
||||
|
||||
const policy = await loadPolicy("b4-quota-noalloc");
|
||||
const qtSdModel = quotaModelName("AnyGroup", "codex", "gpt-5.5");
|
||||
|
||||
const blocked = await policy.enforceApiKeyPolicy(makeRequest(created.key), qtSdModel);
|
||||
assert.ok(blocked.rejection, "non-quota key must be blocked from qtSd models");
|
||||
assert.equal(blocked.rejection.status, 403);
|
||||
const body = await readBody(blocked.rejection);
|
||||
assert.equal(body.error.code, "QUOTA_NOT_ALLOCATED", "must use QUOTA_NOT_ALLOCATED code");
|
||||
assert.match(body.error.message, /quota-pool allocation/);
|
||||
|
||||
// Sanity: the same key can still use a normal (non-qtSd) model freely.
|
||||
const allowed = await policy.enforceApiKeyPolicy(makeRequest(created.key), "openai/gpt-4.1");
|
||||
assert.equal(allowed.rejection, null, "non-quota key still uses normal models freely");
|
||||
});
|
||||
|
||||
test("quota-only key whose allowedQuotas references a non-existent pool is rejected 403 QUOTA_ONLY (fail-closed)", async () => {
|
||||
// Create an API key bound to a pool ID that does not exist in the DB (dangling reference)
|
||||
const created = await apiKeysDb.createApiKey("Dangling Quota Key", "machine-b4-dangling");
|
||||
|
||||
Reference in New Issue
Block a user