mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-11 03:38:38 +00:00
fix(proxy): keep project object permission validation on the stored payload
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
parent
a27bb1088e
commit
0b5ea60bfd
3 changed files with 35 additions and 43 deletions
|
|
@ -790,16 +790,8 @@ async def update_project(
|
|||
data, existing_project, _router_access_group_names(llm_router)
|
||||
)
|
||||
|
||||
object_permission_data: Final = (
|
||||
data.object_permission.model_dump(exclude_none=True) if data.object_permission is not None else None
|
||||
)
|
||||
object_permission_payload: Final = (
|
||||
_OBJECT_PERMISSION_PAYLOAD.validate_python(object_permission_data) if object_permission_data else None
|
||||
)
|
||||
|
||||
# Prepare update data
|
||||
update_data = _jsonified(prisma_client, data.model_dump(exclude_none=True, exclude={"project_id"}))
|
||||
update_data.pop("object_permission", None)
|
||||
update_data["updated_by"] = user_api_key_dict.user_id or litellm_proxy_admin_name
|
||||
|
||||
# Handle budget updates
|
||||
|
|
@ -809,6 +801,11 @@ async def update_project(
|
|||
**({"max_budget": None} if "max_budget" in data.model_fields_set and data.max_budget is None else {}),
|
||||
}
|
||||
|
||||
object_permission_data: Final = update_data.pop("object_permission", None)
|
||||
object_permission_payload: Final = (
|
||||
_OBJECT_PERMISSION_PAYLOAD.validate_python(object_permission_data) if object_permission_data else None
|
||||
)
|
||||
|
||||
if data.team_id is not None:
|
||||
current_project_record: Final = await _writer_project_table(prisma_client).find_unique(
|
||||
where={"project_id": data.project_id}
|
||||
|
|
|
|||
|
|
@ -65,17 +65,6 @@ def _clear_key_object_permission(key: str, permission_id: str) -> None:
|
|||
)
|
||||
|
||||
|
||||
def _clear_project_object_permission(project_id: str, permission_id: str) -> None:
|
||||
write_rows(
|
||||
'UPDATE "LiteLLM_ProjectTable" SET object_permission_id = NULL WHERE project_id = %s',
|
||||
(project_id,),
|
||||
)
|
||||
write_rows(
|
||||
'DELETE FROM "LiteLLM_ObjectPermissionTable" WHERE object_permission_id = %s',
|
||||
(permission_id,),
|
||||
)
|
||||
|
||||
|
||||
def _deleted_key_rows(key: str) -> list[dict[str, JsonValue]]:
|
||||
return read_rows(
|
||||
'SELECT token FROM "LiteLLM_DeletedVerificationToken" WHERE token = %s',
|
||||
|
|
@ -660,22 +649,8 @@ def test_project_update_rejects_moving_project_with_attached_key(ownership_gatew
|
|||
budget: Final = scenario.budget(max_budget=3)
|
||||
attached_project: Final = scenario.project(team_a, budget_id=budget, models=[model])
|
||||
key: Final = scenario.key(team_id=team_a, project_id=attached_project, models=[model])
|
||||
permission_seed: Final = ownership_gateway.request(
|
||||
"POST",
|
||||
"/project/update",
|
||||
{"project_id": attached_project, "object_permission": {"vector_stores": ["existing-store"]}},
|
||||
)
|
||||
assert permission_seed.status_code == 200, permission_seed.text
|
||||
project_permission: Final = read_rows(
|
||||
'SELECT object_permission_id FROM "LiteLLM_ProjectTable" WHERE project_id = %s',
|
||||
(attached_project,),
|
||||
)
|
||||
assert len(project_permission) == 1
|
||||
permission_id: Final = string_value(project_permission[0]["object_permission_id"])
|
||||
scenario.cleanups.callback(_clear_project_object_permission, attached_project, permission_id)
|
||||
project_before: Final = _project_rows(attached_project)
|
||||
key_before: Final = _key_rows(key)
|
||||
permission_before: Final = _object_permission_rows(permission_id)
|
||||
budget_before: Final = _budget_rows(budget)
|
||||
moved_with_key: Final = ownership_gateway.request(
|
||||
"POST",
|
||||
|
|
@ -684,13 +659,11 @@ def test_project_update_rejects_moving_project_with_attached_key(ownership_gatew
|
|||
"project_id": attached_project,
|
||||
"team_id": team_b,
|
||||
"max_budget": 11,
|
||||
"object_permission": {"vector_stores": ["replacement-store"]},
|
||||
},
|
||||
)
|
||||
assert moved_with_key.status_code == 400, moved_with_key.text
|
||||
assert _project_rows(attached_project) == project_before
|
||||
assert _key_rows(key) == key_before
|
||||
assert _object_permission_rows(permission_id) == permission_before
|
||||
assert _budget_rows(budget) == budget_before
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1269,6 +1269,36 @@ def _written_project_data(mock_prisma: mock.MagicMock) -> dict:
|
|||
return mock_prisma.db.litellm_projecttable.update.await_args.kwargs["data"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_project_object_permission_validation_precedes_budget_write(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
project_id: Final = "project-object-permission-validation"
|
||||
mock_prisma: Final = _project_update_mocks(monkeypatch, {})
|
||||
mock_prisma.db.litellm_projecttable.find_unique.return_value.budget_id = "budget-project"
|
||||
budget_table: Final = mock.MagicMock()
|
||||
budget_table.update = mock.AsyncMock()
|
||||
mock_prisma.db.litellm_budgettable = budget_table
|
||||
|
||||
def jsonify_object_permission_as_string(payload: dict[str, object]) -> dict[str, object]:
|
||||
return {**payload, "object_permission": '{"vector_stores": ["replacement-store"]}'}
|
||||
|
||||
mock_prisma.jsonify_object = jsonify_object_permission_as_string
|
||||
|
||||
with pytest.raises(ProxyException) as error:
|
||||
await _run_project_update(
|
||||
project_id,
|
||||
max_budget=50,
|
||||
object_permission={"vector_stores": ["replacement-store"]},
|
||||
)
|
||||
|
||||
assert error.value.code == "500"
|
||||
assert "Input should be a valid dictionary" in error.value.message
|
||||
assert "input_type=str" in error.value.message
|
||||
budget_table.update.assert_not_awaited()
|
||||
mock_prisma.db.litellm_projecttable.update.assert_not_awaited()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_project_rejects_move_when_attached_teamless_key_exists(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
|
|
@ -1278,14 +1308,9 @@ async def test_update_project_rejects_move_when_attached_teamless_key_exists(
|
|||
mock_prisma: Final = _project_update_mocks(monkeypatch, {})
|
||||
mock_prisma.db.litellm_projecttable.find_unique.return_value.team_id = "team-a"
|
||||
mock_prisma.db.litellm_projecttable.find_unique.return_value.budget_id = "budget-project"
|
||||
mock_prisma.db.litellm_projecttable.find_unique.return_value.object_permission_id = "permission-project"
|
||||
budget_table: Final = mock.MagicMock()
|
||||
budget_table.update = mock.AsyncMock()
|
||||
permission_table: Final = mock.MagicMock()
|
||||
permission_table.update = mock.AsyncMock()
|
||||
permission_table.create = mock.AsyncMock()
|
||||
mock_prisma.db.litellm_budgettable = budget_table
|
||||
mock_prisma.db.litellm_objectpermissiontable = permission_table
|
||||
mock_prisma.db.litellm_teamtable.find_unique = mock.AsyncMock(
|
||||
return_value=LiteLLM_TeamTable(team_id=destination_team_id)
|
||||
)
|
||||
|
|
@ -1306,7 +1331,6 @@ async def test_update_project_rejects_move_when_attached_teamless_key_exists(
|
|||
project_id,
|
||||
team_id=destination_team_id,
|
||||
max_budget=50,
|
||||
object_permission={"vector_stores": ["replacement-store"]},
|
||||
)
|
||||
|
||||
expected_detail: Final = {
|
||||
|
|
@ -1320,8 +1344,6 @@ async def test_update_project_rejects_move_when_attached_teamless_key_exists(
|
|||
mock_prisma.writer_db.litellm_verificationtoken.count.assert_awaited_once()
|
||||
mock_prisma.db.litellm_verificationtoken.count.assert_not_awaited()
|
||||
budget_table.update.assert_not_awaited()
|
||||
permission_table.update.assert_not_awaited()
|
||||
permission_table.create.assert_not_awaited()
|
||||
mock_prisma.db.litellm_projecttable.update.assert_not_awaited()
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue