mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-10 22:41:41 +00:00
fix(jwt): cascade-delete JWT key mappings when their virtual key is deleted
LiteLLM_JWTKeyMapping_token_fkey was created ON DELETE RESTRICT, so deleting a virtual key that a JWT mapping pointed at failed with a foreign key violation on every deletion path (/key/delete, Admin UI, alias delete, team and user cascades). Declaring onDelete: Cascade on the relation lets the database clean the mapping up uniformly, so the next JWT call from that identity re-registers against the newly created key. Rebase of #33703 onto current staging. Claude-Session: https://claude.ai/code/session_011Tn3657NkV6ojLqewL64Kb
This commit is contained in:
parent
264fc82dc1
commit
eee239cd9e
5 changed files with 85 additions and 3 deletions
|
|
@ -0,0 +1,15 @@
|
|||
-- DropForeignKey
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'LiteLLM_JWTKeyMapping_token_fkey') THEN
|
||||
ALTER TABLE "LiteLLM_JWTKeyMapping" DROP CONSTRAINT "LiteLLM_JWTKeyMapping_token_fkey";
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- AddForeignKey
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'LiteLLM_JWTKeyMapping_token_fkey') THEN
|
||||
ALTER TABLE "LiteLLM_JWTKeyMapping" ADD CONSTRAINT "LiteLLM_JWTKeyMapping_token_fkey" FOREIGN KEY ("token") REFERENCES "LiteLLM_VerificationToken"("token") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
END IF;
|
||||
END $$;
|
||||
|
|
@ -492,7 +492,7 @@ model LiteLLM_JWTKeyMapping {
|
|||
updated_at DateTime @default(now()) @updatedAt
|
||||
updated_by String?
|
||||
|
||||
litellm_verification_token LiteLLM_VerificationToken @relation(fields: [token], references: [token])
|
||||
litellm_verification_token LiteLLM_VerificationToken @relation(fields: [token], references: [token], onDelete: Cascade)
|
||||
|
||||
@@unique([jwt_claim_name, jwt_claim_value])
|
||||
@@index([jwt_claim_name, jwt_claim_value, is_active])
|
||||
|
|
|
|||
|
|
@ -492,7 +492,7 @@ model LiteLLM_JWTKeyMapping {
|
|||
updated_at DateTime @default(now()) @updatedAt
|
||||
updated_by String?
|
||||
|
||||
litellm_verification_token LiteLLM_VerificationToken @relation(fields: [token], references: [token])
|
||||
litellm_verification_token LiteLLM_VerificationToken @relation(fields: [token], references: [token], onDelete: Cascade)
|
||||
|
||||
@@unique([jwt_claim_name, jwt_claim_value])
|
||||
@@index([jwt_claim_name, jwt_claim_value, is_active])
|
||||
|
|
|
|||
|
|
@ -492,7 +492,7 @@ model LiteLLM_JWTKeyMapping {
|
|||
updated_at DateTime @default(now()) @updatedAt
|
||||
updated_by String?
|
||||
|
||||
litellm_verification_token LiteLLM_VerificationToken @relation(fields: [token], references: [token])
|
||||
litellm_verification_token LiteLLM_VerificationToken @relation(fields: [token], references: [token], onDelete: Cascade)
|
||||
|
||||
@@unique([jwt_claim_name, jwt_claim_value])
|
||||
@@index([jwt_claim_name, jwt_claim_value, is_active])
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import glob
|
|||
import os
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
|
|
@ -870,3 +871,69 @@ class TestMigrateDeployAttemptAccounting:
|
|||
harness.run()
|
||||
assert len(harness.deploy_calls) == 1
|
||||
assert harness.resolved == []
|
||||
|
||||
|
||||
class TestJWTKeyMappingCascade:
|
||||
"""Regression tests for issue #33702.
|
||||
|
||||
A virtual key referenced by a LiteLLM_JWTKeyMapping row could not be deleted
|
||||
because LiteLLM_JWTKeyMapping_token_fkey was created ON DELETE RESTRICT, so
|
||||
deleting the key (Admin UI, /key/delete, team delete, ...) raised a foreign
|
||||
key violation. The mapping must be removed automatically when its key is
|
||||
deleted, which the FK now enforces via ON DELETE CASCADE.
|
||||
"""
|
||||
|
||||
_FK_NAME = "LiteLLM_JWTKeyMapping_token_fkey"
|
||||
|
||||
def _effective_on_delete(self):
|
||||
"""Replay every migration in order and return the last ON DELETE action
|
||||
declared for the JWT key mapping FK."""
|
||||
action = None
|
||||
for _migration_name, sql in _get_all_migrations():
|
||||
for match in re.finditer(
|
||||
rf'ADD\s+CONSTRAINT\s+"{re.escape(self._FK_NAME)}".*?'
|
||||
r"ON\s+DELETE\s+(CASCADE|RESTRICT|SET\s+NULL|NO\s+ACTION|SET\s+DEFAULT)",
|
||||
sql,
|
||||
re.IGNORECASE | re.DOTALL,
|
||||
):
|
||||
action = re.sub(r"\s+", " ", match.group(1).upper())
|
||||
return action
|
||||
|
||||
def test_fk_effective_on_delete_is_cascade(self):
|
||||
"""The final FK definition across all migrations must cascade deletes."""
|
||||
assert self._effective_on_delete() == "CASCADE", (
|
||||
f"{self._FK_NAME} must end up ON DELETE CASCADE so deleting a "
|
||||
"virtual key removes its JWT key mapping (issue #33702)"
|
||||
)
|
||||
|
||||
def test_schema_declares_cascade_on_relation(self):
|
||||
"""schema.prisma must declare onDelete: Cascade on the mapping relation
|
||||
so the generated client and DB agree."""
|
||||
schema_paths = glob.glob(
|
||||
os.path.abspath(
|
||||
os.path.join(
|
||||
os.path.dirname(__file__), "../../**/schema.prisma"
|
||||
)
|
||||
),
|
||||
recursive=True,
|
||||
)
|
||||
declaring = tuple(
|
||||
(path, schema)
|
||||
for path, schema in ((p, Path(p).read_text()) for p in schema_paths)
|
||||
if "model LiteLLM_JWTKeyMapping" in schema
|
||||
)
|
||||
assert declaring, "No schema.prisma declaring LiteLLM_JWTKeyMapping found"
|
||||
for path, schema in declaring:
|
||||
match = re.search(
|
||||
r"litellm_verification_token\s+LiteLLM_VerificationToken\s+@relation\(([^)]*)\)",
|
||||
schema,
|
||||
)
|
||||
assert match is not None, (
|
||||
f"{path} declares LiteLLM_JWTKeyMapping but its verification token "
|
||||
"relation could not be parsed, so this test cannot vouch for it "
|
||||
"(issue #33702)"
|
||||
)
|
||||
assert "onDelete: Cascade" in match.group(1), (
|
||||
f"{path} must declare onDelete: Cascade on the JWT key mapping "
|
||||
"relation (issue #33702)"
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue