fix(policies): reject non-existent team/key/model scope entries on attachment create - #32131
Conversation
…tachment create Creating a policy attachment accepted arbitrary team, key, and model values with no validation, so a typo'd or non-existent team was silently persisted (LIT-4199). The create endpoint now rejects a concrete (non-wildcard) team, key, or model that does not resolve to a real entity, wiring the previously-dead PolicyValidator existence checks and reusing RouteChecks._is_wildcard_pattern so validation agrees with request-time matching, where only a trailing "*" is a wildcard. Wildcard patterns are still allowed through since they may match zero entities today and more later, and tags stay free-form. The Admin UI's Teams field validates the same rule for immediate feedback when its team list has loaded, deferring to the backend otherwise.
Greptile SummaryThis PR validates scope entries (teams, keys, models) on policy attachment creation, rejecting concrete entries that don't resolve to existing entities and returning a 400 with the offending values named. A matching front-end validator is added for the Teams field in the attachment form.
Confidence Score: 5/5Safe to merge — the change adds a validation gate that was previously absent, with a clean fail-open posture when the DB or router is unavailable, and comprehensive tests at both layers. The backend validation is correctly scoped to concrete (non-wildcard) entries, fails open on DB/router errors, and is well-covered by async unit tests using in-process fakes. The frontend validator only activates after a successful team-list load and defers to the backend otherwise. No regressions to existing happy-path behaviour were introduced. The dead
|
| Filename | Overview |
|---|---|
| litellm/proxy/policy_engine/policy_validator.py | Adds find_invalid_scope_entries method that validates concrete scope entries via DB/router; correctly delegates wildcard detection to RouteChecks._is_wildcard_pattern. The existing is_wildcard_pattern static method (which checks for ? or any *) is now dead code whose broader definition conflicts with the new method, but this was already flagged in a previous review thread. |
| litellm/proxy/policy_engine/policy_endpoints.py | Wires PolicyValidator.find_invalid_scope_entries into create_policy_attachment before persistence; raises HTTP 400 with joined error messages on any invalid scope entry. Logic is clean and only runs after confirming DB connectivity. |
| tests/test_litellm/proxy/policy_engine/test_policy_validator.py | New TestAttachmentScopeValidation class with 7 async tests covers: non-existent team/key/model flagged, existing entries pass, trailing-* wildcards allowed, non-trailing * and ? treated as concrete, fail-open when prisma_client=None, and empty scope returns no errors. All tests use in-process fakes — no real network calls. |
| ui/litellm-dashboard/src/components/policies/add_attachment_form.tsx | Adds teamsLoaded flag and antd validator on the Teams field that defers validation until a successful team-list fetch; wildcard entries pass through without a DB check. |
| ui/litellm-dashboard/src/components/policies/scope_validation.ts | New utility: isWildcardPattern (trailing * only) and getInvalidTeamEntries, mirroring backend wildcard semantics. Small and clear. |
| ui/litellm-dashboard/src/components/policies/scope_validation.test.ts | Unit tests for isWildcardPattern and getInvalidTeamEntries; covers wildcard pass-through, concrete alias rejection, and empty input. All pure, no network calls. |
| ui/litellm-dashboard/src/components/policies/add_attachment_form.test.tsx | Four new integration tests cover: non-existent team blocked, existing team passes, wildcard passes, and team-list-load-failure defers to backend. Tests use mocked networking and in-process renders. |
Reviews (2): Last reviewed commit: "fix(policies): separate multiple attachm..." | Re-trigger Greptile
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
Keeps the new find_invalid_scope_entries signature off the UP006/UP045 strict ruff budgets instead of copying the surrounding legacy typing.List/Optional idiom.
Addresses Greptile review: joining per-entry validation messages with a bare space read as one run-on sentence; ' | ' makes the multi-error 400 detail easier to parse for users and programmatically.
|
@greptileai re review |
23873f8
into
litellm_internal_staging
The policy attachment form fetched /team/list with the caller's own user_id, which the backend treats as a membership filter even for proxy admins. Admins only saw teams they were personally a member of, and the scope validation added in #32131 then rejected every other valid team alias as nonexistent. Drop the user_id filter; the policies page is admin-only and /team/list without user_id returns all teams for admin roles. Fixes LIT-4199
Relevant issues
Linear ticket
Resolves LIT-4199
Pre-Submission checklist
Please complete all items before asking a LiteLLM maintainer to review your PR
@greptileaiand received a Confidence Score of at least 4/5 before requesting a maintainer reviewDelays in PR merge?
If you're seeing a delay in your PR being merged, ping the LiteLLM Team on Slack (#pr-review).
Screenshots / Proof of Fix
Backend, against a live proxy on
localhost:4000. Before this change every one of these returned200and persisted the bogus valueOutput
UI, for screenshots:
this-team-does-not-exist, press Enter, then submit; the field now shows an inline error and the attachment is not createdsomething-*wildcard; submit succeedsType
Bug Fix
Changes
The policy attachment create endpoint (
POST /policies/attachments) accepted whatever teams, keys, and models were passed and persisted them verbatim, so a typo'd or non-existent team was stored silently and the policy applied nowhere. The Admin UI made this easy to hit because the Teams field is an Ant DesignSelectintagsmode, which accepts arbitrary free text.create_policy_attachmentnow validates the scope before writing. It wires in the previously unusedPolicyValidatorexistence checks and rejects any concrete team, key, or model that does not resolve to a real entity, returning a 400 that names the offending value. Whether an entry is "concrete" is decided byRouteChecks._is_wildcard_pattern, the same predicate the request-time matcher uses, so validation and matching agree: only a trailing*is a wildcard. Wildcard patterns are intentionally allowed through since a pattern likehealthcare-*may match zero teams today and more once they are created, and tags remain free-form because there is no registry to validate them against.On the frontend the Teams field keeps free-text entry (wildcards are a feature) but gains a validator that rejects a typed value that is neither an existing team alias nor a wildcard, giving immediate feedback. It only enforces this once the team list has loaded and defers to the backend otherwise, so a failed or partial load never blocks a legitimate team. Keys and models are left to the authoritative backend check rather than pre-validated in the browser, since the form loads only a partial key list and shows model ids rather than the names the backend matches on.