""" 1. Default permissions for members in a team - allowed to call /key/info and /key/health - Create a team, create a member in a team (role = "user") Invalid Permissions: - User tries creating a key with team_id = team_id -> expect to fail. Invalid Permissions - User tries editing a key with team_id = team_id -> expect to fail. Invalid Permissions - User tries deleting a key with team_id = team_id -> expect to fail. Invalid Permissions - User tries regenerating a key with team_id = team_id -> expect to fail. Invalid Permissions Valid Permissions: - User tries calling /key/info with team_id, expect to get valid response 2. Permissions - members allowd to edit, delete keys but not allowed to create keys - Create a team with member_permissions = ["/key/update", "/key/delete", "/key/info"] - Create a member in the team with role = "user" Valid Permissions: - User tries editing a key with team_id = team_id -> expect to pass. Valid Permissions - Note: Delete/regenerate require key ownership or team admin status, not just team member permissions - User tries deleting a key with team_id = team_id -> expect to fail (403) unless user owns the key or is team admin - User tries regenerating a key with team_id = team_id -> expect to fail (403) unless user owns the key or is team admin Invalid Permissions: - User tries creating a key with team_id = team_id -> expect to fail. Invalid Permissions - User tries calling /key/info with team_id, expect to get valid response 3. Permissions - members allowed to create keys but not allowed to edit, delete keys - Create a team with member_permissions = ["/key/generate"] - Create a member in the team with role = "user" Valid Permissions: - User tries creating a key with team_id = team_id -> expect to pass. Valid Permissions Invalid Permissions: - User tries editing a key with team_id = team_id -> expect to fail. Invalid Permissions - User tries deleting a key with team_id = team_id -> expect to fail. Invalid Permissions - User tries regenerating a key with team_id = team_id -> expect to fail. Invalid Permissions """ import pytest import asyncio import aiohttp, openai from litellm._uuid import uuid import json from litellm.proxy._types import ProxyErrorTypes from typing import Optional LITELLM_MASTER_KEY = "sk-1234" async def create_team(session, key, member_permissions=None): url = "http://0.0.0.0:4000/team/new" headers = { "Authorization": f"Bearer {key}", "Content-Type": "application/json", } data = {"team_member_permissions": member_permissions} async with session.post(url, headers=headers, json=data) as response: status = response.status response_text = await response.text() if status != 200: raise Exception(response_text) return await response.json() async def create_user(session, key, user_id, team_id=None): url = "http://0.0.0.0:4000/user/new" headers = { "Authorization": f"Bearer {key}", "Content-Type": "application/json", } data = {"user_id": user_id} if team_id: data["team_id"] = team_id async with session.post(url, headers=headers, json=data) as response: status = response.status response_text = await response.text() if status != 200: raise Exception(response_text) return await response.json() async def add_team_member(session, key, team_id, user_id, role="user"): url = "http://0.0.0.0:4000/team/member_add" headers = { "Authorization": f"Bearer {key}", "Content-Type": "application/json", } data = {"team_id": team_id, "member": {"role": role, "user_id": user_id}} print("Adding team member with data: ", data) async with session.post(url, headers=headers, json=data) as response: status = response.status response_text = await response.text() if status != 200: raise Exception(response_text) return await response.json() async def generate_key(session, key, team_id=None, user_id=None): url = "http://0.0.0.0:4000/key/generate" headers = { "Authorization": f"Bearer {key}", "Content-Type": "application/json", } data = {} if team_id: data["team_id"] = team_id if user_id: data["user_id"] = user_id async with session.post(url, headers=headers, json=data) as response: status = response.status response_text = await response.text() if status != 200: return {"status": status, "error": response_text} return await response.json() async def key_info(session, key, key_id): url = f"http://0.0.0.0:4000/key/info?key={key_id}" headers = { "Authorization": f"Bearer {key}", "Content-Type": "application/json", } async with session.get(url, headers=headers) as response: status = response.status response_text = await response.text() if status != 200: return {"status": status, "error": response_text} return await response.json() async def update_key( session: aiohttp.ClientSession, key: str, key_id: str, team_id: Optional[str] = None, ): """ Update a key Args: key: key to use for authentication key_id: key to update """ url = "http://0.0.0.0:4000/key/update" headers = { "Authorization": f"Bearer {key}", "Content-Type": "application/json", } data = {"key": key_id, "metadata": {"updated": True}} if team_id: data["team_id"] = team_id async with session.post(url, headers=headers, json=data) as response: status = response.status response_text = await response.text() if status != 200: return {"status": status, "error": response_text} return await response.json() async def delete_key(session, key, key_id): url = "http://0.0.0.0:4000/key/delete" headers = { "Authorization": f"Bearer {key}", "Content-Type": "application/json", } data = {"keys": [key_id]} async with session.post(url, headers=headers, json=data) as response: status = response.status response_text = await response.text() if status != 200: return {"status": status, "error": response_text} return await response.json() async def regenerate_key(session, key, key_id, team_id=None): url = "http://0.0.0.0:4000/key/regenerate" headers = { "Authorization": f"Bearer {key}", "Content-Type": "application/json", } data = {"key": key_id} if team_id: data["team_id"] = team_id async with session.post(url, headers=headers, json=data) as response: status = response.status response_text = await response.text() if status != 200: return {"status": status, "error": response_text} return await response.json() @pytest.mark.asyncio() async def test_default_member_permissions(): """ Test default permissions for members in a team - allowed to call /key/info and /key/health """ async with aiohttp.ClientSession() as session: master_key = LITELLM_MASTER_KEY # Create a team team_data = await create_team(session=session, key=master_key) team_id = team_data["team_id"] # create a team key team_key_data = await generate_key( session=session, key=master_key, team_id=team_id ) team_key = team_key_data["key"] # create a user user_data = await create_user( session=session, key=master_key, user_id=f"user_{uuid.uuid4().hex[:8]}", team_id=team_id, ) user_id = user_data["user_id"] # Create a user key print("New user data: ", user_data) # Create a user key user_key_data = await generate_key( session=session, key=master_key, user_id=user_id ) print("new user key: ", user_key_data) user_key = user_key_data["key"] # Test invalid permissions # User tries creating a key with team_id print( "Regular team member trying to create a key with team_id. Expecting error." ) create_result = await generate_key( session=session, key=user_key, team_id=team_id ) print("result: ", create_result) assert ( "status" in create_result and create_result["status"] == 401 ), "User should not be able to create keys for team" error_data = json.loads(create_result["error"]) print("error response =", json.dumps(error_data, indent=4)) assert ( error_data["error"]["type"] == ProxyErrorTypes.team_member_permission_error.value ), "Error should be a team member permission error" # User tries editing a key with team_id print("Regular team member trying to edit a key with team_id. Expecting error.") update_result = await update_key( session=session, key=user_key, key_id=team_key, team_id="ATTACKER_TEAM_ID" ) assert ( "status" in update_result and update_result["status"] == 401 ), "User should not be able to update keys for team" error_data = json.loads(update_result["error"]) print("error response =", json.dumps(error_data, indent=4)) assert ( error_data["error"]["type"] == ProxyErrorTypes.team_member_permission_error.value ), "Error should be a team member permission error" # User tries deleting a key with team_id print( "Regular team member trying to delete a key with team_id. Expecting error." ) delete_result = await delete_key( session=session, key=user_key, key_id=team_key, ) assert ( "status" in delete_result and delete_result["status"] == 403 ), "User should not be able to delete keys for team" error_data = json.loads(delete_result["error"]) print("error response =", json.dumps(error_data, indent=4)) # Delete endpoint now returns 403 with authorization error, not team_member_permission_error assert "error" in error_data, "Error should contain error field" # User tries regenerating a key with team_id print( "Regular team member trying to regenerate a key with team_id. Expecting error." ) regenerate_result = await regenerate_key( session=session, key=user_key, key_id=team_key, ) assert ( "status" in regenerate_result and regenerate_result["status"] == 401 ), "User should not be able to regenerate keys for team" error_data = json.loads(regenerate_result["error"]) print("error response =", json.dumps(error_data, indent=4)) # Regenerate endpoint now returns 403 with authorization error, not team_member_permission_error assert "error" in error_data, "Error should contain error field" # Test valid permissions # User tries calling /key/info with team_id print( "Regular team member trying to get key info with team_id. Expecting success." ) info_result = await key_info( session=session, key=user_key, key_id=team_key, ) print("info result =", info_result) assert "status" not in info_result, "Admin should be able to get key info" @pytest.mark.asyncio() async def test_edit_delete_permissions(): """ Test permissions - members allowed to edit, delete keys but not allowed to create keys """ async with aiohttp.ClientSession() as session: master_key = LITELLM_MASTER_KEY # Create a team with specific member permissions team_data = await create_team( session=session, key=master_key, member_permissions=["/key/update", "/key/delete", "/key/info"], ) team_id = team_data["team_id"] # create a user in team=team_id user_data = await create_user( session=session, key=master_key, user_id=f"user_{uuid.uuid4().hex[:8]}", team_id=team_id, ) user_id = user_data["user_id"] # Generate an admin key for the team admin_key_data = await generate_key(session, master_key, team_id) key_id = admin_key_data["key"] # Create a user key user_key_data = await generate_key( session=session, key=master_key, user_id=user_id ) user_key = user_key_data["key"] # Test valid permissions # User tries editing a key with team_id update_result = await update_key( session=session, key=user_key, key_id=key_id, team_id=team_id ) assert ( "status" not in update_result ), "User should be able to update keys for team" # User tries deleting a key with team_id # Note: Even with /key/delete permission, users can only delete keys they own or if they're team admin # The delete endpoint checks ownership/team admin status, not just team member permissions delete_result = await delete_key(session=session, key=user_key, key_id=key_id) assert ( "status" in delete_result and delete_result["status"] == 403 ), "User should not be able to delete keys they don't own (even with /key/delete permission, ownership is required)" # Test invalid permissions # User tries creating a key with team_id create_result = await generate_key( session=session, key=user_key, team_id=team_id ) assert ( "status" in create_result and create_result["status"] != 200 ), "User should not be able to create keys for team" # User tries regenerating a key with team_id # Note: Even with /key/regenerate permission, users can only regenerate keys they own or if they're team admin regenerate_result = await regenerate_key( session=session, key=user_key, key_id=key_id, team_id=team_id ) assert ( "status" in regenerate_result and regenerate_result["status"] == 401 ), "User should not be able to regenerate keys they don't own (even with /key/regenerate permission, ownership is required)" @pytest.mark.asyncio() async def test_create_permissions(): """ Test permissions - members allowed to create keys but not allowed to edit, delete keys """ async with aiohttp.ClientSession() as session: master_key = LITELLM_MASTER_KEY # Create a team with specific member permissions team_data = await create_team( session=session, key=master_key, member_permissions=["/key/generate"] ) team_id = team_data["team_id"] # Create a user in the team user_id = f"user_{uuid.uuid4().hex[:8]}" await add_team_member( session=session, key=master_key, team_id=team_id, user_id=user_id, role="user", ) # Generate an admin key for the team admin_key_data = await generate_key( session=session, key=master_key, team_id=team_id ) admin_key = admin_key_data["key"] key_id = admin_key_data["key"] # Create a user key user_key_data = await generate_key( session=session, key=master_key, user_id=user_id ) user_key = user_key_data["key"] # Test valid permissions # User tries creating a key with team_id create_result = await generate_key( session=session, key=user_key, team_id=team_id ) print("success, user created key for team=", create_result) assert "key" in create_result, "User should be able to create keys for team" assert ( create_result["team_id"] == team_id ), "User should be able to create keys for team" assert ( "status" not in create_result ), "User should be able to create keys for team" # Test invalid permissions # User tries editing a key with team_id update_result = await update_key( session=session, key=user_key, key_id=key_id, team_id=team_id ) assert ( "status" in update_result and update_result["status"] != 200 ), "User should not be able to update keys for team" # User tries deleting a key with team_id delete_result = await delete_key(session=session, key=user_key, key_id=key_id) assert ( "status" in delete_result and delete_result["status"] == 403 ), "User should not be able to delete keys for team" # User tries regenerating a key with team_id # User doesn't have /key/regenerate permission, so should get 401 (team member permission error) regenerate_result = await regenerate_key( session=session, key=user_key, key_id=key_id, team_id=team_id ) assert ( "status" in regenerate_result and regenerate_result["status"] == 401 ), "User should not be able to regenerate keys for team (no /key/regenerate permission)" error_data = json.loads(regenerate_result["error"]) assert ( error_data["error"]["type"] == ProxyErrorTypes.team_member_permission_error.value ), "Error should be a team member permission error"