mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-06 02:48:13 +00:00
Add team admin permissions validation script and setup guide
- scripts/validate_team_admin_permissions.py: Validates team admin can view all team keys, add/remove members, add/edit/delete models, create team keys with all team models, and see team models in test key dropdown - docs/TEAM_ADMIN_VALIDATION_SETUP.md: Setup guide for database, proxy config, team admin creation, and running the validation Co-authored-by: yuneng-jiang <yuneng-jiang@users.noreply.github.com>
This commit is contained in:
parent
78834b5fa4
commit
b423fe92d9
2 changed files with 430 additions and 0 deletions
149
docs/TEAM_ADMIN_VALIDATION_SETUP.md
Normal file
149
docs/TEAM_ADMIN_VALIDATION_SETUP.md
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
# Team Admin Permissions Validation - Setup Guide
|
||||
|
||||
This guide explains how to set up the environment to validate that a team admin can perform the required actions.
|
||||
|
||||
## What You Need to Set Up
|
||||
|
||||
### 1. Database
|
||||
|
||||
The LiteLLM proxy uses **PostgreSQL** or **SQLite** for storing teams, keys, models, and memberships.
|
||||
|
||||
**Option A: Use default (Neon dev database)**
|
||||
If you run the proxy without `DATABASE_URL`, it connects to a default Neon dev database bundled with `litellm-proxy-extras`. This works for quick local testing.
|
||||
|
||||
**Option B: Use your own PostgreSQL**
|
||||
```bash
|
||||
# Create a database (e.g. via Docker)
|
||||
docker run -d --name litellm-postgres -e POSTGRES_PASSWORD=postgres -e POSTGRES_DB=litellm -p 5432:5432 postgres:15
|
||||
|
||||
# Set DATABASE_URL
|
||||
export DATABASE_URL="postgresql://postgres:postgres@localhost:5432/litellm"
|
||||
```
|
||||
|
||||
**Option C: Use SQLite (simplest for local dev)**
|
||||
```bash
|
||||
export DATABASE_URL="sqlite:///./litellm.db"
|
||||
```
|
||||
|
||||
### 2. Proxy Configuration
|
||||
|
||||
Create or update a config file (e.g. `dev_config.yaml`) with:
|
||||
|
||||
```yaml
|
||||
model_list:
|
||||
- model_name: openai/gpt-3.5-turbo
|
||||
litellm_params:
|
||||
model: gpt-3.5-turbo
|
||||
api_key: os.environ/OPENAI_API_KEY # or your key
|
||||
|
||||
general_settings:
|
||||
master_key: sk-1234 # Proxy admin key
|
||||
store_virtual_keys: true # Required for team keys
|
||||
|
||||
litellm_settings:
|
||||
drop_params: true
|
||||
telemetry: false
|
||||
```
|
||||
|
||||
### 3. Start the Proxy
|
||||
|
||||
```bash
|
||||
poetry run litellm --config dev_config.yaml --port 4000
|
||||
```
|
||||
|
||||
Wait for `/health` to return 200 (migrations run on startup, ~15–20 seconds).
|
||||
|
||||
### 4. Create a Team and Team Admin
|
||||
|
||||
**Step 1: Create a team (as proxy admin)**
|
||||
```bash
|
||||
curl -X POST 'http://localhost:4000/team/new' \
|
||||
-H 'Authorization: Bearer sk-1234' \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"team_id": "team-validation-test", "team_alias": "Validation Team"}'
|
||||
```
|
||||
|
||||
**Step 2: Create a user and add as team admin**
|
||||
|
||||
If using **SSO/JWT**: Log in via the UI, then use the proxy admin to update the team membership to make the user an admin.
|
||||
|
||||
If using **internal auth** (no SSO):
|
||||
```bash
|
||||
# Create user (if your setup supports it)
|
||||
# Add user to team as admin
|
||||
curl -X POST 'http://localhost:4000/team/member_add' \
|
||||
-H 'Authorization: Bearer sk-1234' \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{
|
||||
"team_id": "team-validation-test",
|
||||
"user_id": "<USER_ID>",
|
||||
"role": "admin"
|
||||
}'
|
||||
```
|
||||
|
||||
**Step 3: Get a token for the team admin**
|
||||
|
||||
- **JWT (SSO)**: Log in via UI, select the team, copy the JWT from the browser.
|
||||
- **API key**: Generate a key for the team admin user with `team_id` set.
|
||||
|
||||
```bash
|
||||
# As proxy admin, generate a key for the team admin user
|
||||
curl -X POST 'http://localhost:4000/key/generate' \
|
||||
-H 'Authorization: Bearer sk-1234' \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{
|
||||
"user_id": "<TEAM_ADMIN_USER_ID>",
|
||||
"team_id": "team-validation-test",
|
||||
"models": ["all-team-models"]
|
||||
}'
|
||||
```
|
||||
|
||||
The response contains the key; use it as `TEAM_ADMIN_TOKEN`.
|
||||
|
||||
### 5. Run the Validation Script
|
||||
|
||||
```bash
|
||||
# Install httpx if not present
|
||||
poetry add httpx
|
||||
|
||||
# Run validation
|
||||
export PROXY_URL="http://localhost:4000"
|
||||
export TEAM_ADMIN_TOKEN="<jwt-or-api-key-for-team-admin>"
|
||||
export TEAM_ID="team-validation-test"
|
||||
|
||||
# Optional: for add/remove member test
|
||||
export TEST_USER_ID="<user-id-to-add-then-remove>"
|
||||
|
||||
poetry run python scripts/validate_team_admin_permissions.py
|
||||
```
|
||||
|
||||
Or with arguments:
|
||||
|
||||
```bash
|
||||
poetry run python scripts/validate_team_admin_permissions.py \
|
||||
--proxy-url http://localhost:4000 \
|
||||
--team-admin-token <token> \
|
||||
--team-id team-validation-test \
|
||||
--test-user-id <optional-user-for-member-test>
|
||||
```
|
||||
|
||||
**Skip destructive tests:**
|
||||
- `--skip-model-crud` – Skip add/edit/delete model (avoids creating a test model)
|
||||
- `--skip-member-test` – Skip add/remove member (or provide `--test-user-id`)
|
||||
|
||||
## Validation Checklist
|
||||
|
||||
| # | Capability | API / UI | Notes |
|
||||
|---|------------|----------|-------|
|
||||
| 1 | View all team keys | `GET /key/list?include_team_keys=true&team_id=<team_id>` | Team admin sees all keys for their team |
|
||||
| 2 | Add/remove member | `POST /team/member_add`, `POST /team/member_delete` | Requires `TEST_USER_ID` |
|
||||
| 3 | Add/edit/delete model | `POST /model/new`, `POST /model/update`, `POST /model/delete` | Use `--skip-model-crud` to avoid creating test model |
|
||||
| 4 | Create team key with all team models | `POST /key/generate` with `models: ["all-team-models"]` | Script cleans up the created key |
|
||||
| 5 | See all team models in test key dropdown | `GET /models?team_id=<team_id>` | Used by UI for model selector |
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- **"Database not connected"**: Ensure `DATABASE_URL` is set and the proxy has finished migrations.
|
||||
- **403 on key list**: Ensure `include_team_keys=true` is passed and the user is a team admin (role=admin in `members_with_roles`).
|
||||
- **403 on model/new**: Team admin must specify `model_info.team_id` matching their admin team.
|
||||
- **Enterprise features**: Assigning team admins is an enterprise feature; ensure `LITELLM_LICENSE` is set if required.
|
||||
281
scripts/validate_team_admin_permissions.py
Normal file
281
scripts/validate_team_admin_permissions.py
Normal file
|
|
@ -0,0 +1,281 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Validate Team Admin Permissions
|
||||
|
||||
This script validates that a team admin can perform the following:
|
||||
1. View all team keys
|
||||
2. Add/remove member from team
|
||||
3. Add/edit/delete model in team
|
||||
4. Create team key with all team models
|
||||
5. See all team models in test key dropdown
|
||||
|
||||
Prerequisites:
|
||||
- Proxy running with database (PostgreSQL or SQLite)
|
||||
- Proxy config with store_virtual_keys: true
|
||||
- A team with at least one team admin user
|
||||
- Team admin JWT or API key for authentication
|
||||
|
||||
Usage:
|
||||
export PROXY_URL="http://localhost:4000"
|
||||
export TEAM_ADMIN_TOKEN="<jwt-or-api-key-for-team-admin>"
|
||||
export TEAM_ID="<team-id-to-test>"
|
||||
poetry run python scripts/validate_team_admin_permissions.py
|
||||
|
||||
Or pass as arguments:
|
||||
poetry run python scripts/validate_team_admin_permissions.py \\
|
||||
--proxy-url http://localhost:4000 \\
|
||||
--team-admin-token <token> \\
|
||||
--team-id <team-id>
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from typing import Optional, Tuple
|
||||
|
||||
try:
|
||||
import httpx
|
||||
except ImportError:
|
||||
httpx = None
|
||||
|
||||
|
||||
def _headers(token: str) -> dict:
|
||||
return {
|
||||
"Authorization": f"Bearer {token}",
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
|
||||
|
||||
def _get(url: str, token: str, params: Optional[dict] = None) -> dict:
|
||||
r = httpx.get(url, headers=_headers(token), params=params or {}, timeout=30)
|
||||
r.raise_for_status()
|
||||
return r.json()
|
||||
|
||||
|
||||
def _post(url: str, token: str, json_body: dict) -> dict:
|
||||
r = httpx.post(url, headers=_headers(token), json=json_body, timeout=30)
|
||||
r.raise_for_status()
|
||||
return r.json()
|
||||
|
||||
|
||||
def _delete(url: str, token: str, json_body: dict) -> dict:
|
||||
r = httpx.request("DELETE", url, headers=_headers(token), json=json_body, timeout=30)
|
||||
r.raise_for_status()
|
||||
return r.json()
|
||||
|
||||
|
||||
def validate_view_all_team_keys(base_url: str, token: str, team_id: str) -> Tuple[bool, str]:
|
||||
"""1. Team admin should be able to view all team keys."""
|
||||
url = f"{base_url}/key/list"
|
||||
params = {
|
||||
"page": 1,
|
||||
"size": 50,
|
||||
"return_full_object": "true",
|
||||
"include_team_keys": "true",
|
||||
"include_created_by_keys": "true",
|
||||
"team_id": team_id,
|
||||
}
|
||||
try:
|
||||
data = _get(url, token, params)
|
||||
keys = data.get("keys", [])
|
||||
total = data.get("total_count", 0)
|
||||
return True, f"OK: Listed {len(keys)} keys (total_count={total})"
|
||||
except Exception as e:
|
||||
return False, f"FAIL: {e}"
|
||||
|
||||
|
||||
def validate_add_remove_member(
|
||||
base_url: str, token: str, team_id: str, test_user_id: Optional[str] = None
|
||||
) -> Tuple[bool, str]:
|
||||
"""2. Team admin should be able to add/remove member from team."""
|
||||
if not test_user_id:
|
||||
return True, "SKIP: No test_user_id provided (set --test-user-id to validate)"
|
||||
|
||||
# Add member
|
||||
add_url = f"{base_url}/team/member_add"
|
||||
try:
|
||||
_post(add_url, token, {"team_id": team_id, "user_id": test_user_id})
|
||||
except Exception as e:
|
||||
# May already be a member
|
||||
if "already" in str(e).lower() or "409" in str(e):
|
||||
pass
|
||||
else:
|
||||
return False, f"FAIL add member: {e}"
|
||||
|
||||
# Remove member
|
||||
del_url = f"{base_url}/team/member_delete"
|
||||
try:
|
||||
_post(del_url, token, {"team_id": team_id, "user_id": test_user_id})
|
||||
return True, "OK: Add and remove member succeeded"
|
||||
except Exception as e:
|
||||
return False, f"FAIL remove member: {e}"
|
||||
|
||||
|
||||
def validate_add_edit_delete_model(
|
||||
base_url: str, token: str, team_id: str, model_id: Optional[str] = None
|
||||
) -> Tuple[bool, str]:
|
||||
"""3. Team admin should be able to add/edit/delete model in team."""
|
||||
model_name = model_id or f"openai/test-team-model-{team_id[:8]}"
|
||||
|
||||
# Add model
|
||||
add_url = f"{base_url}/model/new"
|
||||
add_body = {
|
||||
"model_name": model_name,
|
||||
"litellm_params": {
|
||||
"model": "gpt-3.5-turbo",
|
||||
"api_key": "test-key",
|
||||
},
|
||||
"model_info": {"team_id": team_id},
|
||||
}
|
||||
try:
|
||||
_post(add_url, token, add_body)
|
||||
except Exception as e:
|
||||
return False, f"FAIL add model: {e}"
|
||||
|
||||
# Update model
|
||||
update_url = f"{base_url}/model/update"
|
||||
update_body = {
|
||||
"id": model_name,
|
||||
"model_info": {"team_id": team_id, "team_public_model_name": "Test Model"},
|
||||
}
|
||||
try:
|
||||
_post(update_url, token, update_body)
|
||||
except Exception as e:
|
||||
return False, f"FAIL update model: {e}"
|
||||
|
||||
# Delete model
|
||||
del_url = f"{base_url}/model/delete"
|
||||
try:
|
||||
_post(del_url, token, {"id": model_name})
|
||||
return True, "OK: Add, edit, delete model succeeded"
|
||||
except Exception as e:
|
||||
return False, f"FAIL delete model: {e}"
|
||||
|
||||
|
||||
def validate_create_team_key(base_url: str, token: str, team_id: str) -> Tuple[bool, str]:
|
||||
"""4. Team admin should be able to create team key with all team models."""
|
||||
url = f"{base_url}/key/generate"
|
||||
body = {
|
||||
"team_id": team_id,
|
||||
"models": ["all-team-models"],
|
||||
"key_alias": "team-admin-validation-key",
|
||||
}
|
||||
try:
|
||||
data = _post(url, token, body)
|
||||
key = data.get("key") or (data.get("keys", [None])[0] if data.get("keys") else None)
|
||||
# Clean up: delete the key (by alias to avoid raw key handling)
|
||||
del_url = f"{base_url}/key/delete"
|
||||
try:
|
||||
_post(del_url, token, {"key_aliases": ["team-admin-validation-key"]})
|
||||
except Exception:
|
||||
pass # Best-effort cleanup
|
||||
return True, "OK: Created team key with all team models"
|
||||
except Exception as e:
|
||||
return False, f"FAIL: {e}"
|
||||
|
||||
|
||||
def validate_test_key_dropdown_models(base_url: str, token: str, team_id: str) -> Tuple[bool, str]:
|
||||
"""5. Team admin should see all team models in test key dropdown (GET /models)."""
|
||||
url = f"{base_url}/models"
|
||||
params = {"team_id": team_id, "return_wildcard_routes": "True"}
|
||||
try:
|
||||
data = _get(url, token, params)
|
||||
models = data.get("data", [])
|
||||
model_ids = [m.get("id", m) if isinstance(m, dict) else str(m) for m in models]
|
||||
return True, f"OK: Got {len(model_ids)} models for team (sample: {model_ids[:5]})"
|
||||
except Exception as e:
|
||||
return False, f"FAIL: {e}"
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="Validate team admin permissions")
|
||||
parser.add_argument(
|
||||
"--proxy-url",
|
||||
default=os.environ.get("PROXY_URL", "http://localhost:4000"),
|
||||
help="Proxy base URL",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--team-admin-token",
|
||||
default=os.environ.get("TEAM_ADMIN_TOKEN"),
|
||||
required=True,
|
||||
help="JWT or API key for team admin user",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--team-id",
|
||||
default=os.environ.get("TEAM_ID"),
|
||||
required=True,
|
||||
help="Team ID to validate",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--test-user-id",
|
||||
default=os.environ.get("TEST_USER_ID"),
|
||||
help="User ID to add/remove for member test (optional)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--skip-member-test",
|
||||
action="store_true",
|
||||
help="Skip add/remove member test",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--skip-model-crud",
|
||||
action="store_true",
|
||||
help="Skip add/edit/delete model test (avoids creating test model)",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
if httpx is None:
|
||||
print("ERROR: httpx required. Run: poetry add httpx")
|
||||
return 1
|
||||
|
||||
base = args.proxy_url.rstrip("/")
|
||||
token = args.team_admin_token
|
||||
team_id = args.team_id
|
||||
|
||||
checks = [
|
||||
("View all team keys", lambda: validate_view_all_team_keys(base, token, team_id)),
|
||||
(
|
||||
"Add/remove member from team",
|
||||
lambda: validate_add_remove_member(base, token, team_id, args.test_user_id)
|
||||
if not args.skip_member_test
|
||||
else (True, "SKIP: --skip-member-test"),
|
||||
),
|
||||
(
|
||||
"Add/edit/delete model in team",
|
||||
lambda: validate_add_edit_delete_model(base, token, team_id)
|
||||
if not args.skip_model_crud
|
||||
else (True, "SKIP: --skip-model-crud"),
|
||||
),
|
||||
("Create team key with all team models", lambda: validate_create_team_key(base, token, team_id)),
|
||||
(
|
||||
"See all team models in test key dropdown",
|
||||
lambda: validate_test_key_dropdown_models(base, token, team_id),
|
||||
),
|
||||
]
|
||||
|
||||
print("=" * 60)
|
||||
print("Team Admin Permissions Validation")
|
||||
print("=" * 60)
|
||||
print(f"Proxy: {base}")
|
||||
print(f"Team ID: {team_id}")
|
||||
print()
|
||||
|
||||
failed = 0
|
||||
for name, fn in checks:
|
||||
ok, msg = fn()
|
||||
status = "PASS" if ok else "FAIL"
|
||||
print(f" [{status}] {name}")
|
||||
print(f" {msg}")
|
||||
if not ok:
|
||||
failed += 1
|
||||
|
||||
print()
|
||||
if failed == 0:
|
||||
print("All checks passed.")
|
||||
return 0
|
||||
print(f"{failed} check(s) failed.")
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Loading…
Add table
Reference in a new issue