Skip to content

fix(api): accept blockedModels in the key permissions schema - #13666

Merged
diegosouzapw merged 2 commits into
diegosouzapw:release/v3.8.51from
fouadSalkini:fix/api-key-blocked-models-schema
Sep 17, 2026
Merged

diegosouzapw merged 2 commits into
diegosouzapw:release/v3.8.51from
fouadSalkini:fix/api-key-blocked-models-schema

Conversation

@fouadSalkini

Copy link
Copy Markdown
Contributor

Problem

blockedModels is plumbed end-to-end but unreachable through the API.

PATCH /api/keys/[id] destructures blockedModels from validation.data, forwards it into the payload, and updateApiKeyPermissions() writes it to the blocked_models column:

  • src/app/api/keys/[id]/route.ts:72 — destructured
  • src/app/api/keys/[id]/route.ts:101 — if (blockedModels !== undefined) payload.blockedModels = blockedModels;
  • src/lib/db/apiKeys.ts:846-849 — updates.push("blocked_models = @blockedModels")

Every link exists except the first: updateKeyPermissionsSchema never declared the field, so Zod strips it from the parsed body and the destructured value is always undefined. The request answers 200 and writes nothing.

Impact

The API Manager permissions modal sends blockedModels on every save:

  • src/app/(dashboard)/dashboard/api-manager/ApiManagerPageClient.tsx:875 — blockedModels: validBlockedModels in the PATCH body

So the Claude-Code family-blocking control (getBlockedClaudeCodeFamilies / CLAUDE_CODE_FAMILY_BLOCK_PATTERNS, same file around :196) silently does nothing — the operator toggles families, saves, gets a success, and the column is never written. An existing deny-list also cannot be cleared through the UI.

blockedModels is the deny-list half of the model policy: isModelAllowedForKey() checks it before the allow-list and it wins over it (src/lib/db/apiKeys.ts:1516), which is what lets an operator keep a broad scope like cc/* while excluding specific families. With the field unsettable, that half of the policy could only be written by editing the database by hand.

Fix

Two lines in src/shared/validation/schemas/keys.ts:

  1. Declare blockedModels in updateKeyPermissionsSchema, mirroring allowedModels exactly (trimmed, non-empty entries, max 1000).
  2. Count it in the "No valid fields to update" superRefine guard, so a body carrying only blockedModels is a valid update instead of being rejected as empty.

Deliberately not added to createKeySchema: the create route (src/app/api/keys/route.ts) does not read blockedModels, so declaring it there would be dead weight.

Tests

New: tests/unit/api-keys-blocked-models-schema.test.ts — 6 rules.

R1 schema preserves blockedModels verbatim
R2 blockedModels alone is a valid update
R3 coexists with the allow-list and with modelAccessMode: "all"
R4 entry validation matches allowedModels (trimmed / non-empty / max 1000 / must be an array)
R5 an empty array survives as [] so a deny-list can be cleared
R6 the update route still forwards the field it destructures

Before the fix: 5 failed, 1 passed (R6 passes — it guards the already-correct route wiring).
After the fix: 6 passed.

node --import tsx/esm --test tests/unit/api-keys-blocked-models-schema.test.ts
# before → ℹ pass 1 / ℹ fail 5
# after  → ℹ pass 6 / ℹ fail 0

Neighbouring key-permission suites re-run, no regressions — 36/36:

node --import tsx/esm --test \
  tests/unit/api-keys-allowed-combos-preserve-12267.test.ts \
  tests/unit/t08-allowed-connections.test.ts \
  tests/unit/t07-no-log-key-config.test.ts \
  tests/unit/api-key-compression-enabled-2101.test.ts \
  tests/unit/api-manager-provider-permissions.test.ts
# ℹ tests 36 / ℹ pass 36 / ℹ fail 0

npm run typecheck:core clean. Prettier clean. The 11 no-unused-vars ESLint errors on schemas/keys.ts lines 3-15 are pre-existing (import block byte-identical to the base ref; the file is already in config/quality/eslint-suppressions.json).

Live validation

Reproduced on a production gateway before the fix: PATCH /api/keys/{id} with { modelAccessMode: "all", allowedModels: [], blockedModels: [...] } returned 200 and echoed modelAccessMode/allowedModels back, while a direct read of the blocked_models column showed it still NULL.

⚠️ base-red inherited: #12732

`PATCH /api/keys/[id]` already destructures `blockedModels`, forwards it
into the update payload, and `updateApiKeyPermissions()` writes it to the
`blocked_models` column. Only the first link was missing:
`updateKeyPermissionsSchema` never declared the field, so Zod stripped it
from the parsed body and the destructured value was always `undefined`.
The request answered 200 and wrote nothing.

The API Manager permissions modal sends `blockedModels` on every save
(ApiManagerPageClient.tsx), so the Claude-Code family-blocking control
silently did nothing and an existing deny-list could not be cleared.
`blockedModels` is the deny-list half of the model policy — read by
`isModelAllowedForKey()` before the allow-list and winning over it — so
that half was only reachable by editing the database by hand.

Declare the field mirroring `allowedModels` (trimmed, non-empty, max
1000) and count it in the "No valid fields to update" guard so a body
carrying only `blockedModels` is a valid update.

Left out of `createKeySchema` deliberately: the create route does not
read `blockedModels`, so declaring it there would be dead weight.
@diegosouzapw

Copy link
Copy Markdown
Owner

Thanks for this — nice catch and a clean, minimal fix. Confirmed on the current release tip
that blockedModels really is stripped by updateKeyPermissionsSchema before it reaches the
route, so the deny-list half of the model policy was silently a no-op through the API. Ran your
new test file on this branch: 6/6 passing. Only thing missing before merge is a changelog
fragment under changelog.d/fixes/ — could you add one? No other changes needed.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
@fouadSalkini

Copy link
Copy Markdown
Contributor Author

Added the changelog fragment in commit 73a8ce0ea under changelog.d/fixes/api-key-blocked-models-schema.md. All 6 tests passing.

@diegosouzapw
diegosouzapw merged commit ec60915 into diegosouzapw:release/v3.8.51 Sep 17, 2026
3 checks passed
muhamadgalihsaputra pushed a commit to niyatna/NiyatnaRoute that referenced this pull request Sep 27, 2026
…uzapw#13666)

* fix(api): accept blockedModels in the key permissions schema

`PATCH /api/keys/[id]` already destructures `blockedModels`, forwards it
into the update payload, and `updateApiKeyPermissions()` writes it to the
`blocked_models` column. Only the first link was missing:
`updateKeyPermissionsSchema` never declared the field, so Zod stripped it
from the parsed body and the destructured value was always `undefined`.
The request answered 200 and wrote nothing.

The API Manager permissions modal sends `blockedModels` on every save
(ApiManagerPageClient.tsx), so the Claude-Code family-blocking control
silently did nothing and an existing deny-list could not be cleared.
`blockedModels` is the deny-list half of the model policy — read by
`isModelAllowedForKey()` before the allow-list and winning over it — so
that half was only reachable by editing the database by hand.

Declare the field mirroring `allowedModels` (trimmed, non-empty, max
1000) and count it in the "No valid fields to update" guard so a body
carrying only `blockedModels` is a valid update.

Left out of `createKeySchema` deliberately: the create route does not
read `blockedModels`, so declaring it there would be dead weight.

* docs(changelog): add fragment for blockedModels key schema fix

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants