mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-14 23:21:35 +00:00
feat(scim): add opt-in auto_create_keys_for_scim_users flag
Add a new litellm_settings flag 'auto_create_keys_for_scim_users' that,
when set to True, causes SCIM user provisioning to also auto-generate
an API key for the new user.
This applies to both:
1. POST /scim/v2/Users (direct SCIM user creation)
2. _create_user_if_not_exists (users created via group membership)
The flag defaults to False, preserving existing behavior where SCIM
provisioning creates user accounts without API keys.
Usage in config:
litellm_settings:
auto_create_keys_for_scim_users: true
Also refactors the config-reading logic into a shared _get_litellm_setting
helper used by both scim_upsert_user and the new flag.
Co-authored-by: Krrish Dholakia <krrish-berri-2@users.noreply.github.com>
This commit is contained in:
parent
c8fb77f119
commit
e3325193d8
2 changed files with 211 additions and 15 deletions
|
|
@ -207,6 +207,30 @@ def _build_scim_metadata(
|
|||
return metadata
|
||||
|
||||
|
||||
async def _get_litellm_setting(setting_name: str, default: bool) -> bool:
|
||||
"""
|
||||
Read a boolean flag from litellm_settings in the proxy config.
|
||||
|
||||
Args:
|
||||
setting_name: The key to look up in litellm_settings.
|
||||
default: Value returned when the key is absent or on error.
|
||||
|
||||
Returns:
|
||||
The boolean value of the setting, or *default* on missing key / error.
|
||||
"""
|
||||
try:
|
||||
from litellm.proxy.proxy_server import proxy_config
|
||||
|
||||
config = await proxy_config.get_config()
|
||||
litellm_settings = config.get("litellm_settings", {}) or {}
|
||||
return bool(litellm_settings.get(setting_name, default))
|
||||
except Exception as e:
|
||||
verbose_proxy_logger.warning(
|
||||
f"Error reading {setting_name} setting, defaulting to {default}: {e}"
|
||||
)
|
||||
return default
|
||||
|
||||
|
||||
async def _get_scim_upsert_user_setting() -> bool:
|
||||
"""
|
||||
Get the scim_upsert_user setting from litellm_settings.
|
||||
|
|
@ -215,21 +239,21 @@ async def _get_scim_upsert_user_setting() -> bool:
|
|||
True if scim_upsert_user is not set or is True (default behavior),
|
||||
False if scim_upsert_user is explicitly set to False (SCIM 2.0 strict mode)
|
||||
"""
|
||||
try:
|
||||
from litellm.proxy.proxy_server import proxy_config
|
||||
return await _get_litellm_setting("scim_upsert_user", default=True)
|
||||
|
||||
config = await proxy_config.get_config()
|
||||
litellm_settings = config.get("litellm_settings", {}) or {}
|
||||
scim_upsert_user = litellm_settings.get("scim_upsert_user", True)
|
||||
|
||||
# Default to True if not set (backward compatibility)
|
||||
return bool(scim_upsert_user)
|
||||
except Exception as e:
|
||||
verbose_proxy_logger.warning(
|
||||
f"Error reading scim_upsert_user setting, defaulting to True: {e}"
|
||||
)
|
||||
# Default to True for backward compatibility
|
||||
return True
|
||||
async def _get_auto_create_key_for_scim_user_setting() -> bool:
|
||||
"""
|
||||
Get the auto_create_keys_for_scim_users setting from litellm_settings.
|
||||
|
||||
When True, SCIM user provisioning (POST /scim/v2/Users and group-membership
|
||||
auto-creation) will also auto-generate an API key for the new user.
|
||||
|
||||
Returns:
|
||||
False by default (backward-compatible). True only when the admin
|
||||
explicitly opts in via litellm_settings.auto_create_keys_for_scim_users.
|
||||
"""
|
||||
return await _get_litellm_setting("auto_create_keys_for_scim_users", default=False)
|
||||
|
||||
|
||||
async def _extract_group_member_ids(group: SCIMGroup) -> GroupMemberExtractionResult:
|
||||
|
|
@ -364,13 +388,15 @@ async def _create_user_if_not_exists(
|
|||
if litellm.default_internal_user_params:
|
||||
default_role = litellm.default_internal_user_params.get("user_role")
|
||||
|
||||
auto_create_key = await _get_auto_create_key_for_scim_user_setting()
|
||||
|
||||
new_user_request = NewUserRequest(
|
||||
user_id=user_id,
|
||||
user_email=user_id, # We don't have email from group membership
|
||||
user_alias=None,
|
||||
teams=[], # Teams will be added separately
|
||||
metadata={"created_via": created_via},
|
||||
auto_create_key=False,
|
||||
auto_create_key=auto_create_key,
|
||||
user_role=default_role,
|
||||
)
|
||||
|
||||
|
|
@ -871,13 +897,15 @@ async def create_user(
|
|||
if litellm.default_internal_user_params:
|
||||
default_role = litellm.default_internal_user_params.get("user_role")
|
||||
|
||||
auto_create_key = await _get_auto_create_key_for_scim_user_setting()
|
||||
|
||||
new_user_request = NewUserRequest(
|
||||
user_id=user_id,
|
||||
user_email=user_data["user_email"],
|
||||
user_alias=user_data["user_alias"],
|
||||
teams=user_data["teams"],
|
||||
metadata=metadata,
|
||||
auto_create_key=False,
|
||||
auto_create_key=auto_create_key,
|
||||
user_role=default_role,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ from litellm.proxy._types import (
|
|||
)
|
||||
from litellm.proxy.management_endpoints.scim.scim_v2 import (
|
||||
UserProvisionerHelpers,
|
||||
_create_user_if_not_exists,
|
||||
_extract_group_member_ids,
|
||||
_handle_team_membership_changes,
|
||||
_process_group_patch_operations,
|
||||
|
|
@ -1601,3 +1602,170 @@ async def test_process_group_patch_operations_with_flag_false_rejects(
|
|||
assert exc_info.value.status_code == 400
|
||||
assert "does not exist" in str(exc_info.value.detail)
|
||||
assert "new-user-1" in str(exc_info.value.detail)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tests for auto_create_keys_for_scim_users setting
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_user_auto_create_key_false_by_default(mocker, monkeypatch):
|
||||
"""
|
||||
When auto_create_keys_for_scim_users is not set (default), SCIM user
|
||||
creation should pass auto_create_key=False to new_user.
|
||||
"""
|
||||
|
||||
scim_user = SCIMUser(
|
||||
schemas=["urn:ietf:params:scim:schemas:core:2.0:User"],
|
||||
userName="scim-nokey-user",
|
||||
name=SCIMUserName(familyName="User", givenName="NoKey"),
|
||||
emails=[SCIMUserEmail(value="nokey@example.com")],
|
||||
)
|
||||
|
||||
mock_prisma_client = mocker.MagicMock()
|
||||
mock_prisma_client.db = mocker.MagicMock()
|
||||
mock_prisma_client.db.litellm_usertable = mocker.MagicMock()
|
||||
mock_prisma_client.db.litellm_usertable.find_unique = AsyncMock(return_value=None)
|
||||
mock_prisma_client.db.litellm_usertable.find_first = AsyncMock(return_value=None)
|
||||
|
||||
monkeypatch.setattr("litellm.default_internal_user_params", None, raising=False)
|
||||
|
||||
mocker.patch(
|
||||
"litellm.proxy.management_endpoints.scim.scim_v2._get_prisma_client_or_raise_exception",
|
||||
AsyncMock(return_value=mock_prisma_client),
|
||||
)
|
||||
|
||||
async def mock_get_config():
|
||||
return {"litellm_settings": {}}
|
||||
|
||||
from litellm.proxy.proxy_server import proxy_config
|
||||
|
||||
monkeypatch.setattr(proxy_config, "get_config", mock_get_config)
|
||||
|
||||
new_user_mock = mocker.patch(
|
||||
"litellm.proxy.management_endpoints.scim.scim_v2.new_user",
|
||||
AsyncMock(return_value=NewUserRequest(user_id="scim-nokey-user")),
|
||||
)
|
||||
|
||||
mocker.patch(
|
||||
"litellm.proxy.management_endpoints.scim.scim_v2.ScimTransformations.transform_litellm_user_to_scim_user",
|
||||
AsyncMock(return_value=scim_user),
|
||||
)
|
||||
|
||||
await create_user(user=scim_user)
|
||||
|
||||
called_args = new_user_mock.call_args.kwargs["data"]
|
||||
assert called_args.auto_create_key is False
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_user_auto_create_key_true_when_enabled(mocker, monkeypatch):
|
||||
"""
|
||||
When auto_create_keys_for_scim_users is True, SCIM user creation
|
||||
should pass auto_create_key=True to new_user.
|
||||
"""
|
||||
|
||||
scim_user = SCIMUser(
|
||||
schemas=["urn:ietf:params:scim:schemas:core:2.0:User"],
|
||||
userName="scim-key-user",
|
||||
name=SCIMUserName(familyName="User", givenName="WithKey"),
|
||||
emails=[SCIMUserEmail(value="withkey@example.com")],
|
||||
)
|
||||
|
||||
mock_prisma_client = mocker.MagicMock()
|
||||
mock_prisma_client.db = mocker.MagicMock()
|
||||
mock_prisma_client.db.litellm_usertable = mocker.MagicMock()
|
||||
mock_prisma_client.db.litellm_usertable.find_unique = AsyncMock(return_value=None)
|
||||
mock_prisma_client.db.litellm_usertable.find_first = AsyncMock(return_value=None)
|
||||
|
||||
monkeypatch.setattr("litellm.default_internal_user_params", None, raising=False)
|
||||
|
||||
mocker.patch(
|
||||
"litellm.proxy.management_endpoints.scim.scim_v2._get_prisma_client_or_raise_exception",
|
||||
AsyncMock(return_value=mock_prisma_client),
|
||||
)
|
||||
|
||||
async def mock_get_config():
|
||||
return {"litellm_settings": {"auto_create_keys_for_scim_users": True}}
|
||||
|
||||
from litellm.proxy.proxy_server import proxy_config
|
||||
|
||||
monkeypatch.setattr(proxy_config, "get_config", mock_get_config)
|
||||
|
||||
new_user_mock = mocker.patch(
|
||||
"litellm.proxy.management_endpoints.scim.scim_v2.new_user",
|
||||
AsyncMock(return_value=NewUserRequest(user_id="scim-key-user")),
|
||||
)
|
||||
|
||||
mocker.patch(
|
||||
"litellm.proxy.management_endpoints.scim.scim_v2.ScimTransformations.transform_litellm_user_to_scim_user",
|
||||
AsyncMock(return_value=scim_user),
|
||||
)
|
||||
|
||||
await create_user(user=scim_user)
|
||||
|
||||
called_args = new_user_mock.call_args.kwargs["data"]
|
||||
assert called_args.auto_create_key is True
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_user_if_not_exists_auto_create_key_false_by_default(
|
||||
mocker, monkeypatch
|
||||
):
|
||||
"""
|
||||
_create_user_if_not_exists should pass auto_create_key=False when
|
||||
auto_create_keys_for_scim_users is not set (default).
|
||||
"""
|
||||
monkeypatch.setattr("litellm.default_internal_user_params", None, raising=False)
|
||||
|
||||
async def mock_get_config():
|
||||
return {"litellm_settings": {}}
|
||||
|
||||
from litellm.proxy.proxy_server import proxy_config
|
||||
|
||||
monkeypatch.setattr(proxy_config, "get_config", mock_get_config)
|
||||
|
||||
new_user_mock = mocker.patch(
|
||||
"litellm.proxy.management_endpoints.internal_user_endpoints.new_user",
|
||||
AsyncMock(return_value=NewUserRequest(user_id="group-user")),
|
||||
)
|
||||
|
||||
result = await _create_user_if_not_exists(
|
||||
user_id="group-user", created_via="scim_group_membership"
|
||||
)
|
||||
|
||||
assert result is not None
|
||||
called_args = new_user_mock.call_args.kwargs["data"]
|
||||
assert called_args.auto_create_key is False
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_user_if_not_exists_auto_create_key_true_when_enabled(
|
||||
mocker, monkeypatch
|
||||
):
|
||||
"""
|
||||
_create_user_if_not_exists should pass auto_create_key=True when
|
||||
auto_create_keys_for_scim_users is True.
|
||||
"""
|
||||
monkeypatch.setattr("litellm.default_internal_user_params", None, raising=False)
|
||||
|
||||
async def mock_get_config():
|
||||
return {"litellm_settings": {"auto_create_keys_for_scim_users": True}}
|
||||
|
||||
from litellm.proxy.proxy_server import proxy_config
|
||||
|
||||
monkeypatch.setattr(proxy_config, "get_config", mock_get_config)
|
||||
|
||||
new_user_mock = mocker.patch(
|
||||
"litellm.proxy.management_endpoints.internal_user_endpoints.new_user",
|
||||
AsyncMock(return_value=NewUserRequest(user_id="group-user-key")),
|
||||
)
|
||||
|
||||
result = await _create_user_if_not_exists(
|
||||
user_id="group-user-key", created_via="scim_group_membership"
|
||||
)
|
||||
|
||||
assert result is not None
|
||||
called_args = new_user_mock.call_args.kwargs["data"]
|
||||
assert called_args.auto_create_key is True
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue