From 6b803ca24e888ce49286cc21e305d7f602204fdc Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Wed, 18 Jun 2025 09:40:59 -0700 Subject: [PATCH] working provisioning for teams on SCIM --- .../scim/scim_transformations.py | 2 + .../management_endpoints/scim/scim_v2.py | 96 ++++++++++++++++++- 2 files changed, 97 insertions(+), 1 deletion(-) diff --git a/litellm/proxy/management_endpoints/scim/scim_transformations.py b/litellm/proxy/management_endpoints/scim/scim_transformations.py index f68f728e2a8..bb07cdbd770 100644 --- a/litellm/proxy/management_endpoints/scim/scim_transformations.py +++ b/litellm/proxy/management_endpoints/scim/scim_transformations.py @@ -124,6 +124,8 @@ class ScimTransformations: # Get team members scim_members: List[SCIMMember] = [] for member in team.members_with_roles or []: + if isinstance(member, dict): + member = Member(**member) scim_members.append( SCIMMember( value=ScimTransformations._get_scim_member_value(member), diff --git a/litellm/proxy/management_endpoints/scim/scim_v2.py b/litellm/proxy/management_endpoints/scim/scim_v2.py index 8feb0a832d5..b8712b55a2f 100644 --- a/litellm/proxy/management_endpoints/scim/scim_v2.py +++ b/litellm/proxy/management_endpoints/scim/scim_v2.py @@ -19,6 +19,7 @@ from fastapi import ( ) from litellm._logging import verbose_proxy_logger +from litellm.litellm_core_utils.safe_json_dumps import safe_dumps from litellm.proxy._types import ( LiteLLM_UserTable, LitellmUserRoles, @@ -925,7 +926,100 @@ async def patch_group( verbose_proxy_logger.debug("SCIM PATCH GROUP request: %s", patch_ops) try: + prisma_client = await _get_prisma_client_or_raise_exception() existing_team = await _check_team_exists(group_id) - return None + + # Initialize update data + update_data: Dict[str, Any] = {} + + # Create a fresh copy of existing metadata to avoid Prisma issues + existing_metadata = existing_team.metadata or {} + metadata = dict(existing_metadata) if existing_metadata else {} + + # Track member changes + current_members = set(existing_team.members or []) + final_members = current_members.copy() + + # Process each patch operation + for op in patch_ops.Operations: + path = (op.path or "").lower() + value = op.value + op_type = op.op + + if path == "displayname": + if op_type == "remove": + update_data["team_alias"] = None + else: + update_data["team_alias"] = str(value) + elif path == "externalid": + if op_type == "remove": + metadata.pop("externalId", None) + else: + metadata["externalId"] = str(value) + elif path.startswith("members"): + # Handle member operations + member_values = _extract_group_values(value) + # Validate that users exist + valid_members = [] + for member_id in member_values: + user = await prisma_client.db.litellm_usertable.find_unique( + where={"user_id": member_id} + ) + if user: + valid_members.append(member_id) + + if op_type == "replace": + final_members = set(valid_members) + elif op_type == "add": + final_members.update(valid_members) + elif op_type == "remove": + for member_id in valid_members: + final_members.discard(member_id) + else: + # Handle other generic metadata + if op_type == "remove": + metadata.pop(path, None) + else: + metadata[path] = value + + # Only include metadata in update if it has changed + if "metadata" in update_data and isinstance(update_data["metadata"], dict): + # ensure metadata gets stored as a json for a team + update_data["metadata"] = safe_dumps(update_data["metadata"]) + + # Update members list + update_data["members"] = list(final_members) + + # Update team in database + updated_team = await prisma_client.db.litellm_teamtable.update( + where={"team_id": group_id}, + data=update_data, + ) + + # Handle user-team relationship changes using existing helper + members_to_add = final_members - current_members + members_to_remove = current_members - final_members + + # Use existing helper functions for team membership changes + for member_id in members_to_add: + await patch_team_membership( + user_id=member_id, + teams_ids_to_add_user_to=[group_id], + teams_ids_to_remove_user_from=[], + ) + + for member_id in members_to_remove: + await patch_team_membership( + user_id=member_id, + teams_ids_to_add_user_to=[], + teams_ids_to_remove_user_from=[group_id], + ) + + # Convert to SCIM format and return + scim_group = await ScimTransformations.transform_litellm_team_to_scim_group( + updated_team + ) + return scim_group + except Exception as e: raise handle_exception_on_proxy(e)