mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-07 02:59:05 +00:00
[Fix] AI Gateway Auth - allow using wildcard patterns for public routes (#17686)
* edit auth utils to allow wildcard patterns * docs fix private / public routes * test_route_in_additional_public_routes_wildcard_match
This commit is contained in:
parent
a57608a4e8
commit
074445edb1
5 changed files with 268 additions and 148 deletions
|
|
@ -15,8 +15,7 @@ Features:
|
|||
- ✅ [SSO for Admin UI](./ui.md#✨-enterprise-features)
|
||||
- ✅ [Audit Logs with retention policy](#audit-logs)
|
||||
- ✅ [JWT-Auth](./token_auth.md)
|
||||
- ✅ [Control available public, private routes (Restrict certain endpoints on proxy)](#control-available-public-private-routes)
|
||||
- ✅ [Control available public, private routes](#control-available-public-private-routes)
|
||||
- ✅ [Control available public, private routes](./public_routes.md)
|
||||
- ✅ [Secret Managers - AWS Key Manager, Google Secret Manager, Azure Key, Hashicorp Vault](../secret)
|
||||
- ✅ [[BETA] AWS Key Manager v2 - Key Decryption](#beta-aws-key-manager---key-decryption)
|
||||
- ✅ IP address‑based access control lists
|
||||
|
|
@ -181,148 +180,7 @@ Expected Response
|
|||
|
||||
### Control available public, private routes
|
||||
|
||||
**Restrict certain endpoints of proxy**
|
||||
|
||||
:::info
|
||||
|
||||
❓ Use this when you want to:
|
||||
- make an existing private route -> public
|
||||
- set certain routes as admin_only routes
|
||||
|
||||
:::
|
||||
|
||||
#### Usage - Define public, admin only routes
|
||||
|
||||
**Step 1** - Set on config.yaml
|
||||
|
||||
|
||||
| Route Type | Optional | Requires Virtual Key Auth | Admin Can Access | All Roles Can Access | Description |
|
||||
|------------|----------|---------------------------|-------------------|----------------------|-------------|
|
||||
| `public_routes` | ✅ | ❌ | ✅ | ✅ | Routes that can be accessed without any authentication |
|
||||
| `admin_only_routes` | ✅ | ✅ | ✅ | ❌ | Routes that can only be accessed by [Proxy Admin](./self_serve#available-roles) |
|
||||
| `allowed_routes` | ✅ | ✅ | ✅ | ✅ | Routes are exposed on the proxy. If not set then all routes exposed. |
|
||||
|
||||
`LiteLLMRoutes.public_routes` is an ENUM corresponding to the default public routes on LiteLLM. [You can see this here](https://github.com/BerriAI/litellm/blob/main/litellm/proxy/_types.py)
|
||||
|
||||
```yaml
|
||||
general_settings:
|
||||
master_key: sk-1234
|
||||
public_routes: ["LiteLLMRoutes.public_routes", "/spend/calculate"] # routes that can be accessed without any auth
|
||||
admin_only_routes: ["/key/generate"] # Optional - routes that can only be accessed by Proxy Admin
|
||||
allowed_routes: ["/chat/completions", "/spend/calculate", "LiteLLMRoutes.public_routes"] # Optional - routes that can be accessed by anyone after Authentication
|
||||
```
|
||||
|
||||
**Step 2** - start proxy
|
||||
|
||||
```shell
|
||||
litellm --config config.yaml
|
||||
```
|
||||
|
||||
**Step 3** - Test it
|
||||
|
||||
<Tabs>
|
||||
|
||||
<TabItem value="public" label="Test `public_routes`">
|
||||
|
||||
```shell
|
||||
curl --request POST \
|
||||
--url 'http://localhost:4000/spend/calculate' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"model": "gpt-4",
|
||||
"messages": [{"role": "user", "content": "Hey, how'\''s it going?"}]
|
||||
}'
|
||||
```
|
||||
|
||||
🎉 Expect this endpoint to work without an `Authorization / Bearer Token`
|
||||
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="admin_only_routes" label="Test `admin_only_routes`">
|
||||
|
||||
|
||||
**Successful Request**
|
||||
|
||||
```shell
|
||||
curl --location 'http://0.0.0.0:4000/key/generate' \
|
||||
--header 'Authorization: Bearer <your-master-key>' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{}'
|
||||
```
|
||||
|
||||
|
||||
**Un-successfull Request**
|
||||
|
||||
```shell
|
||||
curl --location 'http://0.0.0.0:4000/key/generate' \
|
||||
--header 'Authorization: Bearer <virtual-key-from-non-admin>' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{"user_role": "internal_user"}'
|
||||
```
|
||||
|
||||
**Expected Response**
|
||||
|
||||
```json
|
||||
{
|
||||
"error": {
|
||||
"message": "user not allowed to access this route. Route=/key/generate is an admin only route",
|
||||
"type": "auth_error",
|
||||
"param": "None",
|
||||
"code": "403"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="allowed_routes" label="Test `allowed_routes`">
|
||||
|
||||
|
||||
**Successful Request**
|
||||
|
||||
```shell
|
||||
curl http://localhost:4000/chat/completions \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer sk-1234" \
|
||||
-d '{
|
||||
"model": "fake-openai-endpoint",
|
||||
"messages": [
|
||||
{"role": "user", "content": "Hello, Claude"}
|
||||
]
|
||||
}'
|
||||
```
|
||||
|
||||
|
||||
**Un-successfull Request**
|
||||
|
||||
```shell
|
||||
curl --location 'http://0.0.0.0:4000/embeddings' \
|
||||
--header 'Content-Type: application/json' \
|
||||
-H "Authorization: Bearer sk-1234" \
|
||||
--data ' {
|
||||
"model": "text-embedding-ada-002",
|
||||
"input": ["write a litellm poem"]
|
||||
}'
|
||||
```
|
||||
|
||||
**Expected Response**
|
||||
|
||||
```json
|
||||
{
|
||||
"error": {
|
||||
"message": "Route /embeddings not allowed",
|
||||
"type": "auth_error",
|
||||
"param": "None",
|
||||
"code": "403"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
</TabItem>
|
||||
|
||||
</Tabs>
|
||||
See [Control Public & Private Routes](./public_routes.md) for detailed documentation on configuring public routes, admin-only routes, allowed routes, and wildcard patterns.
|
||||
|
||||
## Spend Tracking
|
||||
|
||||
|
|
|
|||
223
docs/my-website/docs/proxy/public_routes.md
Normal file
223
docs/my-website/docs/proxy/public_routes.md
Normal file
|
|
@ -0,0 +1,223 @@
|
|||
import Tabs from '@theme/Tabs';
|
||||
import TabItem from '@theme/TabItem';
|
||||
|
||||
# Control Public & Private Routes
|
||||
|
||||
:::info
|
||||
|
||||
Requires a LiteLLM Enterprise License. [Get a free trial](https://calendly.com/d/4mp-gd3-k5k/litellm-1-1-onboarding-chat).
|
||||
|
||||
:::
|
||||
|
||||
Control which routes require authentication and which routes are publicly accessible.
|
||||
|
||||
## Route Types
|
||||
|
||||
| Route Type | Requires Auth | Description |
|
||||
|------------|---------------|-------------|
|
||||
| `public_routes` | No | Routes accessible without any authentication |
|
||||
| `admin_only_routes` | Yes (Admin only) | Routes only accessible by [Proxy Admin](./self_serve#available-roles) |
|
||||
| `allowed_routes` | Yes | Routes exposed on the proxy. If not set, all routes are exposed |
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Make Routes Public
|
||||
|
||||
Allow specific routes to be accessed without authentication:
|
||||
|
||||
```yaml
|
||||
general_settings:
|
||||
master_key: sk-1234
|
||||
public_routes: ["LiteLLMRoutes.public_routes", "/spend/calculate"]
|
||||
```
|
||||
|
||||
### Restrict Routes to Admin Only
|
||||
|
||||
Restrict certain routes to only be accessible by Proxy Admin:
|
||||
|
||||
```yaml
|
||||
general_settings:
|
||||
master_key: sk-1234
|
||||
admin_only_routes: ["/key/generate", "/key/delete"]
|
||||
```
|
||||
|
||||
### Limit Available Routes
|
||||
|
||||
Only expose specific routes on the proxy:
|
||||
|
||||
```yaml
|
||||
general_settings:
|
||||
master_key: sk-1234
|
||||
allowed_routes: ["/chat/completions", "/embeddings", "LiteLLMRoutes.public_routes"]
|
||||
```
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Define Public, Admin Only, and Allowed Routes
|
||||
|
||||
```yaml
|
||||
general_settings:
|
||||
master_key: sk-1234
|
||||
public_routes: ["LiteLLMRoutes.public_routes", "/spend/calculate"]
|
||||
admin_only_routes: ["/key/generate"]
|
||||
allowed_routes: ["/chat/completions", "/spend/calculate", "LiteLLMRoutes.public_routes"]
|
||||
```
|
||||
|
||||
`LiteLLMRoutes.public_routes` is an ENUM corresponding to the default public routes on LiteLLM. [View the source](https://github.com/BerriAI/litellm/blob/main/litellm/proxy/_types.py).
|
||||
|
||||
### Testing
|
||||
|
||||
<Tabs>
|
||||
|
||||
<TabItem value="public" label="Test public_routes">
|
||||
|
||||
```shell
|
||||
curl --request POST \
|
||||
--url 'http://localhost:4000/spend/calculate' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"model": "gpt-4",
|
||||
"messages": [{"role": "user", "content": "Hey, how'\''s it going?"}]
|
||||
}'
|
||||
```
|
||||
|
||||
This endpoint works without an `Authorization` header.
|
||||
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="admin_only_routes" label="Test admin_only_routes">
|
||||
|
||||
**Successful Request (Admin)**
|
||||
|
||||
```shell
|
||||
curl --location 'http://0.0.0.0:4000/key/generate' \
|
||||
--header 'Authorization: Bearer <your-master-key>' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{}'
|
||||
```
|
||||
|
||||
**Unsuccessful Request (Non-Admin)**
|
||||
|
||||
```shell
|
||||
curl --location 'http://0.0.0.0:4000/key/generate' \
|
||||
--header 'Authorization: Bearer <virtual-key-from-non-admin>' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{"user_role": "internal_user"}'
|
||||
```
|
||||
|
||||
**Expected Response**
|
||||
|
||||
```json
|
||||
{
|
||||
"error": {
|
||||
"message": "user not allowed to access this route. Route=/key/generate is an admin only route",
|
||||
"type": "auth_error",
|
||||
"param": "None",
|
||||
"code": "403"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="allowed_routes" label="Test allowed_routes">
|
||||
|
||||
**Successful Request**
|
||||
|
||||
```shell
|
||||
curl http://localhost:4000/chat/completions \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer sk-1234" \
|
||||
-d '{
|
||||
"model": "fake-openai-endpoint",
|
||||
"messages": [
|
||||
{"role": "user", "content": "Hello, Claude"}
|
||||
]
|
||||
}'
|
||||
```
|
||||
|
||||
**Unsuccessful Request (Route Not Allowed)**
|
||||
|
||||
```shell
|
||||
curl --location 'http://0.0.0.0:4000/embeddings' \
|
||||
--header 'Content-Type: application/json' \
|
||||
-H "Authorization: Bearer sk-1234" \
|
||||
--data '{
|
||||
"model": "text-embedding-ada-002",
|
||||
"input": ["write a litellm poem"]
|
||||
}'
|
||||
```
|
||||
|
||||
**Expected Response**
|
||||
|
||||
```json
|
||||
{
|
||||
"error": {
|
||||
"message": "Route /embeddings not allowed",
|
||||
"type": "auth_error",
|
||||
"param": "None",
|
||||
"code": "403"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
|
||||
</Tabs>
|
||||
|
||||
## Advanced: Wildcard Patterns
|
||||
|
||||
Use wildcard patterns to match multiple routes at once.
|
||||
|
||||
### Syntax
|
||||
|
||||
| Pattern | Description | Example |
|
||||
|---------|-------------|---------|
|
||||
| `/path/*` | Matches any route starting with `/path/` | `/api/*` matches `/api/users`, `/api/users/123` |
|
||||
|
||||
|
||||
### Examples
|
||||
|
||||
#### Make All Routes Under a Path Public
|
||||
|
||||
```yaml
|
||||
general_settings:
|
||||
master_key: sk-1234
|
||||
public_routes:
|
||||
- "LiteLLMRoutes.public_routes"
|
||||
- "/api/v1/*" # All routes under /api/v1/
|
||||
- "/health/*" # All health check routes
|
||||
```
|
||||
|
||||
#### Restrict Admin Routes with Wildcards
|
||||
|
||||
```yaml
|
||||
general_settings:
|
||||
master_key: sk-1234
|
||||
admin_only_routes:
|
||||
- "/admin/*" # All admin routes
|
||||
- "/internal/*" # All internal routes
|
||||
```
|
||||
|
||||
### Testing Wildcard Routes
|
||||
|
||||
**Config:**
|
||||
```yaml
|
||||
general_settings:
|
||||
master_key: sk-1234
|
||||
public_routes:
|
||||
- "/public/*"
|
||||
```
|
||||
|
||||
**Test:**
|
||||
```shell
|
||||
# This works without auth (matches /public/*)
|
||||
curl http://localhost:4000/public/status
|
||||
|
||||
# This also works without auth (matches /public/*)
|
||||
curl http://localhost:4000/public/health/detailed
|
||||
|
||||
# This requires auth (doesn't match /public/*)
|
||||
curl http://localhost:4000/private/data
|
||||
```
|
||||
|
||||
|
|
@ -298,6 +298,7 @@ const sidebars = {
|
|||
"proxy/custom_auth",
|
||||
"proxy/ip_address",
|
||||
"proxy/multiple_admins",
|
||||
"proxy/public_routes",
|
||||
],
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -251,30 +251,38 @@ def route_in_additonal_public_routes(current_route: str):
|
|||
- bool - True if the route is defined in public_routes
|
||||
- bool - False if the route is not defined in public_routes
|
||||
|
||||
Supports wildcard patterns (e.g., "/api/*" matches "/api/users", "/api/users/123")
|
||||
|
||||
In order to use this the litellm config.yaml should have the following in general_settings:
|
||||
|
||||
```yaml
|
||||
general_settings:
|
||||
master_key: sk-1234
|
||||
public_routes: ["LiteLLMRoutes.public_routes", "/spend/calculate"]
|
||||
public_routes: ["LiteLLMRoutes.public_routes", "/spend/calculate", "/api/*"]
|
||||
```
|
||||
"""
|
||||
|
||||
# check if user is premium_user - if not do nothing
|
||||
from litellm.proxy.auth.route_checks import RouteChecks
|
||||
from litellm.proxy.proxy_server import general_settings, premium_user
|
||||
|
||||
try:
|
||||
if premium_user is not True:
|
||||
return False
|
||||
# check if this is defined on the config
|
||||
if general_settings is None:
|
||||
return False
|
||||
|
||||
routes_defined = general_settings.get("public_routes", [])
|
||||
|
||||
# Check exact match first
|
||||
if current_route in routes_defined:
|
||||
return True
|
||||
|
||||
# Check wildcard patterns
|
||||
for route_pattern in routes_defined:
|
||||
if RouteChecks._route_matches_wildcard_pattern(
|
||||
route=current_route, pattern=route_pattern
|
||||
):
|
||||
return True
|
||||
|
||||
return False
|
||||
except Exception as e:
|
||||
verbose_proxy_logger.error(f"route_in_additonal_public_routes: {str(e)}")
|
||||
|
|
|
|||
|
|
@ -804,4 +804,34 @@ def test_proxy_admin_viewer_can_access_global_spend_tags():
|
|||
pytest.fail(
|
||||
f"proxy_admin_viewer should be able to access /global/spend/tags route. Got error: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
def test_route_in_additional_public_routes_wildcard_match():
|
||||
"""
|
||||
Test that route_in_additonal_public_routes supports wildcard patterns.
|
||||
"""
|
||||
from litellm.proxy.auth.auth_utils import route_in_additonal_public_routes
|
||||
|
||||
with patch("litellm.proxy.proxy_server.general_settings", {"public_routes": ["/api/*"]}), \
|
||||
patch("litellm.proxy.proxy_server.premium_user", True):
|
||||
# Wildcard should match subpaths
|
||||
assert route_in_additonal_public_routes("/api/users") is True
|
||||
assert route_in_additonal_public_routes("/api/users/123") is True
|
||||
# Should not match different prefix
|
||||
assert route_in_additonal_public_routes("/other/path") is False
|
||||
|
||||
|
||||
def test_route_in_additional_public_routes_exact_match():
|
||||
"""
|
||||
Test that route_in_additonal_public_routes supports exact matches.
|
||||
"""
|
||||
from litellm.proxy.auth.auth_utils import route_in_additonal_public_routes
|
||||
|
||||
with patch("litellm.proxy.proxy_server.general_settings", {"public_routes": ["/health", "/status"]}), \
|
||||
patch("litellm.proxy.proxy_server.premium_user", True):
|
||||
# Exact matches should work
|
||||
assert route_in_additonal_public_routes("/health") is True
|
||||
assert route_in_additonal_public_routes("/status") is True
|
||||
# Non-matching routes should fail
|
||||
assert route_in_additonal_public_routes("/other") is False
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue