From 34243e23e73f4b44175aee3696f0d8bbd1f9c9ac Mon Sep 17 00:00:00 2001 From: sangeethailango Date: Tue, 9 Sep 2025 13:52:40 +0530 Subject: [PATCH 1/6] chore: project admin accesss to workspace admins --- apps/api/plane/app/permissions/base.py | 22 ++++++++++++++++++++-- apps/api/plane/app/permissions/project.py | 20 ++++++++++++++++++-- apps/api/plane/app/views/project/base.py | 14 ++++++++++---- 3 files changed, 48 insertions(+), 8 deletions(-) diff --git a/apps/api/plane/app/permissions/base.py b/apps/api/plane/app/permissions/base.py index 7ba12a2e26c..406a277cb1b 100644 --- a/apps/api/plane/app/permissions/base.py +++ b/apps/api/plane/app/permissions/base.py @@ -39,13 +39,31 @@ def _wrapped_view(instance, request, *args, **kwargs): ).exists(): return view_func(instance, request, *args, **kwargs) else: - if ProjectMember.objects.filter( + is_user_has_allowed_role = ProjectMember.objects.filter( member=request.user, workspace__slug=kwargs["slug"], project_id=kwargs["project_id"], role__in=allowed_role_values, is_active=True, - ).exists(): + ).exists() + + is_user_part_of_project = ProjectMember.objects.filter( + member=request.user, + workspace__slug=kwargs["slug"], + project_id=kwargs["project_id"], + is_active=True, + ).exists() + + is_user_workspace_admin = WorkspaceMember.objects.filter( + member=request.user, + workspace__slug=kwargs["slug"], + role=ROLE.ADMIN.value, + is_active=True, + ).exists() + + if is_user_has_allowed_role: + return view_func(instance, request, *args, **kwargs) + elif is_user_part_of_project and is_user_workspace_admin: return view_func(instance, request, *args, **kwargs) # Return permission denied if no conditions are met diff --git a/apps/api/plane/app/permissions/project.py b/apps/api/plane/app/permissions/project.py index 1596d90b37b..93bb5bb23d2 100644 --- a/apps/api/plane/app/permissions/project.py +++ b/apps/api/plane/app/permissions/project.py @@ -30,8 +30,7 @@ def has_permission(self, request, view): is_active=True, ).exists() - ## Only Project Admins can update project attributes - return ProjectMember.objects.filter( + is_project_admin = ProjectMember.objects.filter( workspace__slug=view.workspace_slug, member=request.user, role=Admin, @@ -39,6 +38,23 @@ def has_permission(self, request, view): is_active=True, ).exists() + is_project_member = ProjectMember.objects.filter( + workspace__slug=view.workspace_slug, + member=request.user, + project_id=view.project_id, + is_active=True, + ).exists() + + is_user_workspace_admin = WorkspaceMember.objects.filter( + member=request.user, + workspace__slug=view.workspace_slug, + role=Admin, + is_active=True, + ).exists() + + ## Only project admins or workspace admin who is part of the project can access + return is_project_admin or (is_project_member and is_user_workspace_admin) + class ProjectMemberPermission(BasePermission): def has_permission(self, request, view): diff --git a/apps/api/plane/app/views/project/base.py b/apps/api/plane/app/views/project/base.py index b4ee113c46b..854602efe7d 100644 --- a/apps/api/plane/app/views/project/base.py +++ b/apps/api/plane/app/views/project/base.py @@ -5,13 +5,12 @@ import json # Django imports -from django.db import IntegrityError from django.db.models import Exists, F, OuterRef, Prefetch, Q, Subquery from django.core.serializers.json import DjangoJSONEncoder # Third Party imports from rest_framework.response import Response -from rest_framework import serializers, status +from rest_framework import status from rest_framework.permissions import AllowAny # Module imports @@ -341,13 +340,20 @@ def create(self, request, slug): def partial_update(self, request, slug, pk=None): # try: - if not ProjectMember.objects.filter( + is_workspace_admin = WorkspaceMember.objects.filter( + member=request.user, workspace__slug=slug, is_active=True, role=20 + ).exists() + + is_project_admin = ProjectMember.objects.filter( member=request.user, workspace__slug=slug, project_id=pk, role=20, is_active=True, - ).exists(): + ).exists() + + # Return error for if the user is neither workspace admin nor project admin + if not is_project_admin and not is_workspace_admin: return Response( {"error": "You don't have the required permissions."}, status=status.HTTP_403_FORBIDDEN, From 1252aba57a6dffc4a7dc08c0a4193a76b0dbf393 Mon Sep 17 00:00:00 2001 From: sangeethailango Date: Tue, 9 Sep 2025 13:54:48 +0530 Subject: [PATCH 2/6] chore: frontend changes --- apps/web/core/store/user/base-permissions.store.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/apps/web/core/store/user/base-permissions.store.ts b/apps/web/core/store/user/base-permissions.store.ts index b88d7493083..404dd2ae60f 100644 --- a/apps/web/core/store/user/base-permissions.store.ts +++ b/apps/web/core/store/user/base-permissions.store.ts @@ -118,7 +118,13 @@ export abstract class BaseUserPermissionStore implements IBaseUserPermissionStor */ protected getProjectRole = computedFn((workspaceSlug: string, projectId: string): EUserPermissions | undefined => { if (!workspaceSlug || !projectId) return undefined; - return this.workspaceProjectsPermissions?.[workspaceSlug]?.[projectId] || undefined; + const projectRole = this.workspaceProjectsPermissions?.[workspaceSlug]?.[projectId]; + console.log("projectRole", projectRole); + if (!projectRole) return undefined; + const workspaceRole = this.workspaceUserInfo?.[workspaceSlug]?.role; + console.log("workspaceRole", workspaceRole); + if (workspaceRole === EUserWorkspaceRoles.ADMIN) return EUserPermissions.ADMIN; + else return projectRole; }); /** From a02c3d6c429e697e13494cfc46e24d5962673142 Mon Sep 17 00:00:00 2001 From: sangeethailango Date: Tue, 9 Sep 2025 13:58:50 +0530 Subject: [PATCH 3/6] chore: remove console.log --- apps/web/core/store/user/base-permissions.store.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/apps/web/core/store/user/base-permissions.store.ts b/apps/web/core/store/user/base-permissions.store.ts index 404dd2ae60f..4d6f13d2685 100644 --- a/apps/web/core/store/user/base-permissions.store.ts +++ b/apps/web/core/store/user/base-permissions.store.ts @@ -119,10 +119,8 @@ export abstract class BaseUserPermissionStore implements IBaseUserPermissionStor protected getProjectRole = computedFn((workspaceSlug: string, projectId: string): EUserPermissions | undefined => { if (!workspaceSlug || !projectId) return undefined; const projectRole = this.workspaceProjectsPermissions?.[workspaceSlug]?.[projectId]; - console.log("projectRole", projectRole); if (!projectRole) return undefined; const workspaceRole = this.workspaceUserInfo?.[workspaceSlug]?.role; - console.log("workspaceRole", workspaceRole); if (workspaceRole === EUserWorkspaceRoles.ADMIN) return EUserPermissions.ADMIN; else return projectRole; }); From 40966c0a7d47ae9cbe953e70dfe721eef4788e8e Mon Sep 17 00:00:00 2001 From: sangeethailango Date: Tue, 9 Sep 2025 16:30:10 +0530 Subject: [PATCH 4/6] chore: refactor permission decorator --- apps/api/plane/app/permissions/base.py | 30 ++++++++++++------------ apps/api/plane/app/views/project/base.py | 26 ++++++++++++-------- 2 files changed, 31 insertions(+), 25 deletions(-) diff --git a/apps/api/plane/app/permissions/base.py b/apps/api/plane/app/permissions/base.py index 406a277cb1b..881088a3fb4 100644 --- a/apps/api/plane/app/permissions/base.py +++ b/apps/api/plane/app/permissions/base.py @@ -47,23 +47,23 @@ def _wrapped_view(instance, request, *args, **kwargs): is_active=True, ).exists() - is_user_part_of_project = ProjectMember.objects.filter( - member=request.user, - workspace__slug=kwargs["slug"], - project_id=kwargs["project_id"], - is_active=True, - ).exists() - - is_user_workspace_admin = WorkspaceMember.objects.filter( - member=request.user, - workspace__slug=kwargs["slug"], - role=ROLE.ADMIN.value, - is_active=True, - ).exists() - + # Return if the user has the allowed role else if they are workspace admin and part of the project regardless of the role if is_user_has_allowed_role: return view_func(instance, request, *args, **kwargs) - elif is_user_part_of_project and is_user_workspace_admin: + elif ( + ProjectMember.objects.filter( + member=request.user, + workspace__slug=kwargs["slug"], + project_id=kwargs["project_id"], + is_active=True, + ).exists() + and WorkspaceMember.objects.filter( + member=request.user, + workspace__slug=kwargs["slug"], + role=ROLE.ADMIN.value, + is_active=True, + ).exists() + ): return view_func(instance, request, *args, **kwargs) # Return permission denied if no conditions are met diff --git a/apps/api/plane/app/views/project/base.py b/apps/api/plane/app/views/project/base.py index 854602efe7d..db00e2dd4a0 100644 --- a/apps/api/plane/app/views/project/base.py +++ b/apps/api/plane/app/views/project/base.py @@ -41,6 +41,12 @@ from plane.utils.host import base_host +# Permission Mappings +Admin = 20 +Member = 15 +Guest = 5 + + class ProjectViewSet(BaseViewSet): serializer_class = ProjectListSerializer model = Project @@ -105,7 +111,7 @@ def list_detail(self, request, slug): fields = [field for field in request.GET.get("fields", "").split(",") if field] projects = self.get_queryset().order_by("sort_order", "name") if WorkspaceMember.objects.filter( - member=request.user, workspace__slug=slug, is_active=True, role=5 + member=request.user, workspace__slug=slug, is_active=True, role=Guest ).exists(): projects = projects.filter( project_projectmember__member=self.request.user, @@ -113,7 +119,7 @@ def list_detail(self, request, slug): ) if WorkspaceMember.objects.filter( - member=request.user, workspace__slug=slug, is_active=True, role=15 + member=request.user, workspace__slug=slug, is_active=True, role=Member ).exists(): projects = projects.filter( Q( @@ -188,7 +194,7 @@ def list(self, request, slug): ) if WorkspaceMember.objects.filter( - member=request.user, workspace__slug=slug, is_active=True, role=5 + member=request.user, workspace__slug=slug, is_active=True, role=Guest ).exists(): projects = projects.filter( project_projectmember__member=self.request.user, @@ -196,7 +202,7 @@ def list(self, request, slug): ) if WorkspaceMember.objects.filter( - member=request.user, workspace__slug=slug, is_active=True, role=15 + member=request.user, workspace__slug=slug, is_active=True, role=Member ).exists(): projects = projects.filter( Q( @@ -249,7 +255,7 @@ def create(self, request, slug): # Add the user as Administrator to the project _ = ProjectMember.objects.create( - project_id=serializer.data["id"], member=request.user, role=20 + project_id=serializer.data["id"], member=request.user, role=Admin ) # Also create the issue property for the user _ = IssueUserProperty.objects.create( @@ -262,7 +268,7 @@ def create(self, request, slug): ProjectMember.objects.create( project_id=serializer.data["id"], member_id=serializer.data["project_lead"], - role=20, + role=Admin, ) # Also create the issue property for the user IssueUserProperty.objects.create( @@ -341,14 +347,14 @@ def create(self, request, slug): def partial_update(self, request, slug, pk=None): # try: is_workspace_admin = WorkspaceMember.objects.filter( - member=request.user, workspace__slug=slug, is_active=True, role=20 + member=request.user, workspace__slug=slug, is_active=True, role=Admin ).exists() is_project_admin = ProjectMember.objects.filter( member=request.user, workspace__slug=slug, project_id=pk, - role=20, + role=Admin, is_active=True, ).exists() @@ -408,13 +414,13 @@ def partial_update(self, request, slug, pk=None): def destroy(self, request, slug, pk): if ( WorkspaceMember.objects.filter( - member=request.user, workspace__slug=slug, is_active=True, role=20 + member=request.user, workspace__slug=slug, is_active=True, role=Admin ).exists() or ProjectMember.objects.filter( member=request.user, workspace__slug=slug, project_id=pk, - role=20, + role=Admin, is_active=True, ).exists() ): From 0cfff05bf401f80ae8dcb0a5c7b9e4618c6fbb26 Mon Sep 17 00:00:00 2001 From: sangeethailango Date: Tue, 9 Sep 2025 19:09:50 +0530 Subject: [PATCH 5/6] chore: role enum --- apps/api/plane/app/permissions/project.py | 47 ++++++++++------------- apps/api/plane/app/views/project/base.py | 46 ++++++++++++++-------- apps/api/plane/db/models/project.py | 8 +++- 3 files changed, 57 insertions(+), 44 deletions(-) diff --git a/apps/api/plane/app/permissions/project.py b/apps/api/plane/app/permissions/project.py index 93bb5bb23d2..e095ffed483 100644 --- a/apps/api/plane/app/permissions/project.py +++ b/apps/api/plane/app/permissions/project.py @@ -3,11 +3,7 @@ # Module import from plane.db.models import ProjectMember, WorkspaceMember - -# Permission Mappings -Admin = 20 -Member = 15 -Guest = 5 +from plane.db.models.project import ROLE class ProjectBasePermission(BasePermission): @@ -26,34 +22,31 @@ def has_permission(self, request, view): return WorkspaceMember.objects.filter( workspace__slug=view.workspace_slug, member=request.user, - role__in=[Admin, Member], + role__in=[ROLE.ADMIN.value, ROLE.MEMBER.value], is_active=True, ).exists() - is_project_admin = ProjectMember.objects.filter( + project_member_qs = ProjectMember.objects.filter( workspace__slug=view.workspace_slug, member=request.user, - role=Admin, project_id=view.project_id, is_active=True, - ).exists() - - is_project_member = ProjectMember.objects.filter( - workspace__slug=view.workspace_slug, - member=request.user, - project_id=view.project_id, - is_active=True, - ).exists() - - is_user_workspace_admin = WorkspaceMember.objects.filter( - member=request.user, - workspace__slug=view.workspace_slug, - role=Admin, - is_active=True, - ).exists() + ) ## Only project admins or workspace admin who is part of the project can access - return is_project_admin or (is_project_member and is_user_workspace_admin) + + if project_member_qs.filter(role=ROLE.ADMIN.value).exists(): + return True + else: + return ( + project_member_qs.exists() + and WorkspaceMember.objects.filter( + member=request.user, + workspace__slug=view.workspace_slug, + role=ROLE.ADMIN.value, + is_active=True, + ).exists() + ) class ProjectMemberPermission(BasePermission): @@ -71,7 +64,7 @@ def has_permission(self, request, view): return WorkspaceMember.objects.filter( workspace__slug=view.workspace_slug, member=request.user, - role__in=[Admin, Member], + role__in=[ROLE.ADMIN.value, ROLE.MEMBER.value], is_active=True, ).exists() @@ -79,7 +72,7 @@ def has_permission(self, request, view): return ProjectMember.objects.filter( workspace__slug=view.workspace_slug, member=request.user, - role__in=[Admin, Member], + role__in=[ROLE.ADMIN.value, ROLE.MEMBER.value], project_id=view.project_id, is_active=True, ).exists() @@ -113,7 +106,7 @@ def has_permission(self, request, view): return ProjectMember.objects.filter( workspace__slug=view.workspace_slug, member=request.user, - role__in=[Admin, Member], + role__in=[ROLE.ADMIN.value, ROLE.MEMBER.value], project_id=view.project_id, is_active=True, ).exists() diff --git a/apps/api/plane/app/views/project/base.py b/apps/api/plane/app/views/project/base.py index db00e2dd4a0..ce3cb4134cb 100644 --- a/apps/api/plane/app/views/project/base.py +++ b/apps/api/plane/app/views/project/base.py @@ -41,12 +41,6 @@ from plane.utils.host import base_host -# Permission Mappings -Admin = 20 -Member = 15 -Guest = 5 - - class ProjectViewSet(BaseViewSet): serializer_class = ProjectListSerializer model = Project @@ -111,7 +105,10 @@ def list_detail(self, request, slug): fields = [field for field in request.GET.get("fields", "").split(",") if field] projects = self.get_queryset().order_by("sort_order", "name") if WorkspaceMember.objects.filter( - member=request.user, workspace__slug=slug, is_active=True, role=Guest + member=request.user, + workspace__slug=slug, + is_active=True, + role=ROLE.GUEST.value, ).exists(): projects = projects.filter( project_projectmember__member=self.request.user, @@ -119,7 +116,10 @@ def list_detail(self, request, slug): ) if WorkspaceMember.objects.filter( - member=request.user, workspace__slug=slug, is_active=True, role=Member + member=request.user, + workspace__slug=slug, + is_active=True, + role=ROLE.MEMBER.value, ).exists(): projects = projects.filter( Q( @@ -194,7 +194,10 @@ def list(self, request, slug): ) if WorkspaceMember.objects.filter( - member=request.user, workspace__slug=slug, is_active=True, role=Guest + member=request.user, + workspace__slug=slug, + is_active=True, + role=ROLE.GUEST.value, ).exists(): projects = projects.filter( project_projectmember__member=self.request.user, @@ -202,7 +205,10 @@ def list(self, request, slug): ) if WorkspaceMember.objects.filter( - member=request.user, workspace__slug=slug, is_active=True, role=Member + member=request.user, + workspace__slug=slug, + is_active=True, + role=ROLE.GUEST.value, ).exists(): projects = projects.filter( Q( @@ -255,7 +261,9 @@ def create(self, request, slug): # Add the user as Administrator to the project _ = ProjectMember.objects.create( - project_id=serializer.data["id"], member=request.user, role=Admin + project_id=serializer.data["id"], + member=request.user, + role=ROLE.ADMIN.value, ) # Also create the issue property for the user _ = IssueUserProperty.objects.create( @@ -268,7 +276,7 @@ def create(self, request, slug): ProjectMember.objects.create( project_id=serializer.data["id"], member_id=serializer.data["project_lead"], - role=Admin, + role=ROLE.ADMIN.value, ) # Also create the issue property for the user IssueUserProperty.objects.create( @@ -347,14 +355,17 @@ def create(self, request, slug): def partial_update(self, request, slug, pk=None): # try: is_workspace_admin = WorkspaceMember.objects.filter( - member=request.user, workspace__slug=slug, is_active=True, role=Admin + member=request.user, + workspace__slug=slug, + is_active=True, + role=ROLE.ADMIN.value, ).exists() is_project_admin = ProjectMember.objects.filter( member=request.user, workspace__slug=slug, project_id=pk, - role=Admin, + role=ROLE.ADMIN.value, is_active=True, ).exists() @@ -414,13 +425,16 @@ def partial_update(self, request, slug, pk=None): def destroy(self, request, slug, pk): if ( WorkspaceMember.objects.filter( - member=request.user, workspace__slug=slug, is_active=True, role=Admin + member=request.user, + workspace__slug=slug, + is_active=True, + role=ROLE.ADMIN.value, ).exists() or ProjectMember.objects.filter( member=request.user, workspace__slug=slug, project_id=pk, - role=Admin, + role=ROLE.ADMIN.value, is_active=True, ).exists() ): diff --git a/apps/api/plane/db/models/project.py b/apps/api/plane/db/models/project.py index e58f60e804b..0163be80e88 100644 --- a/apps/api/plane/db/models/project.py +++ b/apps/api/plane/db/models/project.py @@ -15,7 +15,13 @@ # Module imports from .base import BaseModel -ROLE_CHOICES = ((20, "Admin"), (15, "Member"), (5, "Guest")) +ROLE_CHOICES = (("Admin", 20), ("Member", 15), ("Guest", 5)) + + +class ROLE(Enum): + ADMIN = 20 + MEMBER = 15 + GUEST = 5 class ProjectNetwork(Enum): From f914c787598fbb16f699d4e0ca8511417d47ae41 Mon Sep 17 00:00:00 2001 From: sangeethailango Date: Wed, 10 Sep 2025 15:18:23 +0530 Subject: [PATCH 6/6] chore: rearrange role_choices --- apps/api/plane/app/views/project/base.py | 2 +- apps/api/plane/db/models/project.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/api/plane/app/views/project/base.py b/apps/api/plane/app/views/project/base.py index ce3cb4134cb..d4eeca2f7b9 100644 --- a/apps/api/plane/app/views/project/base.py +++ b/apps/api/plane/app/views/project/base.py @@ -208,7 +208,7 @@ def list(self, request, slug): member=request.user, workspace__slug=slug, is_active=True, - role=ROLE.GUEST.value, + role=ROLE.MEMBER.value, ).exists(): projects = projects.filter( Q( diff --git a/apps/api/plane/db/models/project.py b/apps/api/plane/db/models/project.py index 0163be80e88..af576be6e3e 100644 --- a/apps/api/plane/db/models/project.py +++ b/apps/api/plane/db/models/project.py @@ -15,7 +15,7 @@ # Module imports from .base import BaseModel -ROLE_CHOICES = (("Admin", 20), ("Member", 15), ("Guest", 5)) +ROLE_CHOICES = ((20, "Admin"), (15, "Member"), (5, "Guest")) class ROLE(Enum):