[Fix] Key Alias Re-validation on Update Blocks Legacy Aliases#23798
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
…changed 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>
d4024b1 to
4a92db8
Compare
Greptile SummaryThis PR fixes a breaking change where keys created before Key observations:
Confidence Score: 4/5
|
| Filename | Overview |
|---|---|
| litellm/proxy/management_endpoints/key_management_endpoints.py | Skips _validate_key_alias_format() on both the update and regenerate paths when the submitted key_alias equals the existing value in the DB — correctly fixing the legacy-alias edit-block bug with no logic regressions identified. |
| tests/test_litellm/proxy/management_endpoints/test_key_management_endpoints.py | Adds 4 new tests verifying skip-validation logic, but they replicate the if/else conditional inline rather than calling the actual endpoint functions, leaving a test coverage gap; mock_prisma is declared but never used in 3 of the 4 tests, and 3 tests use @pytest.mark.asyncio without any awaits. |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart TD
A["update_key_fn / _execute_virtual_key_regeneration\ncalled with update payload"] --> B["prepare_key_update_data()\nnon_default_values = payload fields (exclude_unset=True)"]
B --> C{"new_key_alias\n== existing_key_row.key_alias?"}
C -- "Yes (unchanged alias)" --> D["⏭ Skip _validate_key_alias_format()\nLegacy alias preserved"]
C -- "No (alias changed or absent)" --> E["_validate_key_alias_format(new_key_alias)"]
E -- "None → early return\nValid format → pass" --> F["_enforce_unique_key_alias()"]
E -- "Invalid format &\nvalidation enabled" --> G["❌ ProxyException 400"]
D --> F
F --> H["Update DB record"]
Last reviewed commit: 4a92db8
| async def test_update_key_unchanged_legacy_alias_passes( | ||
| self, mock_prisma, existing_key_with_legacy_alias |
There was a problem hiding this comment.
mock_prisma fixture parameter is unused in tests 1–3
The mock_prisma fixture is requested as a parameter in test_update_key_unchanged_legacy_alias_passes, test_update_key_changed_alias_still_validated, and test_update_key_changed_to_valid_alias_passes, but its value is never referenced inside any of those test bodies. Since none of the tests actually call update_key_fn or interact with the DB, the fixture is dead weight here. If the intent is to guard against real DB calls, the fixture should be used; if there is no such guard needed, the parameter can be removed.
| @pytest.mark.asyncio | ||
| async def test_update_key_changed_alias_still_validated( |
There was a problem hiding this comment.
@pytest.mark.asyncio on effectively synchronous tests
test_update_key_changed_alias_still_validated, test_update_key_changed_to_valid_alias_passes, and test_update_key_alias_none_skips_validation are all decorated with @pytest.mark.asyncio but contain no await expressions — they are effectively synchronous. Depending on the version of pytest-asyncio in use, this can produce warnings or unexpected behavior in strict mode. These three tests should either be plain def functions (removing async def and the decorator) or actually exercise async code paths.
| 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) |
There was a problem hiding this comment.
None from missing key vs. explicit None are indistinguishable here
non_default_values.get("key_alias", None) returns None both when the client omits key_alias from the payload (field not present after exclude_unset=True) AND when the client explicitly sends "key_alias": null to clear the alias.
If the existing key has a non-None alias (e.g. "user@domain.com") and the client sends a payload without key_alias at all, this evaluates to None != "user@domain.com" → True, and _validate_key_alias_format(None) is called. _validate_key_alias_format handles None safely (returns early), so there is no runtime error. However, the semantics are slightly misleading: the condition fires as though the alias "changed to None" even when it wasn't touched.
This is a pre-existing ambiguity and not a regression introduced by this PR, but it is worth documenting with a comment so future maintainers understand the intent.
| # 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 |
There was a problem hiding this comment.
Tests replicate logic inline instead of calling the actual endpoints
The new tests in TestKeyAliasSkipValidationOnUnchanged manually copy-paste the if new_alias != existing_alias: _validate_key_alias_format(new_alias) conditional inline rather than calling update_key_fn or _execute_virtual_key_regeneration directly. This means these tests will not catch a regression if someone later removes or misplaces the skip logic inside the actual endpoint functions.
For example, if update_key_fn is refactored and the if new_key_alias != existing_key_row.key_alias: guard is accidentally dropped, all four tests here would still pass while the production bug reappears.
For stronger regression protection, consider calling the actual functions with a mocked prisma_client (as the existing TestLIT1884KeyUpdateValidation class does) rather than re-implementing the conditional inline.
616b311
into
litellm_yj_march_16_2026
Relevant issues
User report: keys created with
@inkey_alias(e.g.user@domain.com) before stricter validation rules cannot be edited in the Admin UI, even when the alias itself isn't being changed. This is to fix a breaking change.Summary
Failure Path (Before Fix)
When updating or regenerating a virtual key, the backend re-validates the existing
key_aliasagainst current format rules — even when the alias hasn't changed. Keys created before@was added to the allowed regex (or before validation existed) become uneditable because the unchanged alias fails the newer validation.Fix
Skip
_validate_key_alias_format()on the update and regenerate paths when the submittedkey_aliasmatches the existing value in the database. Validation still runs when the alias is actually being changed.Testing
TestKeyAliasSkipValidationOnUnchanged:Type
🐛 Bug Fix
✅ Test