From 31392baa3562da1df00b51da8c649c18eae8c592 Mon Sep 17 00:00:00 2001 From: Filippo Mattia Menghi Date: Wed, 10 Jun 2026 09:48:25 +0200 Subject: [PATCH] Fix /project/update 500 when object_permission is set prisma_client.jsonify_object serializes the nested object_permission dict into a JSON string, which update_project then passed directly to the Prisma object permission table, raising FieldNotFoundError. Route the update through handle_update_object_permission_common, the same helper used by /team/update and /key/update, which parses string JSON payloads and upserts the LiteLLM_ObjectPermissionTable row. Fixes #28983. --- .../management_endpoints/project_endpoints.py | 31 ++++------- .../test_project_org_authz.py | 52 ++++++++++++++++++- 2 files changed, 61 insertions(+), 22 deletions(-) diff --git a/enterprise/litellm_enterprise/proxy/management_endpoints/project_endpoints.py b/enterprise/litellm_enterprise/proxy/management_endpoints/project_endpoints.py index 75229bacc8f..31a86427c19 100644 --- a/enterprise/litellm_enterprise/proxy/management_endpoints/project_endpoints.py +++ b/enterprise/litellm_enterprise/proxy/management_endpoints/project_endpoints.py @@ -20,6 +20,9 @@ from litellm._uuid import uuid from litellm.proxy._types import * from litellm.proxy.auth.user_api_key_auth import user_api_key_auth from litellm.proxy.management_endpoints.common_utils import _set_object_metadata_field +from litellm.proxy.management_helpers.object_permission_utils import ( + handle_update_object_permission_common, +) from litellm.proxy.management_helpers.utils import ( management_endpoint_wrapper, ) @@ -665,27 +668,13 @@ async def update_project( # noqa: PLR0915 update_data.pop(field, None) # Handle object permissions - if "object_permission" in update_data: - object_permission_data = update_data.pop("object_permission") - if object_permission_data: - if existing_project.object_permission_id: - # Update existing permission - await prisma_client.db.litellm_objectpermissiontable.update( - where={ - "object_permission_id": existing_project.object_permission_id - }, - data=object_permission_data, - ) - else: - # Create new permission - created_permission = ( - await prisma_client.db.litellm_objectpermissiontable.create( - data=object_permission_data, - ) - ) - update_data["object_permission_id"] = ( - created_permission.object_permission_id - ) + object_permission_id = await handle_update_object_permission_common( + data_json=update_data, + existing_object_permission_id=existing_project.object_permission_id, + prisma_client=prisma_client, + ) + if object_permission_id is not None: + update_data["object_permission_id"] = object_permission_id # Handle metadata fields for field in LiteLLM_ManagementEndpoint_MetadataFields: diff --git a/tests/test_litellm/proxy/management_endpoints/test_project_org_authz.py b/tests/test_litellm/proxy/management_endpoints/test_project_org_authz.py index a06d79306ab..1c98677f9f1 100644 --- a/tests/test_litellm/proxy/management_endpoints/test_project_org_authz.py +++ b/tests/test_litellm/proxy/management_endpoints/test_project_org_authz.py @@ -7,7 +7,7 @@ Unit tests for the VERIA-55 fixes: member of. """ -from unittest.mock import AsyncMock, MagicMock +from unittest.mock import AsyncMock, MagicMock, patch import pytest from fastapi import HTTPException @@ -193,3 +193,53 @@ async def test_assign_key_org_blocks_caller_with_no_memberships(): prisma_client=prisma, ) assert exc_info.value.status_code == 403 + + +# --------------------------------------------------------------------------- +# /project/update — object_permission upsert +# --------------------------------------------------------------------------- + + +@pytest.mark.asyncio +async def test_update_project_upserts_object_permission_as_dict(): + """jsonify_object serializes the nested object_permission dict into a JSON + string; passing that string straight to the Prisma table raised + FieldNotFoundError. The shared helper must receive it and upsert a dict.""" + from litellm.proxy._types import ( + LiteLLM_ObjectPermissionBase, + UpdateProjectRequest, + ) + from litellm.proxy.utils import PrismaClient + from litellm_enterprise.proxy.management_endpoints.project_endpoints import ( + update_project, + ) + + prisma = MagicMock() + prisma.jsonify_object = lambda data: PrismaClient.jsonify_object(prisma, data) + prisma.db.litellm_projecttable.find_unique = AsyncMock( + return_value=MagicMock(team_id=None, budget_id=None, object_permission_id=None) + ) + prisma.db.litellm_projecttable.update = AsyncMock(return_value=MagicMock()) + perm_table = prisma.db.litellm_objectpermissiontable + perm_table.find_unique = AsyncMock(return_value=None) + perm_table.upsert = AsyncMock(return_value=MagicMock(object_permission_id="op-1")) + + admin = UserAPIKeyAuth(user_id="root", user_role=LitellmUserRoles.PROXY_ADMIN.value) + data = UpdateProjectRequest( + project_id="proj-1", + object_permission=LiteLLM_ObjectPermissionBase(mcp_servers=["server-1"]), + ) + with ( + patch("litellm.proxy.proxy_server.prisma_client", prisma), + patch("litellm.proxy.proxy_server.premium_user", True), + ): + await update_project( + data=data, http_request=MagicMock(), user_api_key_dict=admin + ) + + perm_table.upsert.assert_awaited_once() + upsert_data = perm_table.upsert.await_args.kwargs["data"] + assert upsert_data["create"] == {"mcp_servers": ["server-1"]} + project_update = prisma.db.litellm_projecttable.update.await_args.kwargs["data"] + assert project_update["object_permission_id"] == "op-1" + assert "object_permission" not in project_update