diff --git a/apps/api/plane/app/views/project/base.py b/apps/api/plane/app/views/project/base.py index dafbbf11cc..d0186206ff 100644 --- a/apps/api/plane/app/views/project/base.py +++ b/apps/api/plane/app/views/project/base.py @@ -565,7 +565,7 @@ class DeployBoardViewSet(BaseViewSet): # caller could aim their own workspace at another tenant's project id. # Defence in depth: the permission class now binds both. if not Project.objects.filter(pk=project_id, workspace__slug=slug).exists(): - return Response({"error": "Project not found"}, status=status.HTTP_404_NOT_FOUND) + return Response({"error": "Project does not exist"}, status=status.HTTP_404_NOT_FOUND) project_deploy_board, _ = DeployBoard.objects.get_or_create( entity_name="project", entity_identifier=project_id, project_id=project_id diff --git a/apps/api/plane/utils/permissions/project.py b/apps/api/plane/utils/permissions/project.py index 5a07bdefe5..e04c68022e 100644 --- a/apps/api/plane/utils/permissions/project.py +++ b/apps/api/plane/utils/permissions/project.py @@ -66,12 +66,16 @@ class ProjectMemberPermission(BasePermission): project_id=view.project_id, is_active=True, ).exists() - ## Only workspace owners or admins can create the projects + # Scope POST to the URL project, as the other two branches already do. + # A workspace-only check would let any member create sub-resources in a + # project they do not belong to. Kept identical to the copy in + # app/permissions/project.py — the two must not drift. if request.method == "POST": - return WorkspaceMember.objects.filter( + return ProjectMember.objects.filter( workspace__slug=view.workspace_slug, member=request.user, role__in=[ROLE.ADMIN.value, ROLE.MEMBER.value], + project_id=view.project_id, is_active=True, ).exists()