From ccaa0b5f7950642b499480756590379c35ec3a10 Mon Sep 17 00:00:00 2001 From: diegosouzapw Date: Tue, 2 Jun 2026 07:40:08 -0300 Subject: [PATCH] fix(quota): block qtSd models for keys with no quota-pool allocation (Check 2.9) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit E2E testing on the VPS showed a normal key (empty allowedQuotas) could call a qtSd/// 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. --- src/shared/utils/apiKeyPolicy.ts | 27 ++++++++++++++++++++++ tests/unit/apikeypolicy-quota-only.test.ts | 20 ++++++++++++++++ 2 files changed, 47 insertions(+) diff --git a/src/shared/utils/apiKeyPolicy.ts b/src/shared/utils/apiKeyPolicy.ts index 1d674af112..2d14ca89fe 100644 --- a/src/shared/utils/apiKeyPolicy.ts +++ b/src/shared/utils/apiKeyPolicy.ts @@ -321,6 +321,33 @@ export async function enforceApiKeyPolicy( } } + // ── Check 2.9: qtSd models require a quota-pool allocation ── + // + // quotaShared-* (qtSd///) 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 diff --git a/tests/unit/apikeypolicy-quota-only.test.ts b/tests/unit/apikeypolicy-quota-only.test.ts index 07dfd9961f..3c2641cb10 100644 --- a/tests/unit/apikeypolicy-quota-only.test.ts +++ b/tests/unit/apikeypolicy-quota-only.test.ts @@ -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");