{"record":{"id":"7050af405c6fa9e8","repo":"BerriAI/litellm","slug":"only-admins-or-team-admins-can-update-projects","errorCode":null,"errorMessage":"Only admins or team admins can update projects","messagePattern":"Only admins or team admins can update projects","errorType":"http","errorClass":"HTTPException","httpStatus":403,"severity":"error","filePath":"enterprise/litellm_enterprise/proxy/management_endpoints/project_endpoints.py","lineNumber":615,"sourceCode":"            )\n\n        # Permission to *edit* the project must be evaluated against the\n        # project's CURRENT team. Sourcing the team from `data.team_id`\n        # would let an admin of any team pass the check by supplying their\n        # own team_id, hijacking the project (VERIA-55).\n        target_team_id = data.team_id or existing_project.team_id\n        target_team_obj = None\n        if target_team_id is not None:\n            target_team_obj = await _validate_team_exists(team_id=target_team_id, prisma_client=prisma_client)\n\n        has_permission = await _check_user_permission_for_project(\n            user_api_key_dict=user_api_key_dict,\n            team_id=existing_project.team_id,\n            prisma_client=prisma_client,\n        )\n\n        if not has_permission:\n            raise HTTPException(\n                status_code=403,\n                detail={\"error\": \"Only admins or team admins can update projects\"},\n            )\n\n        # Reassigning to a different team also requires admin rights on the\n        # destination team — otherwise a team admin could shed projects into\n        # an unsuspecting team's namespace.\n        if data.team_id is not None and data.team_id != existing_project.team_id:\n            can_assign_to_target = await _check_user_permission_for_project(\n                user_api_key_dict=user_api_key_dict,\n                team_id=data.team_id,\n                prisma_client=prisma_client,\n                team_object=(\n                    LiteLLM_TeamTable.model_validate(target_team_obj.model_dump()) if target_team_obj else None\n                ),\n            )\n            if not can_assign_to_target:\n                raise HTTPException(","sourceCodeStart":597,"sourceCodeEnd":633,"githubUrl":"https://github.com/BerriAI/litellm/blob/77b7c6c40c0c5aa5fbcb1d6a1825ac39ca8829b8/enterprise/litellm_enterprise/proxy/management_endpoints/project_endpoints.py#L597-L633","documentation":"Update permission is evaluated against the project's CURRENT team (existing_project.team_id), deliberately not the payload's team_id, so a team admin cannot point at another team to pass the check. Callers must be proxy admins or admins of the team that currently owns the project; otherwise 403.","triggerScenarios":"POST /project/update where the key's user is not a proxy admin and not an admin in the team stored on the existing project row — e.g. a team admin editing a project that was reassigned to another team.","commonSituations":"Projects moved between teams during reorganizations while old team admins keep automation running; shared service keys used across teams; callers assuming the payload's team_id is what authorizes them.","solutions":["Use the proxy admin master key for updates on projects owned by other teams","Have an admin of the project's current team perform the update (verify ownership first via GET /project/info)","If reassignment is the goal, have a proxy admin change team_id first; afterwards the new team's admins can manage it","Audit ownership before choosing credentials — the payload's team_id grants no rights on update"],"exampleFix":"# before (403: team-a admin edits a project now owned by team-b)\nhttpx.post(base + '/project/update', headers=team_a_admin_hdr, json={'project_id': 'p1', 'project_name': 'x'})\n\n# after: proxy admin key\nhttpx.post(base + '/project/update', headers=proxy_admin_hdr, json={'project_id': 'p1', 'project_name': 'x'})","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"try:\n    r = httpx.post(base + '/project/update', headers=hdr, json=payload)\n    r.raise_for_status()\nexcept httpx.HTTPStatusError as e:\n    if e.response.status_code == 403 and 'update projects' in e.response.text:\n        # permission is judged against the project's CURRENT team, not the payload\n        raise PermissionError('caller is not admin of the team that owns this project; escalate to that team admin or proxy admin') from None\n    raise","preventionTips":["Check project ownership (GET /project/info) before choosing credentials for an update","After reassigning a project, update automation credentials to the new team's admins","Reserve cross-team edits for the proxy admin credential"],"tags":["litellm","rbac","http-403","teams","projects"],"backgroundTag":"insufficient-permissions","analyzedSha":"77b7c6c40c0c5aa5fbcb1d6a1825ac39ca8829b8","analyzedAt":"2026-08-18T11:44:31.656Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}