[Fix] Skip key_alias re-validation on update/regenerate when alias unchanged

When updating or regenerating a key without changing its key_alias, the
existing alias was being re-validated against current format rules. This
caused keys with legacy aliases (created before stricter validation) to
become uneditable. Now validation only runs when the alias actually changes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
yuneng-jiang
2026-03-16 18:07:03 -07:00
parent c8c4774213
commit 4a92db8da1
2 changed files with 133 additions and 2 deletions
@@ -2120,7 +2120,10 @@ async def update_key_fn(
data=data, existing_key_row=existing_key_row
)
_validate_key_alias_format(key_alias=non_default_values.get("key_alias", None))
# Only validate key_alias format if it's actually being changed
new_key_alias = non_default_values.get("key_alias", None)
if new_key_alias != existing_key_row.key_alias:
_validate_key_alias_format(key_alias=new_key_alias)
await _enforce_unique_key_alias(
key_alias=non_default_values.get("key_alias", None),
@@ -3579,7 +3582,10 @@ async def _execute_virtual_key_regeneration(
non_default_values = await prepare_key_update_data(
data=data, existing_key_row=key_in_db
)
_validate_key_alias_format(key_alias=non_default_values.get("key_alias"))
# Only validate key_alias format if it's actually being changed
new_key_alias = non_default_values.get("key_alias")
if new_key_alias != key_in_db.key_alias:
_validate_key_alias_format(key_alias=new_key_alias)
verbose_proxy_logger.debug("non_default_values: %s", non_default_values)
update_data.update(non_default_values)
update_data = prisma_client.jsonify_object(data=update_data)
@@ -7833,3 +7833,128 @@ class TestLIT1884KeyUpdateValidation:
prisma_client=mock_prisma_client,
user_api_key_cache=MagicMock(),
)
class TestKeyAliasSkipValidationOnUnchanged:
"""
Test that updating/regenerating a key without changing its key_alias
does NOT re-validate the alias. This prevents legacy aliases (created
before stricter validation rules) from blocking edits to other fields.
"""
@pytest.fixture(autouse=True)
def enable_validation(self):
litellm.enable_key_alias_format_validation = True
yield
litellm.enable_key_alias_format_validation = False
@pytest.fixture
def mock_prisma(self):
prisma = MagicMock()
prisma.db = MagicMock()
prisma.db.litellm_verificationtoken = MagicMock()
prisma.get_data = AsyncMock(return_value=None) # no duplicate alias
prisma.update_data = AsyncMock(return_value=None)
prisma.jsonify_object = MagicMock(side_effect=lambda data: data)
return prisma
@pytest.fixture
def existing_key_with_legacy_alias(self):
"""A key whose alias contains '@' — valid now, but simulates a legacy alias."""
return LiteLLM_VerificationToken(
token="hashed_token_123",
key_alias="user@domain.com",
team_id="team-1",
models=[],
max_budget=100.0,
)
@pytest.mark.asyncio
async def test_update_key_unchanged_legacy_alias_passes(
self, mock_prisma, existing_key_with_legacy_alias
):
"""
Updating a key without changing its key_alias should skip format
validation even if the alias wouldn't pass current rules.
"""
from litellm.proxy.management_endpoints.key_management_endpoints import (
_validate_key_alias_format,
)
# Temporarily make the regex reject '@' to simulate stricter rules
import re
from litellm.proxy.management_endpoints import key_management_endpoints as mod
original_pattern = mod._KEY_ALIAS_PATTERN
mod._KEY_ALIAS_PATTERN = re.compile(
r"^[a-zA-Z0-9][a-zA-Z0-9_\-/\.]{0,253}[a-zA-Z0-9]$"
)
try:
# Confirm the alias WOULD fail validation directly
with pytest.raises(ProxyException):
_validate_key_alias_format("user@domain.com")
# But prepare_key_update_data + the skip logic should allow it
# Simulate what update_key_fn does: alias is in non_default_values
# but matches existing_key_row.key_alias => skip validation
existing_alias = existing_key_with_legacy_alias.key_alias
new_alias = "user@domain.com" # same as existing
assert new_alias == existing_alias # unchanged
# This is the core logic from update_key_fn:
if new_alias != existing_alias:
_validate_key_alias_format(new_alias)
# No exception raised — test passes
finally:
mod._KEY_ALIAS_PATTERN = original_pattern
@pytest.mark.asyncio
async def test_update_key_changed_alias_still_validated(
self, mock_prisma, existing_key_with_legacy_alias
):
"""
When the alias IS being changed, validation should still run.
"""
from litellm.proxy.management_endpoints.key_management_endpoints import (
_validate_key_alias_format,
)
existing_alias = existing_key_with_legacy_alias.key_alias
new_alias = "!invalid!"
assert new_alias != existing_alias
with pytest.raises(ProxyException):
if new_alias != existing_alias:
_validate_key_alias_format(new_alias)
@pytest.mark.asyncio
async def test_update_key_changed_to_valid_alias_passes(
self, mock_prisma, existing_key_with_legacy_alias
):
"""
Changing the alias to a new valid value should pass validation.
"""
from litellm.proxy.management_endpoints.key_management_endpoints import (
_validate_key_alias_format,
)
existing_alias = existing_key_with_legacy_alias.key_alias
new_alias = "new-valid-alias"
assert new_alias != existing_alias
# Should not raise
if new_alias != existing_alias:
_validate_key_alias_format(new_alias)
@pytest.mark.asyncio
async def test_update_key_alias_none_skips_validation(self):
"""
When key_alias is not in the update payload (None), validation
should be skipped regardless.
"""
from litellm.proxy.management_endpoints.key_management_endpoints import (
_validate_key_alias_format,
)
# None alias should always pass
_validate_key_alias_format(None)