mirror of
https://github.com/BerriAI/litellm.git
synced 2025-04-25 18:54:30 +00:00
(feat proxy) [beta] add support for organization role based access controls (#6112)
* track LiteLLM_OrganizationMembership * add add_internal_user_to_organization * add org membership to schema * read organization membership when reading user info in auth checks * add check for valid organization_id * add test for test_create_new_user_in_organization * test test_create_new_user_in_organization * add new ADMIN role * add test for org admins creating teams * add test for test_org_admin_create_user_permissions * test_org_admin_create_user_team_wrong_org_permissions * test_org_admin_create_user_team_wrong_org_permissions * fix organization_role_based_access_check * fix getting user members * fix TeamBase * fix types used for use role * fix type checks * sync prisma schema * docs - organization admins * fix use organization_endpoints for /organization management * add types for org member endpoints * fix role name for org admin * add type for member add response * add organization/member_add * add error handling for adding members to an org * add nice doc string for oranization/member_add * fix test_create_new_user_in_organization * linting fix * use simple route changes * fix types * add organization member roles * add org admin auth checks * add auth checks for orgs * test for creating teams as org admin * simplify org id usage * fix typo * test test_org_admin_create_user_team_wrong_org_permissions * fix type check issue * code quality fix * fix schema.prisma
This commit is contained in:
parent
945267a511
commit
1fd437e263
14 changed files with 1474 additions and 261 deletions
433
litellm/proxy/management_endpoints/organization_endpoints.py
Normal file
433
litellm/proxy/management_endpoints/organization_endpoints.py
Normal file
|
@ -0,0 +1,433 @@
|
|||
"""
|
||||
Endpoints for /organization operations
|
||||
|
||||
/organization/new
|
||||
/organization/update
|
||||
/organization/delete
|
||||
/organization/info
|
||||
"""
|
||||
|
||||
#### ORGANIZATION MANAGEMENT ####
|
||||
|
||||
import asyncio
|
||||
import copy
|
||||
import json
|
||||
import re
|
||||
import secrets
|
||||
import traceback
|
||||
import uuid
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import List, Optional, Tuple
|
||||
|
||||
import fastapi
|
||||
from fastapi import APIRouter, Depends, Header, HTTPException, Query, Request, status
|
||||
|
||||
import litellm
|
||||
from litellm._logging import verbose_proxy_logger
|
||||
from litellm.proxy._types import *
|
||||
from litellm.proxy.auth.user_api_key_auth import user_api_key_auth
|
||||
from litellm.proxy.management_helpers.utils import (
|
||||
get_new_internal_user_defaults,
|
||||
management_endpoint_wrapper,
|
||||
)
|
||||
from litellm.proxy.utils import PrismaClient
|
||||
from litellm.secret_managers.main import get_secret
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post(
|
||||
"/organization/new",
|
||||
tags=["organization management"],
|
||||
dependencies=[Depends(user_api_key_auth)],
|
||||
response_model=NewOrganizationResponse,
|
||||
)
|
||||
async def new_organization(
|
||||
data: NewOrganizationRequest,
|
||||
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
|
||||
):
|
||||
"""
|
||||
Allow orgs to own teams
|
||||
|
||||
Set org level budgets + model access.
|
||||
|
||||
Only admins can create orgs.
|
||||
|
||||
# Parameters
|
||||
|
||||
- `organization_alias`: *str* = The name of the organization.
|
||||
- `models`: *List* = The models the organization has access to.
|
||||
- `budget_id`: *Optional[str]* = The id for a budget (tpm/rpm/max budget) for the organization.
|
||||
### IF NO BUDGET ID - CREATE ONE WITH THESE PARAMS ###
|
||||
- `max_budget`: *Optional[float]* = Max budget for org
|
||||
- `tpm_limit`: *Optional[int]* = Max tpm limit for org
|
||||
- `rpm_limit`: *Optional[int]* = Max rpm limit for org
|
||||
- `model_max_budget`: *Optional[dict]* = Max budget for a specific model
|
||||
- `budget_duration`: *Optional[str]* = Frequency of reseting org budget
|
||||
|
||||
Case 1: Create new org **without** a budget_id
|
||||
|
||||
```bash
|
||||
curl --location 'http://0.0.0.0:4000/organization/new' \
|
||||
|
||||
--header 'Authorization: Bearer sk-1234' \
|
||||
|
||||
--header 'Content-Type: application/json' \
|
||||
|
||||
--data '{
|
||||
"organization_alias": "my-secret-org",
|
||||
"models": ["model1", "model2"],
|
||||
"max_budget": 100
|
||||
}'
|
||||
|
||||
|
||||
```
|
||||
|
||||
Case 2: Create new org **with** a budget_id
|
||||
|
||||
```bash
|
||||
curl --location 'http://0.0.0.0:4000/organization/new' \
|
||||
|
||||
--header 'Authorization: Bearer sk-1234' \
|
||||
|
||||
--header 'Content-Type: application/json' \
|
||||
|
||||
--data '{
|
||||
"organization_alias": "my-secret-org",
|
||||
"models": ["model1", "model2"],
|
||||
"budget_id": "428eeaa8-f3ac-4e85-a8fb-7dc8d7aa8689"
|
||||
}'
|
||||
```
|
||||
"""
|
||||
from litellm.proxy.proxy_server import litellm_proxy_admin_name, prisma_client
|
||||
|
||||
if prisma_client is None:
|
||||
raise HTTPException(status_code=500, detail={"error": "No db connected"})
|
||||
|
||||
if (
|
||||
user_api_key_dict.user_role is None
|
||||
or user_api_key_dict.user_role != LitellmUserRoles.PROXY_ADMIN
|
||||
):
|
||||
raise HTTPException(
|
||||
status_code=401,
|
||||
detail={
|
||||
"error": f"Only admins can create orgs. Your role is = {user_api_key_dict.user_role}"
|
||||
},
|
||||
)
|
||||
|
||||
if data.budget_id is None:
|
||||
"""
|
||||
Every organization needs a budget attached.
|
||||
|
||||
If none provided, create one based on provided values
|
||||
"""
|
||||
budget_params = LiteLLM_BudgetTable.model_fields.keys()
|
||||
|
||||
# Only include Budget Params when creating an entry in litellm_budgettable
|
||||
_json_data = data.json(exclude_none=True)
|
||||
_budget_data = {k: v for k, v in _json_data.items() if k in budget_params}
|
||||
budget_row = LiteLLM_BudgetTable(**_budget_data)
|
||||
|
||||
new_budget = prisma_client.jsonify_object(budget_row.json(exclude_none=True))
|
||||
|
||||
_budget = await prisma_client.db.litellm_budgettable.create(
|
||||
data={
|
||||
**new_budget, # type: ignore
|
||||
"created_by": user_api_key_dict.user_id or litellm_proxy_admin_name,
|
||||
"updated_by": user_api_key_dict.user_id or litellm_proxy_admin_name,
|
||||
}
|
||||
) # type: ignore
|
||||
|
||||
data.budget_id = _budget.budget_id
|
||||
|
||||
"""
|
||||
Ensure only models that user has access to, are given to org
|
||||
"""
|
||||
if len(user_api_key_dict.models) == 0: # user has access to all models
|
||||
pass
|
||||
else:
|
||||
if len(data.models) == 0:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail={
|
||||
"error": "User not allowed to give access to all models. Select models you want org to have access to."
|
||||
},
|
||||
)
|
||||
for m in data.models:
|
||||
if m not in user_api_key_dict.models:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail={
|
||||
"error": f"User not allowed to give access to model={m}. Models you have access to = {user_api_key_dict.models}"
|
||||
},
|
||||
)
|
||||
organization_row = LiteLLM_OrganizationTable(
|
||||
**data.json(exclude_none=True),
|
||||
created_by=user_api_key_dict.user_id or litellm_proxy_admin_name,
|
||||
updated_by=user_api_key_dict.user_id or litellm_proxy_admin_name,
|
||||
)
|
||||
new_organization_row = prisma_client.jsonify_object(
|
||||
organization_row.json(exclude_none=True)
|
||||
)
|
||||
response = await prisma_client.db.litellm_organizationtable.create(
|
||||
data={
|
||||
**new_organization_row, # type: ignore
|
||||
}
|
||||
)
|
||||
|
||||
return response
|
||||
|
||||
|
||||
@router.post(
|
||||
"/organization/update",
|
||||
tags=["organization management"],
|
||||
dependencies=[Depends(user_api_key_auth)],
|
||||
)
|
||||
async def update_organization():
|
||||
"""[TODO] Not Implemented yet. Let us know if you need this - https://github.com/BerriAI/litellm/issues"""
|
||||
pass
|
||||
|
||||
|
||||
@router.post(
|
||||
"/organization/delete",
|
||||
tags=["organization management"],
|
||||
dependencies=[Depends(user_api_key_auth)],
|
||||
)
|
||||
async def delete_organization():
|
||||
"""[TODO] Not Implemented yet. Let us know if you need this - https://github.com/BerriAI/litellm/issues"""
|
||||
pass
|
||||
|
||||
|
||||
@router.post(
|
||||
"/organization/info",
|
||||
tags=["organization management"],
|
||||
dependencies=[Depends(user_api_key_auth)],
|
||||
)
|
||||
async def info_organization(data: OrganizationRequest):
|
||||
"""
|
||||
Get the org specific information
|
||||
"""
|
||||
from litellm.proxy.proxy_server import prisma_client
|
||||
|
||||
if prisma_client is None:
|
||||
raise HTTPException(status_code=500, detail={"error": "No db connected"})
|
||||
|
||||
if len(data.organizations) == 0:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail={
|
||||
"error": f"Specify list of organization id's to query. Passed in={data.organizations}"
|
||||
},
|
||||
)
|
||||
response = await prisma_client.db.litellm_organizationtable.find_many(
|
||||
where={"organization_id": {"in": data.organizations}},
|
||||
include={"litellm_budget_table": True},
|
||||
)
|
||||
|
||||
return response
|
||||
|
||||
|
||||
@router.post(
|
||||
"/organization/member_add",
|
||||
tags=["organization management"],
|
||||
dependencies=[Depends(user_api_key_auth)],
|
||||
response_model=OrganizationAddMemberResponse,
|
||||
)
|
||||
@management_endpoint_wrapper
|
||||
async def organization_member_add(
|
||||
data: OrganizationMemberAddRequest,
|
||||
http_request: Request,
|
||||
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
|
||||
) -> OrganizationAddMemberResponse:
|
||||
"""
|
||||
[BETA]
|
||||
|
||||
Add new members (either via user_email or user_id) to an organization
|
||||
|
||||
If user doesn't exist, new user row will also be added to User Table
|
||||
|
||||
Only proxy_admin or org_admin of organization, allowed to access this endpoint.
|
||||
|
||||
# Parameters:
|
||||
|
||||
- organization_id: str (required)
|
||||
- member: Union[List[Member], Member] (required)
|
||||
- role: Literal[LitellmUserRoles] (required)
|
||||
- user_id: Optional[str]
|
||||
- user_email: Optional[str]
|
||||
|
||||
Note: Either user_id or user_email must be provided for each member.
|
||||
|
||||
Example:
|
||||
```
|
||||
curl -X POST 'http://0.0.0.0:4000/organization/member_add' \
|
||||
-H 'Authorization: Bearer sk-1234' \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{
|
||||
"organization_id": "45e3e396-ee08-4a61-a88e-16b3ce7e0849",
|
||||
"member": {
|
||||
"role": "internal_user",
|
||||
"user_id": "krrish247652@berri.ai"
|
||||
},
|
||||
"max_budget_in_organization": 100.0
|
||||
}'
|
||||
```
|
||||
|
||||
The following is executed in this function:
|
||||
|
||||
1. Check if organization exists
|
||||
2. Creates a new Internal User if the user_id or user_email is not found in LiteLLM_UserTable
|
||||
3. Add Internal User to the `LiteLLM_OrganizationMembership` table
|
||||
"""
|
||||
try:
|
||||
from litellm.proxy.proxy_server import (
|
||||
litellm_proxy_admin_name,
|
||||
prisma_client,
|
||||
proxy_logging_obj,
|
||||
user_api_key_cache,
|
||||
)
|
||||
|
||||
if prisma_client is None:
|
||||
raise HTTPException(status_code=500, detail={"error": "No db connected"})
|
||||
|
||||
# Check if organization exists
|
||||
existing_organization_row = (
|
||||
await prisma_client.db.litellm_organizationtable.find_unique(
|
||||
where={"organization_id": data.organization_id}
|
||||
)
|
||||
)
|
||||
if existing_organization_row is None:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail={
|
||||
"error": f"Organization not found for organization_id={getattr(data, 'organization_id', None)}"
|
||||
},
|
||||
)
|
||||
|
||||
members: List[Member]
|
||||
if isinstance(data.member, List):
|
||||
members = data.member
|
||||
else:
|
||||
members = [data.member]
|
||||
|
||||
updated_users: List[LiteLLM_UserTable] = []
|
||||
updated_organization_memberships: List[LiteLLM_OrganizationMembershipTable] = []
|
||||
|
||||
for member in members:
|
||||
updated_user, updated_organization_membership = (
|
||||
await add_member_to_organization(
|
||||
member=member,
|
||||
organization_id=data.organization_id,
|
||||
prisma_client=prisma_client,
|
||||
)
|
||||
)
|
||||
|
||||
updated_users.append(updated_user)
|
||||
updated_organization_memberships.append(updated_organization_membership)
|
||||
|
||||
return OrganizationAddMemberResponse(
|
||||
organization_id=data.organization_id,
|
||||
updated_users=updated_users,
|
||||
updated_organization_memberships=updated_organization_memberships,
|
||||
)
|
||||
except Exception as e:
|
||||
if isinstance(e, HTTPException):
|
||||
raise ProxyException(
|
||||
message=getattr(e, "detail", f"Authentication Error({str(e)})"),
|
||||
type=ProxyErrorTypes.auth_error,
|
||||
param=getattr(e, "param", "None"),
|
||||
code=getattr(e, "status_code", status.HTTP_500_INTERNAL_SERVER_ERROR),
|
||||
)
|
||||
elif isinstance(e, ProxyException):
|
||||
raise e
|
||||
raise ProxyException(
|
||||
message="Authentication Error, " + str(e),
|
||||
type=ProxyErrorTypes.auth_error,
|
||||
param=getattr(e, "param", "None"),
|
||||
code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
)
|
||||
|
||||
|
||||
async def add_member_to_organization(
|
||||
member: Member,
|
||||
organization_id: str,
|
||||
prisma_client: PrismaClient,
|
||||
) -> Tuple[LiteLLM_UserTable, LiteLLM_OrganizationMembershipTable]:
|
||||
"""
|
||||
Add a member to an organization
|
||||
|
||||
- Checks if member.user_id or member.user_email is in LiteLLM_UserTable
|
||||
- If not found, create a new user in LiteLLM_UserTable
|
||||
- Add user to organization in LiteLLM_OrganizationMembership
|
||||
"""
|
||||
|
||||
try:
|
||||
user_object: Optional[LiteLLM_UserTable] = None
|
||||
existing_user_id_row = None
|
||||
existing_user_email_row = None
|
||||
## Check if user exists in LiteLLM_UserTable - user exists - either the user_id or user_email is in LiteLLM_UserTable
|
||||
if member.user_id is not None:
|
||||
existing_user_id_row = await prisma_client.db.litellm_usertable.find_unique(
|
||||
where={"user_id": member.user_id}
|
||||
)
|
||||
|
||||
if member.user_email is not None:
|
||||
existing_user_email_row = (
|
||||
await prisma_client.db.litellm_usertable.find_unique(
|
||||
where={"user_email": member.user_email}
|
||||
)
|
||||
)
|
||||
|
||||
## If user does not exist, create a new user
|
||||
if existing_user_id_row is None and existing_user_email_row is None:
|
||||
# Create a new user - since user does not exist
|
||||
user_id: str = member.user_id or str(uuid.uuid4())
|
||||
new_user_defaults = get_new_internal_user_defaults(
|
||||
user_id=user_id,
|
||||
user_email=member.user_email,
|
||||
)
|
||||
|
||||
_returned_user = await prisma_client.insert_data(data=new_user_defaults, table_name="user") # type: ignore
|
||||
if _returned_user is not None:
|
||||
user_object = LiteLLM_UserTable(**_returned_user.model_dump())
|
||||
elif existing_user_email_row is not None and len(existing_user_email_row) > 1:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail={
|
||||
"error": "Multiple users with this email found in db. Please use 'user_id' instead."
|
||||
},
|
||||
)
|
||||
elif existing_user_email_row is not None:
|
||||
user_object = LiteLLM_UserTable(**existing_user_email_row.model_dump())
|
||||
elif existing_user_id_row is not None:
|
||||
user_object = LiteLLM_UserTable(**existing_user_id_row.model_dump())
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail={
|
||||
"error": f"User not found for user_id={member.user_id} and user_email={member.user_email}"
|
||||
},
|
||||
)
|
||||
|
||||
if user_object is None:
|
||||
raise ValueError(
|
||||
f"User does not exist in LiteLLM_UserTable. user_id={member.user_id} and user_email={member.user_email}"
|
||||
)
|
||||
|
||||
# Add user to organization
|
||||
_organization_membership = (
|
||||
await prisma_client.db.litellm_organizationmembership.create(
|
||||
data={
|
||||
"organization_id": organization_id,
|
||||
"user_id": user_object.user_id,
|
||||
"user_role": member.role,
|
||||
}
|
||||
)
|
||||
)
|
||||
organization_membership = LiteLLM_OrganizationMembershipTable(
|
||||
**_organization_membership.model_dump()
|
||||
)
|
||||
return user_object, organization_membership
|
||||
|
||||
except Exception as e:
|
||||
raise ValueError(f"Error adding member to organization: {e}")
|
Loading…
Add table
Add a link
Reference in a new issue