From 2eaa0079f25a4bbc4a8a0e2a1b0c01cd2148dcff Mon Sep 17 00:00:00 2001 From: Krish Dholakia Date: Tue, 28 Jan 2025 16:27:06 -0800 Subject: [PATCH] =?UTF-8?q?feat(handle=5Fjwt.py):=20initial=20commit=20add?= =?UTF-8?q?ing=20custom=20RBAC=20support=20on=20jwt=E2=80=A6=20(#8037)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(handle_jwt.py): initial commit adding custom RBAC support on jwt auth allows admin to define user role field and allowed roles which map to 'internal_user' on litellm * fix(auth_checks.py): ensure user allowed to access model, when calling via personal keys Fixes https://github.com/BerriAI/litellm/issues/8029 * feat(handle_jwt.py): support role based access with model permission control on proxy Allows admin to just grant users roles on IDP (e.g. Azure AD/Keycloak) and user can immediately start calling models * docs(rbac): add docs on rbac for model access control make it clear how admin can use roles to control model access on proxy * fix: fix linting errors * test(test_user_api_key_auth.py): add unit testing to ensure rbac role is correctly enforced * test(test_user_api_key_auth.py): add more testing * test(test_users.py): add unit testing to ensure user model access is always checked for new keys Resolves https://github.com/BerriAI/litellm/issues/8029 * test: fix unit test * fix(dot_notation_indexing.py): fix typing to work with python 3.8 --- docs/my-website/docs/proxy/jwt_auth_arch.md | 116 +++++++++++++ docs/my-website/docs/proxy/model_access.md | 3 + docs/my-website/docs/proxy/token_auth.md | 20 ++- .../img/control_model_access_jwt.png | Bin 0 -> 116105 bytes docs/my-website/sidebars.js | 2 +- .../dot_notation_indexing.py | 59 +++++++ litellm/proxy/_new_secret_config.yaml | 24 ++- litellm/proxy/_types.py | 22 +++ litellm/proxy/auth/auth_checks.py | 162 +++++++++++++----- litellm/proxy/auth/handle_jwt.py | 47 ++++- litellm/proxy/auth/user_api_key_auth.py | 74 +++++--- tests/proxy_unit_tests/test_auth_checks.py | 40 +++++ .../test_user_api_key_auth.py | 54 ++++++ tests/test_users.py | 109 +++++++++++- 14 files changed, 648 insertions(+), 84 deletions(-) create mode 100644 docs/my-website/docs/proxy/jwt_auth_arch.md create mode 100644 docs/my-website/img/control_model_access_jwt.png create mode 100644 litellm/litellm_core_utils/dot_notation_indexing.py diff --git a/docs/my-website/docs/proxy/jwt_auth_arch.md b/docs/my-website/docs/proxy/jwt_auth_arch.md new file mode 100644 index 0000000000..e48fa71f8b --- /dev/null +++ b/docs/my-website/docs/proxy/jwt_auth_arch.md @@ -0,0 +1,116 @@ +import Image from '@theme/IdealImage'; +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +# Control Model Access with SSO (Azure AD/Keycloak/etc.) + +:::info + +✨ JWT Auth is on LiteLLM Enterprise + +[Enterprise Pricing](https://www.litellm.ai/#pricing) + +[Get free 7-day trial key](https://www.litellm.ai/#trial) + +::: + + + +## Example Token + + + + +```bash +{ + "sub": "1234567890", + "name": "John Doe", + "email": "john.doe@example.com", + "roles": ["basic_user"] # 👈 ROLE +} +``` + + + +```bash +{ + "sub": "1234567890", + "name": "John Doe", + "email": "john.doe@example.com", + "resource_access": { + "litellm-test-client-id": { + "roles": ["basic_user"] # 👈 ROLE + } + } +} +``` + + + +## Proxy Configuration + + + + +```yaml +general_settings: + enable_jwt_auth: True + litellm_jwtauth: + user_roles_jwt_field: "roles" # the field in the JWT that contains the roles + user_allowed_roles: ["basic_user"] # roles that map to an 'internal_user' role on LiteLLM + enforce_rbac: true # if true, will check if the user has the correct role to access the model + + role_permissions: # control what models are allowed for each role + - role: internal_user + models: ["anthropic-claude"] + +model_list: + - model: anthropic-claude + litellm_params: + model: claude-3-5-haiku-20241022 + - model: openai-gpt-4o + litellm_params: + model: gpt-4o +``` + + + + +```yaml +general_settings: + enable_jwt_auth: True + litellm_jwtauth: + user_roles_jwt_field: "resource_access.litellm-test-client-id.roles" # the field in the JWT that contains the roles + user_allowed_roles: ["basic_user"] # roles that map to an 'internal_user' role on LiteLLM + enforce_rbac: true # if true, will check if the user has the correct role to access the model + + role_permissions: # control what models are allowed for each role + - role: internal_user + models: ["anthropic-claude"] + +model_list: + - model: anthropic-claude + litellm_params: + model: claude-3-5-haiku-20241022 + - model: openai-gpt-4o + litellm_params: + model: gpt-4o +``` + + + + + +## How it works + +1. Specify JWT_PUBLIC_KEY_URL - This is the public keys endpoint of your OpenID provider. For Azure AD it's `https://login.microsoftonline.com/{tenant_id}/discovery/v2.0/keys`. For Keycloak it's `{keycloak_base_url}/realms/{your-realm}/protocol/openid-connect/certs`. + +1. Map JWT roles to LiteLLM roles - Done via `user_roles_jwt_field` and `user_allowed_roles` + - Currently just `internal_user` is supported for role mapping. +2. Specify model access: + - `role_permissions`: control what models are allowed for each role. + - `role`: the LiteLLM role to control access for. Allowed roles = ["internal_user", "proxy_admin", "team"] + - `models`: list of models that the role is allowed to access. + - `model_list`: parent list of models on the proxy. [Learn more](./configs.md#llm-configs-model_list) + +3. Model Checks: The proxy will run validation checks on the received JWT. [Code](https://github.com/BerriAI/litellm/blob/3a4f5b23b5025b87b6d969f2485cc9bc741f9ba6/litellm/proxy/auth/user_api_key_auth.py#L284) \ No newline at end of file diff --git a/docs/my-website/docs/proxy/model_access.md b/docs/my-website/docs/proxy/model_access.md index 545d74865b..854baa2edb 100644 --- a/docs/my-website/docs/proxy/model_access.md +++ b/docs/my-website/docs/proxy/model_access.md @@ -344,3 +344,6 @@ curl -i http://localhost:4000/v1/chat/completions \ + + +## [Role Based Access Control (RBAC)](./jwt_auth_arch) \ No newline at end of file diff --git a/docs/my-website/docs/proxy/token_auth.md b/docs/my-website/docs/proxy/token_auth.md index ffff2694fe..df57cadd3b 100644 --- a/docs/my-website/docs/proxy/token_auth.md +++ b/docs/my-website/docs/proxy/token_auth.md @@ -1,7 +1,7 @@ import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; -# JWT-based Auth +# SSO - JWT-based Auth Use JWT's to auth admins / projects into the proxy. @@ -183,6 +183,24 @@ Expected Scope in JWT: } ``` +### Control Model Access + +```yaml +general_settings: + enable_jwt_auth: True + litellm_jwtauth: + user_roles_jwt_field: "resource_access.litellm-test-client-id.roles" + user_allowed_roles: ["basic_user"] # roles that map to an 'internal_user' role on LiteLLM + enforce_rbac: true # if true, will check if the user has the correct role to access the model + endpoint + + role_permissions: # control what models + endpointsare allowed for each role + - role: internal_user + models: ["anthropic-claude"] +``` + + +**[Architecture Diagram (Control Model Access)](./jwt_auth_arch)** + ## Advanced - Allowed Routes Configure which routes a JWT can access via the config. diff --git a/docs/my-website/img/control_model_access_jwt.png b/docs/my-website/img/control_model_access_jwt.png new file mode 100644 index 0000000000000000000000000000000000000000..ab6cda53961f98a2056bef8888429b1b098fdcd3 GIT binary patch literal 116105 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%0AM1U5T^I0aZ+jYifzwmqR2bFNsgz-!Qs~kg9V- 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 literal 0 HcmV?d00001 diff --git a/docs/my-website/sidebars.js b/docs/my-website/sidebars.js index 29d674f3f4..d20f2a73e4 100644 --- a/docs/my-website/sidebars.js +++ b/docs/my-website/sidebars.js @@ -51,7 +51,7 @@ const sidebars = { { type: "category", label: "Architecture", - items: ["proxy/architecture", "proxy/db_info", "router_architecture", "proxy/user_management_heirarchy"], + items: ["proxy/architecture", "proxy/db_info", "router_architecture", "proxy/user_management_heirarchy", "proxy/jwt_auth_arch"], }, { type: "link", diff --git a/litellm/litellm_core_utils/dot_notation_indexing.py b/litellm/litellm_core_utils/dot_notation_indexing.py new file mode 100644 index 0000000000..fda37f6500 --- /dev/null +++ b/litellm/litellm_core_utils/dot_notation_indexing.py @@ -0,0 +1,59 @@ +""" +This file contains the logic for dot notation indexing. + +Used by JWT Auth to get the user role from the token. +""" + +from typing import Any, Dict, Optional, TypeVar + +T = TypeVar("T") + + +def get_nested_value( + data: Dict[str, Any], key_path: str, default: Optional[T] = None +) -> Optional[T]: + """ + Retrieves a value from a nested dictionary using dot notation. + + Args: + data: The dictionary to search in + key_path: The path to the value using dot notation (e.g., "a.b.c") + default: The default value to return if the path is not found + + Returns: + The value at the specified path, or the default value if not found + + Example: + >>> data = {"a": {"b": {"c": "value"}}} + >>> get_nested_value(data, "a.b.c") + 'value' + >>> get_nested_value(data, "a.b.d", "default") + 'default' + """ + if not key_path: + return default + + # Remove metadata. prefix if it exists + key_path = ( + key_path.replace("metadata.", "", 1) + if key_path.startswith("metadata.") + else key_path + ) + + # Split the key path into parts + parts = key_path.split(".") + + # Traverse through the dictionary + current: Any = data + for part in parts: + try: + current = current[part] + except (KeyError, TypeError): + return default + + # If default is None, we can return any type + if default is None: + return current + + # Otherwise, ensure the type matches the default + return current if isinstance(current, type(default)) else default diff --git a/litellm/proxy/_new_secret_config.yaml b/litellm/proxy/_new_secret_config.yaml index 983525f495..423032ac86 100644 --- a/litellm/proxy/_new_secret_config.yaml +++ b/litellm/proxy/_new_secret_config.yaml @@ -1,18 +1,16 @@ model_list: - - model_name: gpt-3.5-turbo-end-user-test + - model_name: gpt-3.5-turbo litellm_params: model: gpt-3.5-turbo - region_name: "eu" - model_info: - id: "1" - - model_name: gpt-3.5-turbo-end-user-test - litellm_params: - model: gpt-3.5-turbo - timeout: 2 - num_retries: 0 - model_name: anthropic-claude litellm_params: - model: anthropic.claude-3-sonnet-20240229-v1:0 - -litellm_settings: - callbacks: ["langsmith"] \ No newline at end of file + model: claude-3-5-haiku-20241022 + - model_name: groq/* + litellm_params: + model: groq/* + api_key: os.environ/GROQ_API_KEY + mock_response: Hi! + - model_name: deepseek/* + litellm_params: + model: deepseek/* + api_key: os.environ/DEEPSEEK_API_KEY diff --git a/litellm/proxy/_types.py b/litellm/proxy/_types.py index 5a456aec97..bf3f6b6543 100644 --- a/litellm/proxy/_types.py +++ b/litellm/proxy/_types.py @@ -445,6 +445,8 @@ class LiteLLM_JWTAuth(LiteLLMPydanticObjectBase): user_id_jwt_field: Optional[str] = None user_email_jwt_field: Optional[str] = None user_allowed_email_domain: Optional[str] = None + user_roles_jwt_field: Optional[str] = None + user_allowed_roles: Optional[List[str]] = None user_id_upsert: bool = Field( default=False, description="If user doesn't exist, upsert them into the db." ) @@ -458,11 +460,19 @@ class LiteLLM_JWTAuth(LiteLLMPydanticObjectBase): allowed_keys = self.__annotations__.keys() invalid_keys = set(kwargs.keys()) - allowed_keys + user_roles_jwt_field = kwargs.get("user_roles_jwt_field") + user_allowed_roles = kwargs.get("user_allowed_roles") if invalid_keys: raise ValueError( f"Invalid arguments provided: {', '.join(invalid_keys)}. Allowed arguments are: {', '.join(allowed_keys)}." ) + if (user_roles_jwt_field is not None and user_allowed_roles is None) or ( + user_roles_jwt_field is None and user_allowed_roles is not None + ): + raise ValueError( + "user_allowed_roles must be provided if user_roles_jwt_field is set." + ) super().__init__(**kwargs) @@ -2335,3 +2345,15 @@ class ClientSideFallbackModel(TypedDict, total=False): ALL_FALLBACK_MODEL_VALUES = Union[str, ClientSideFallbackModel] + + +RBAC_ROLES = Literal[ + LitellmUserRoles.PROXY_ADMIN, + LitellmUserRoles.TEAM, + LitellmUserRoles.INTERNAL_USER, +] + + +class RoleBasedPermissions(TypedDict): + role: Required[RBAC_ROLES] + models: Required[List[str]] diff --git a/litellm/proxy/auth/auth_checks.py b/litellm/proxy/auth/auth_checks.py index d6bbf760bd..8d0132709c 100644 --- a/litellm/proxy/auth/auth_checks.py +++ b/litellm/proxy/auth/auth_checks.py @@ -12,7 +12,7 @@ import asyncio import re import time import traceback -from typing import TYPE_CHECKING, Any, Dict, List, Literal, Optional +from typing import TYPE_CHECKING, Any, Dict, List, Literal, Optional, cast from fastapi import status from pydantic import BaseModel @@ -24,6 +24,7 @@ from litellm.caching.dual_cache import LimitedSizeOrderedDict from litellm.litellm_core_utils.get_llm_provider_logic import get_llm_provider from litellm.proxy._types import ( DB_CONNECTION_ERROR_TYPES, + RBAC_ROLES, CallInfo, LiteLLM_EndUserTable, LiteLLM_JWTAuth, @@ -35,6 +36,7 @@ from litellm.proxy._types import ( LitellmUserRoles, ProxyErrorTypes, ProxyException, + RoleBasedPermissions, UserAPIKeyAuth, ) from litellm.proxy.auth.route_checks import RouteChecks @@ -100,6 +102,14 @@ async def common_checks( llm_router=llm_router, ) + ## 2.1 If user can call model (if personal key) + if team_object is None and user_object is not None: + await can_user_call_model( + model=_model, + llm_router=llm_router, + user_object=user_object, + ) + # 3. If team is in budget await _team_max_budget_check( team_object=team_object, @@ -391,6 +401,30 @@ def _update_last_db_access_time( last_db_access_time[key] = (value, time.time()) +def get_role_based_models( + rbac_role: RBAC_ROLES, + general_settings: dict, +) -> Optional[List[str]]: + """ + Get the models allowed for a user role. + + Used by JWT Auth. + """ + + role_based_permissions = cast( + Optional[List[RoleBasedPermissions]], + general_settings.get("role_permissions", []), + ) + if role_based_permissions is None: + return None + + for role_based_permission in role_based_permissions: + if role_based_permission["role"] == rbac_role: + return role_based_permission["models"] + + return None + + @log_db_metrics async def get_user_object( user_id: str, @@ -836,6 +870,68 @@ async def get_org_object( ) +async def _can_object_call_model( + model: str, + llm_router: Optional[Router], + models: List[str], +) -> Literal[True]: + """ + Checks if token can call a given model + + Returns: + - True: if token allowed to call model + + Raises: + - Exception: If token not allowed to call model + """ + if model in litellm.model_alias_map: + model = litellm.model_alias_map[model] + + ## check if model in allowed model names + from collections import defaultdict + + access_groups: Dict[str, List[str]] = defaultdict(list) + + if llm_router: + access_groups = llm_router.get_model_access_groups(model_name=model) + if ( + len(access_groups) > 0 and llm_router is not None + ): # check if token contains any model access groups + for idx, m in enumerate( + models + ): # loop token models, if any of them are an access group add the access group + if m in access_groups: + return True + + # Filter out models that are access_groups + filtered_models = [m for m in models if m not in access_groups] + + verbose_proxy_logger.debug(f"model: {model}; allowed_models: {filtered_models}") + + if _model_matches_any_wildcard_pattern_in_list( + model=model, allowed_model_list=filtered_models + ): + return True + + all_model_access: bool = False + + if (len(filtered_models) == 0 and len(models) == 0) or "*" in filtered_models: + all_model_access = True + + if model is not None and model not in filtered_models and all_model_access is False: + raise ProxyException( + message=f"API Key not allowed to access model. This token can only access models={models}. Tried to access {model}", + type=ProxyErrorTypes.key_model_access_denied, + param="model", + code=status.HTTP_401_UNAUTHORIZED, + ) + + verbose_proxy_logger.debug( + f"filtered allowed_models: {filtered_models}; models: {models}" + ) + return True + + async def can_key_call_model( model: str, llm_model_list: Optional[list], @@ -851,57 +947,27 @@ async def can_key_call_model( Raises: - Exception: If token not allowed to call model """ - if model in litellm.model_alias_map: - model = litellm.model_alias_map[model] - - ## check if model in allowed model names - verbose_proxy_logger.debug( - f"LLM Model List pre access group check: {llm_model_list}" + return await _can_object_call_model( + model=model, + llm_router=llm_router, + models=valid_token.models, ) - from collections import defaultdict - access_groups: Dict[str, List[str]] = defaultdict(list) - if llm_router: - access_groups = llm_router.get_model_access_groups(model_name=model) - if ( - len(access_groups) > 0 and llm_router is not None - ): # check if token contains any model access groups - for idx, m in enumerate( - valid_token.models - ): # loop token models, if any of them are an access group add the access group - if m in access_groups: - return True +async def can_user_call_model( + model: str, + llm_router: Optional[Router], + user_object: Optional[LiteLLM_UserTable], +) -> Literal[True]: - # Filter out models that are access_groups - filtered_models = [m for m in valid_token.models if m not in access_groups] - - verbose_proxy_logger.debug(f"model: {model}; allowed_models: {filtered_models}") - - if _model_matches_any_wildcard_pattern_in_list( - model=model, allowed_model_list=filtered_models - ): + if user_object is None: return True - all_model_access: bool = False - - if ( - len(filtered_models) == 0 and len(valid_token.models) == 0 - ) or "*" in filtered_models: - all_model_access = True - - if model is not None and model not in filtered_models and all_model_access is False: - raise ProxyException( - message=f"API Key not allowed to access model. This token can only access models={valid_token.models}. Tried to access {model}", - type=ProxyErrorTypes.key_model_access_denied, - param="model", - code=status.HTTP_401_UNAUTHORIZED, - ) - valid_token.models = filtered_models - verbose_proxy_logger.debug( - f"filtered allowed_models: {filtered_models}; valid_token.models: {valid_token.models}" + return await _can_object_call_model( + model=model, + llm_router=llm_router, + models=user_object.models, ) - return True async def is_valid_fallback_model( @@ -1161,7 +1227,11 @@ def _model_custom_llm_provider_matches_wildcard_pattern( - `model=claude-3-5-sonnet-20240620` - `allowed_model_pattern=anthropic/*` """ - model, custom_llm_provider, _, _ = get_llm_provider(model=model) + try: + model, custom_llm_provider, _, _ = get_llm_provider(model=model) + except Exception: + return False + return is_model_allowed_by_pattern( model=f"{custom_llm_provider}/{model}", allowed_model_pattern=allowed_model_pattern, diff --git a/litellm/proxy/auth/handle_jwt.py b/litellm/proxy/auth/handle_jwt.py index cf57011546..bcda413b68 100644 --- a/litellm/proxy/auth/handle_jwt.py +++ b/litellm/proxy/auth/handle_jwt.py @@ -16,8 +16,10 @@ from cryptography.hazmat.primitives import serialization from litellm._logging import verbose_proxy_logger from litellm.caching.caching import DualCache +from litellm.litellm_core_utils.dot_notation_indexing import get_nested_value from litellm.llms.custom_httpx.httpx_handler import HTTPHandler from litellm.proxy._types import ( + RBAC_ROLES, JWKKeyValue, JWTKeyItem, LiteLLM_JWTAuth, @@ -59,7 +61,7 @@ class JWTHandler: parts = token.split(".") return len(parts) == 3 - def get_rbac_role(self, token: dict) -> Optional[LitellmUserRoles]: + def get_rbac_role(self, token: dict) -> Optional[RBAC_ROLES]: """ Returns the RBAC role the token 'belongs' to. @@ -78,12 +80,18 @@ class JWTHandler: """ scopes = self.get_scopes(token=token) is_admin = self.is_admin(scopes=scopes) + user_roles = self.get_user_roles(token=token, default_value=None) + if is_admin: return LitellmUserRoles.PROXY_ADMIN elif self.get_team_id(token=token, default_value=None) is not None: return LitellmUserRoles.TEAM elif self.get_user_id(token=token, default_value=None) is not None: return LitellmUserRoles.INTERNAL_USER + elif user_roles is not None and self.is_allowed_user_role( + user_roles=user_roles + ): + return LitellmUserRoles.INTERNAL_USER return None @@ -166,6 +174,43 @@ class JWTHandler: user_id = default_value return user_id + def get_user_roles( + self, token: dict, default_value: Optional[List[str]] + ) -> Optional[List[str]]: + """ + Returns the user role from the token. + + Set via 'user_roles_jwt_field' in the config. + """ + try: + if self.litellm_jwtauth.user_roles_jwt_field is not None: + user_roles = get_nested_value( + data=token, + key_path=self.litellm_jwtauth.user_roles_jwt_field, + default=default_value, + ) + else: + user_roles = default_value + except KeyError: + user_roles = default_value + return user_roles + + def is_allowed_user_role(self, user_roles: Optional[List[str]]) -> bool: + """ + Returns the user role from the token. + + Set via 'user_allowed_roles' in the config. + """ + if ( + user_roles is not None + and self.litellm_jwtauth.user_allowed_roles is not None + and any( + role in self.litellm_jwtauth.user_allowed_roles for role in user_roles + ) + ): + return True + return False + def get_user_email( self, token: dict, default_value: Optional[str] ) -> Optional[str]: diff --git a/litellm/proxy/auth/user_api_key_auth.py b/litellm/proxy/auth/user_api_key_auth.py index 33247308f6..7d499af5b2 100644 --- a/litellm/proxy/auth/user_api_key_auth.py +++ b/litellm/proxy/auth/user_api_key_auth.py @@ -33,6 +33,7 @@ from litellm.proxy.auth.auth_checks import ( get_end_user_object, get_key_object, get_org_object, + get_role_based_models, get_team_object, get_user_object, is_valid_fallback_model, @@ -281,9 +282,34 @@ def get_rbac_role(jwt_handler: JWTHandler, scopes: List[str]) -> str: return LitellmUserRoles.TEAM +def can_rbac_role_call_model( + rbac_role: RBAC_ROLES, + general_settings: dict, + model: Optional[str], +) -> Literal[True]: + """ + Checks if user is allowed to access the model, based on their role. + """ + role_based_models = get_role_based_models( + rbac_role=rbac_role, general_settings=general_settings + ) + if role_based_models is None or model is None: + return True + + if model not in role_based_models: + raise HTTPException( + status_code=403, + detail=f"User role={rbac_role} not allowed to call model={model}. Allowed models={role_based_models}", + ) + + return True + + async def _jwt_auth_user_api_key_auth_builder( api_key: str, jwt_handler: JWTHandler, + request_data: dict, + general_settings: dict, route: str, prisma_client: Optional[PrismaClient], user_api_key_cache: DualCache, @@ -295,14 +321,20 @@ async def _jwt_auth_user_api_key_auth_builder( jwt_valid_token: dict = await jwt_handler.auth_jwt(token=api_key) # check if unmatched token and enforce_rbac is true - if ( - jwt_handler.litellm_jwtauth.enforce_rbac is True - and jwt_handler.get_rbac_role(token=jwt_valid_token) is None - ): - raise HTTPException( - status_code=403, - detail="Unmatched token passed in. enforce_rbac is set to True. Token must belong to a proxy admin, team, or user. See how to set roles in config here: https://docs.litellm.ai/docs/proxy/token_auth#advanced---spend-tracking-end-users--internal-users--team--org", - ) + if jwt_handler.litellm_jwtauth.enforce_rbac is True: + rbac_role = jwt_handler.get_rbac_role(token=jwt_valid_token) + if rbac_role is None: + raise HTTPException( + status_code=403, + detail="Unmatched token passed in. enforce_rbac is set to True. Token must belong to a proxy admin, team, or user. See how to set roles in config here: https://docs.litellm.ai/docs/proxy/token_auth#advanced---spend-tracking-end-users--internal-users--team--org", + ) + else: + # run rbac validation checks + can_rbac_role_call_model( + rbac_role=rbac_role, + general_settings=general_settings, + model=request_data.get("model"), + ) # get scopes scopes = jwt_handler.get_scopes(token=jwt_valid_token) @@ -431,18 +463,18 @@ async def _jwt_auth_user_api_key_auth_builder( proxy_logging_obj=proxy_logging_obj, ) - return { - "is_proxy_admin": False, - "team_id": team_id, - "team_object": team_object, - "user_id": user_id, - "user_object": user_object, - "org_id": org_id, - "org_object": org_object, - "end_user_id": end_user_id, - "end_user_object": end_user_object, - "token": api_key, - } + return JWTAuthBuilderResult( + is_proxy_admin=False, + team_id=team_id, + team_object=team_object, + user_id=user_id, + user_object=user_object, + org_id=org_id, + org_object=org_object, + end_user_id=end_user_id, + end_user_object=end_user_object, + token=api_key, + ) async def _user_api_key_auth_builder( # noqa: PLR0915 @@ -581,6 +613,8 @@ async def _user_api_key_auth_builder( # noqa: PLR0915 verbose_proxy_logger.debug("is_jwt: %s", is_jwt) if is_jwt: result = await _jwt_auth_user_api_key_auth_builder( + request_data=request_data, + general_settings=general_settings, api_key=api_key, jwt_handler=jwt_handler, route=route, diff --git a/tests/proxy_unit_tests/test_auth_checks.py b/tests/proxy_unit_tests/test_auth_checks.py index 85b5b216a5..04af3d6e29 100644 --- a/tests/proxy_unit_tests/test_auth_checks.py +++ b/tests/proxy_unit_tests/test_auth_checks.py @@ -508,3 +508,43 @@ async def test_virtual_key_soft_budget_check(spend, soft_budget, expect_alert): assert ( alert_triggered == expect_alert ), f"Expected alert_triggered to be {expect_alert} for spend={spend}, soft_budget={soft_budget}" + + +@pytest.mark.asyncio +async def test_can_user_call_model(): + from litellm.proxy.auth.auth_checks import can_user_call_model + from litellm.proxy._types import ProxyException + from litellm import Router + + router = Router( + model_list=[ + { + "model_name": "anthropic-claude", + "litellm_params": {"model": "anthropic/anthropic-claude"}, + }, + { + "model_name": "gpt-3.5-turbo", + "litellm_params": {"model": "gpt-3.5-turbo", "api_key": "test-api-key"}, + }, + ] + ) + + args = { + "model": "anthropic-claude", + "llm_router": router, + "user_object": LiteLLM_UserTable( + user_id="testuser21@mycompany.com", + max_budget=None, + spend=0.0042295, + model_max_budget={}, + model_spend={}, + user_email="testuser@mycompany.com", + models=["gpt-3.5-turbo"], + ), + } + + with pytest.raises(ProxyException) as e: + await can_user_call_model(**args) + + args["model"] = "gpt-3.5-turbo" + await can_user_call_model(**args) diff --git a/tests/proxy_unit_tests/test_user_api_key_auth.py b/tests/proxy_unit_tests/test_user_api_key_auth.py index a428a29c63..3e9ba17889 100644 --- a/tests/proxy_unit_tests/test_user_api_key_auth.py +++ b/tests/proxy_unit_tests/test_user_api_key_auth.py @@ -855,6 +855,8 @@ async def test_jwt_user_api_key_auth_builder_enforce_rbac(enforce_rbac, monkeypa "user_api_key_cache": Mock(), "parent_otel_span": None, "proxy_logging_obj": Mock(), + "request_data": {}, + "general_settings": {}, } if enforce_rbac: @@ -877,3 +879,55 @@ def test_user_api_key_auth_end_user_str(): user_api_key_auth = UserAPIKeyAuth(**user_api_key_args) assert user_api_key_auth.end_user_id == "1" + + +def test_can_rbac_role_call_model(): + from litellm.proxy.auth.user_api_key_auth import can_rbac_role_call_model + from litellm.proxy._types import RoleBasedPermissions + + roles_based_permissions = [ + RoleBasedPermissions( + role=LitellmUserRoles.INTERNAL_USER, + models=["gpt-4"], + ), + RoleBasedPermissions( + role=LitellmUserRoles.PROXY_ADMIN, + models=["anthropic-claude"], + ), + ] + + assert can_rbac_role_call_model( + rbac_role=LitellmUserRoles.INTERNAL_USER, + general_settings={"role_permissions": roles_based_permissions}, + model="gpt-4", + ) + + with pytest.raises(HTTPException): + can_rbac_role_call_model( + rbac_role=LitellmUserRoles.INTERNAL_USER, + general_settings={"role_permissions": roles_based_permissions}, + model="gpt-4o", + ) + + with pytest.raises(HTTPException): + can_rbac_role_call_model( + rbac_role=LitellmUserRoles.PROXY_ADMIN, + general_settings={"role_permissions": roles_based_permissions}, + model="gpt-4o", + ) + + +def test_can_rbac_role_call_model_no_role_permissions(): + from litellm.proxy.auth.user_api_key_auth import can_rbac_role_call_model + + assert can_rbac_role_call_model( + rbac_role=LitellmUserRoles.INTERNAL_USER, + general_settings={}, + model="gpt-4", + ) + + assert can_rbac_role_call_model( + rbac_role=LitellmUserRoles.PROXY_ADMIN, + general_settings={"role_permissions": []}, + model="anthropic-claude", + ) diff --git a/tests/test_users.py b/tests/test_users.py index 7e267ac4df..812783681c 100644 --- a/tests/test_users.py +++ b/tests/test_users.py @@ -7,13 +7,17 @@ import time from openai import AsyncOpenAI from test_team import list_teams from typing import Optional +from test_keys import generate_key +from fastapi import HTTPException -async def new_user(session, i, user_id=None, budget=None, budget_duration=None): +async def new_user( + session, i, user_id=None, budget=None, budget_duration=None, models=None +): url = "http://0.0.0.0:4000/user/new" headers = {"Authorization": "Bearer sk-1234", "Content-Type": "application/json"} data = { - "models": ["azure-models"], + "models": models or ["azure-models"], "aliases": {"mistral-7b": "gpt-3.5-turbo"}, "duration": None, "max_budget": budget, @@ -37,6 +41,51 @@ async def new_user(session, i, user_id=None, budget=None, budget_duration=None): return await response.json() +async def generate_key( + session, + i, + budget=None, + budget_duration=None, + models=["azure-models", "gpt-4", "dall-e-3"], + max_parallel_requests: Optional[int] = None, + user_id: Optional[str] = None, + team_id: Optional[str] = None, + metadata: Optional[dict] = None, + calling_key="sk-1234", +): + url = "http://0.0.0.0:4000/key/generate" + headers = { + "Authorization": f"Bearer {calling_key}", + "Content-Type": "application/json", + } + data = { + "models": models, + "aliases": {"mistral-7b": "gpt-3.5-turbo"}, + "duration": None, + "max_budget": budget, + "budget_duration": budget_duration, + "max_parallel_requests": max_parallel_requests, + "user_id": user_id, + "team_id": team_id, + "metadata": metadata, + } + + print(f"data: {data}") + + async with session.post(url, headers=headers, json=data) as response: + status = response.status + response_text = await response.text() + + print(f"Response {i} (Status code: {status}):") + print(response_text) + print() + + if status != 200: + raise Exception(f"Request {i} did not return a 200 status code: {status}") + + return await response.json() + + @pytest.mark.asyncio async def test_user_new(): """ @@ -210,3 +259,59 @@ async def test_global_proxy_budget_update(): new_new_spend = user_info["user_info"]["spend"] print(f"new_spend: {new_spend}; original_spend: {original_spend}") assert new_new_spend > new_spend + + +@pytest.mark.asyncio +async def test_user_model_access(): + """ + - Create user with model access + - Create key with user + - Call model that user has access to -> should work + - Call wildcard model that user has access to -> should work + - Call model that user does not have access to -> should fail + - Call wildcard model that user does not have access to -> should fail + """ + import openai + + async with aiohttp.ClientSession() as session: + get_user = f"krrish_{time.time()}@berri.ai" + await new_user( + session=session, + i=0, + user_id=get_user, + models=["good-model", "anthropic/*"], + ) + + result = await generate_key( + session=session, + i=0, + user_id=get_user, + models=[], # assign no models. Allow inheritance from user + ) + key = result["key"] + + await chat_completion( + session=session, + key=key, + model="anthropic/claude-3-5-haiku-20241022", + ) + + await chat_completion( + session=session, + key=key, + model="good-model", + ) + + with pytest.raises(openai.AuthenticationError): + await chat_completion( + session=session, + key=key, + model="bedrock/anthropic.claude-3-sonnet-20240229-v1:0", + ) + + with pytest.raises(openai.AuthenticationError): + await chat_completion( + session=session, + key=key, + model="groq/claude-3-5-haiku-20241022", + )