diff --git a/litellm/proxy/_new_secret_config.yaml b/litellm/proxy/_new_secret_config.yaml index b38272560d1..dc1468e5661 100644 --- a/litellm/proxy/_new_secret_config.yaml +++ b/litellm/proxy/_new_secret_config.yaml @@ -4,6 +4,9 @@ model_list: model: openai/fake api_key: fake-key api_base: https://exampleopenaiendpoint-production.up.railway.app/ + - model_name: wildcard_models/* + litellm_params: + model: openai/* - model_name: gpt-5-mini litellm_params: model: azure/gpt-5-mini @@ -24,4 +27,4 @@ router_settings: litellm_settings: callbacks: ["otel"] - success_callback: ["braintrust"] + success_callback: ["braintrust"] \ No newline at end of file diff --git a/litellm/proxy/auth/route_checks.py b/litellm/proxy/auth/route_checks.py index 3774cdfb810..56218b3345e 100644 --- a/litellm/proxy/auth/route_checks.py +++ b/litellm/proxy/auth/route_checks.py @@ -300,7 +300,7 @@ class RouteChecks: if re.match(pattern, route): return True return False - + @staticmethod def _is_wildcard_pattern(pattern: str) -> bool: """ @@ -354,16 +354,22 @@ class RouteChecks: ######################################################### if route in allowed_routes: return True - + ######################################################### # wildcard match route is in allowed_routes # e.g calling /anthropic/v1/messages is allowed if allowed_routes has /anthropic/* ######################################################### - wildcard_allowed_routes = [route for route in allowed_routes if RouteChecks._is_wildcard_pattern(pattern=route)] + wildcard_allowed_routes = [ + route + for route in allowed_routes + if RouteChecks._is_wildcard_pattern(pattern=route) + ] for allowed_route in wildcard_allowed_routes: - if RouteChecks._route_matches_wildcard_pattern(route=route, pattern=allowed_route): + if RouteChecks._route_matches_wildcard_pattern( + route=route, pattern=allowed_route + ): return True - + ######################################################### # pattern match route is in allowed_routes # pattern: "/threads/{thread_id}" @@ -375,7 +381,7 @@ class RouteChecks: for allowed_route in allowed_routes ): return True - + return False @staticmethod @@ -420,7 +426,7 @@ class RouteChecks: status_code=status.HTTP_403_FORBIDDEN, detail=f"user not allowed to access this OpenAI routes, role= {_user_role}", ) - + # Check if this is a write operation on management routes if RouteChecks.check_route_access( route=route, allowed_routes=LiteLLMRoutes.management_routes.value @@ -436,7 +442,28 @@ class RouteChecks: status_code=status.HTTP_403_FORBIDDEN, detail=f"user not allowed to access this route, role= {_user_role}. Trying to access: {route} and updating invalid param: {param}. only user_email and password can be updated", ) - elif route in ["/user/new", "/user/delete", "/team/new", "/team/update", "/team/delete", "/model/new", "/model/update", "/model/delete", "/key/generate", "/key/delete", "/key/update", "/key/regenerate", "/key/service-account/generate", "/key/block", "/key/unblock"] or route.startswith("/key/") and route.endswith("/regenerate"): + elif ( + route + in [ + "/user/new", + "/user/delete", + "/team/new", + "/team/update", + "/team/delete", + "/model/new", + "/model/update", + "/model/delete", + "/key/generate", + "/key/delete", + "/key/update", + "/key/regenerate", + "/key/service-account/generate", + "/key/block", + "/key/unblock", + ] + or route.startswith("/key/") + and route.endswith("/regenerate") + ): # Block write operations for PROXY_ADMIN_VIEW_ONLY raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, diff --git a/litellm/proxy/management_endpoints/internal_user_endpoints.py b/litellm/proxy/management_endpoints/internal_user_endpoints.py index 3876230f5ab..68e6342d6b6 100644 --- a/litellm/proxy/management_endpoints/internal_user_endpoints.py +++ b/litellm/proxy/management_endpoints/internal_user_endpoints.py @@ -835,6 +835,16 @@ async def _update_single_user_helper( existing_user_row = LiteLLM_UserTable( **existing_user_row.model_dump(exclude_none=True) ) + if not can_user_call_user_update( + user_api_key_dict=user_api_key_dict, + user_info=existing_user_row, + ): + raise HTTPException( + status_code=403, + detail={ + "error": "User does not have permission to update this user. Only PROXY_ADMIN can update other users." + }, + ) existing_metadata = ( cast(Dict, getattr(existing_user_row, "metadata", {}) or {}) @@ -929,6 +939,20 @@ async def _update_single_user_helper( return response +def can_user_call_user_update( + user_api_key_dict: UserAPIKeyAuth, + user_info: LiteLLM_UserTable, +) -> bool: + """ + Helper to check if the user has access to the key's info + """ + if user_api_key_dict.user_role == LitellmUserRoles.PROXY_ADMIN.value: + return True + elif user_api_key_dict.user_id == user_info.user_id: + return True + return False + + @router.post( "/user/update", tags=["Internal User management"], @@ -988,6 +1012,7 @@ async def user_update( """ try: verbose_proxy_logger.debug("/user/update: Received data = %s", data) + response = await _update_single_user_helper( user_request=data, user_api_key_dict=user_api_key_dict, diff --git a/tests/llm_translation/test_azure_openai.py b/tests/llm_translation/test_azure_openai.py index 59ca5cf61b8..628702544f0 100644 --- a/tests/llm_translation/test_azure_openai.py +++ b/tests/llm_translation/test_azure_openai.py @@ -171,14 +171,17 @@ def test_azure_extra_headers(input, call_type, header_value): "api_base, model, expected_endpoint", [ ( - "https://my-endpoint-sweden-berri992.openai.azure.com", + os.getenv("AZURE_SWEDEN_API_BASE"), "dall-e-3-test", - "https://my-endpoint-sweden-berri992.openai.azure.com/openai/deployments/dall-e-3-test/images/generations?api-version=2023-12-01-preview", + os.getenv("AZURE_SWEDEN_API_BASE") + + "/openai/deployments/dall-e-3-test/images/generations?api-version=2023-12-01-preview", ), ( - "https://my-endpoint-sweden-berri992.openai.azure.com/openai/deployments/my-custom-deployment", + os.getenv("AZURE_SWEDEN_API_BASE") + + "/openai/deployments/my-custom-deployment", "dall-e-3", - "https://my-endpoint-sweden-berri992.openai.azure.com/openai/deployments/my-custom-deployment/images/generations?api-version=2023-12-01-preview", + os.getenv("AZURE_SWEDEN_API_BASE") + + "/openai/deployments/my-custom-deployment/images/generations?api-version=2023-12-01-preview", ), ], ) @@ -258,7 +261,7 @@ def test_azure_openai_gpt_4o_naming(monkeypatch): client = AzureOpenAI( api_key="test-api-key", - base_url="https://my-endpoint-sweden-berri992.openai.azure.com", + base_url=os.getenv("AZURE_SWEDEN_API_BASE"), api_version="2023-12-01-preview", ) diff --git a/tests/proxy_unit_tests/test_db_schema_migration.py b/tests/proxy_unit_tests/test_db_schema_migration.py index b3178183759..420592431ea 100644 --- a/tests/proxy_unit_tests/test_db_schema_migration.py +++ b/tests/proxy_unit_tests/test_db_schema_migration.py @@ -21,7 +21,6 @@ def test_aaaasschema_migration_check(schema_setup, monkeypatch): """Test to check if schema requires migration""" # Set test database URL test_db_url = f"postgresql://{schema_setup.info.user}:@{schema_setup.info.host}:{schema_setup.info.port}/{schema_setup.info.dbname}" - # test_db_url = "postgresql://neondb_owner:npg_JiZPS0DAhRn4@ep-delicate-wave-a55cvbuc.us-east-2.aws.neon.tech/neondb?sslmode=require" monkeypatch.setenv("DATABASE_URL", test_db_url) deploy_dir = Path("./litellm-proxy-extras/litellm_proxy_extras") diff --git a/tests/proxy_unit_tests/test_key_generate_prisma.py b/tests/proxy_unit_tests/test_key_generate_prisma.py index 983f68b89ee..c196f030159 100644 --- a/tests/proxy_unit_tests/test_key_generate_prisma.py +++ b/tests/proxy_unit_tests/test_key_generate_prisma.py @@ -2642,7 +2642,12 @@ async def test_reset_spend_authentication(prisma_client): _response = await new_user( data=NewUserRequest( tpm_limit=20, - ) + ), + user_api_key_dict=UserAPIKeyAuth( + user_role=LitellmUserRoles.PROXY_ADMIN, + api_key="sk-1234", + user_id="admin_user_id", + ), ) generate_key = "Bearer " + _response.key @@ -2662,7 +2667,12 @@ async def test_reset_spend_authentication(prisma_client): data=NewUserRequest( user_role=LitellmUserRoles.PROXY_ADMIN, tpm_limit=20, - ) + ), + user_api_key_dict=UserAPIKeyAuth( + user_role=LitellmUserRoles.PROXY_ADMIN, + api_key="sk-1234", + user_id="admin_user_id", + ), ) generate_key = "Bearer " + _response.key @@ -2815,7 +2825,12 @@ async def test_update_user_role(prisma_client): key = await new_user( data=NewUserRequest( user_role=LitellmUserRoles.INTERNAL_USER, - ) + ), + user_api_key_dict=UserAPIKeyAuth( + user_role=LitellmUserRoles.PROXY_ADMIN, + api_key="sk-1234", + user_id="admin_user_id", + ), ) print(key) @@ -2844,7 +2859,12 @@ async def test_update_user_role(prisma_client): await user_update( data=UpdateUserRequest( user_id=key.user_id, user_role=LitellmUserRoles.PROXY_ADMIN - ) + ), + user_api_key_dict=UserAPIKeyAuth( + user_role=LitellmUserRoles.PROXY_ADMIN, + api_key="sk-1234", + user_id="admin_user_id", + ), ) # await asyncio.sleep(3) @@ -2868,7 +2888,12 @@ async def test_update_user_unit_test(prisma_client): key = await new_user( data=NewUserRequest( user_email=f"test-{uuid.uuid4()}@test.com", - ) + ), + user_api_key_dict=UserAPIKeyAuth( + user_role=LitellmUserRoles.PROXY_ADMIN, + api_key="sk-1234", + user_id="admin_user_id", + ), ) print(key) @@ -2882,7 +2907,12 @@ async def test_update_user_unit_test(prisma_client): tpm_limit=100, rpm_limit=100, metadata={"very-new-metadata": "something"}, - ) + ), + user_api_key_dict=UserAPIKeyAuth( + user_role=LitellmUserRoles.PROXY_ADMIN, + api_key="sk-1234", + user_id="admin_user_id", + ), ) print("user_info", user_info)