Skip to content

Commit 6b08c22

Browse files
sangeethailangovamsikrishnamathala1akhanBahetiaaryan610sriramveeraghanta
authored
chore: workspace admin access (#4138)
* chore: added access for workspace admin to edit project settings * chore: workspace admin to update members details * chore: workspace admin to label, state, workflow settings * Revert "chore: added access for workspace admin to edit project settings" This reverts commit 803b56514887339d884eaef170de8a9e4ecfda8c. * chore: updated worspace admin access for projects * Revert "chore: workspace admin to update members details" This reverts commit ac465d618d7a89ef696db3484e515957b6b5e264. * Revert "chore: workspace admin to label, state, workflow settings" This reverts commit f01a89604e71792096cbae8e029cac160ea209fb. * chore: workspace admin access in permission classes and decorator * chore: check for teamspace members * chore: refactor permission logic * [WIKI-632] chore: accept additional props for document collaborative editor (#7718) * chore: add collaborative document editor extended props * fix: additional rich text extension props * fix: formatting * chore: add types to the trailing node extension --------- Co-authored-by: Aaryan Khandelwal <[email protected]> * [WEB-4854] chore: project admin accesss to workspace admins (#7749) * chore: project admin accesss to workspace admins * chore: frontend changes * chore: remove console.log * chore: refactor permission decorator * chore: role enum * chore: rearrange role_choices * Potential fix for code scanning alert no. 636: URL redirection from remote source (#7760) Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> * [WEB-4441]fix: members account type dropdown position #7759 * [WEB-4857] fix: applied filters root update #7750 * [WEB-4858]chore: updated content for error page (#7766) * chore: updated content for error page * chore: updated btn url * fix: merge conflicts * fix: merge conflicts * fix: use enum for roles --------- Co-authored-by: vamsikrishnamathala <[email protected]> Co-authored-by: Lakhan Baheti <[email protected]> Co-authored-by: Aaryan Khandelwal <[email protected]> Co-authored-by: sriram veeraghanta <[email protected]> Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> Co-authored-by: Vamsi Krishna <[email protected]>
1 parent 430eedf commit 6b08c22

File tree

16 files changed

+284
-135
lines changed

16 files changed

+284
-135
lines changed

apps/api/plane/app/permissions/base.py

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,14 +52,41 @@ def _wrapped_view(instance, request, *args, **kwargs):
5252
).exists():
5353
return view_func(instance, request, *args, **kwargs)
5454
else:
55-
if ProjectMember.objects.filter(
55+
is_user_has_allowed_role = ProjectMember.objects.filter(
5656
member=request.user,
5757
workspace__slug=kwargs["slug"],
5858
project_id=kwargs["project_id"],
5959
role__in=allowed_role_values,
6060
is_active=True,
61-
).exists():
61+
).exists()
62+
63+
# Return if the user has allowed roles
64+
if is_user_has_allowed_role:
6265
return view_func(instance, request, *args, **kwargs)
66+
67+
# Return if the user is workspace admin and is also part of the project or a teamspace member
68+
if WorkspaceMember.objects.filter(
69+
member=request.user,
70+
workspace__slug=kwargs["slug"],
71+
role=ROLE.ADMIN.value,
72+
is_active=True,
73+
).exists():
74+
teamspace_ids = TeamspaceProject.objects.filter(
75+
workspace__slug=kwargs["slug"], project_id=kwargs["project_id"]
76+
).values_list("team_space_id", flat=True)
77+
if (
78+
ProjectMember.objects.filter(
79+
member=request.user,
80+
workspace__slug=kwargs["slug"],
81+
project_id=kwargs["project_id"],
82+
is_active=True,
83+
).exists()
84+
or TeamspaceMember.objects.filter(
85+
member=request.user, team_space_id__in=teamspace_ids
86+
).exists()
87+
):
88+
return view_func(instance, request, *args, **kwargs)
89+
6390
#
6491
# Check if the user is member of the team space
6592
# if scope is project further check if user is member of the team space

apps/api/plane/app/permissions/project.py

Lines changed: 22 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,7 @@
77
from plane.ee.models import TeamspaceProject, TeamspaceMember
88
from plane.payment.flags.flag_decorator import check_workspace_feature_flag
99
from plane.payment.flags.flag import FeatureFlag
10-
11-
12-
# Permission Role Levels
13-
# These constants define the permission levels in the system
14-
Admin: int = 20 # Administrator level access
15-
Member: int = 15 # Regular member level access
16-
Guest: int = 5 # Guest/restricted level access
10+
from plane.db.models.project import ROLE
1711

1812

1913
def check_teamspace_membership(view, request: Request) -> bool:
@@ -75,18 +69,31 @@ def has_permission(self, request, view) -> bool:
7569
return WorkspaceMember.objects.filter(
7670
workspace__slug=view.workspace_slug,
7771
member=request.user,
78-
role__in=[Admin, Member],
72+
role__in=[ROLE.ADMIN.value, ROLE.MEMBER.value],
7973
is_active=True,
8074
).exists()
8175

82-
## Only Project Admins can update project attributes
83-
return ProjectMember.objects.filter(
76+
project_member_qs = ProjectMember.objects.filter(
8477
workspace__slug=view.workspace_slug,
8578
member=request.user,
86-
role=Admin,
8779
project_id=view.project_id,
8880
is_active=True,
89-
).exists()
81+
)
82+
83+
## Only project admins or workspace admin who is part of the project can access
84+
85+
if project_member_qs.filter(role=ROLE.ADMIN.value).exists():
86+
return True
87+
else:
88+
return (
89+
project_member_qs.exists()
90+
and WorkspaceMember.objects.filter(
91+
member=request.user,
92+
workspace__slug=view.workspace_slug,
93+
role=ROLE.ADMIN.value,
94+
is_active=True,
95+
).exists()
96+
)
9097

9198

9299
class ProjectMemberPermission(BasePermission):
@@ -112,15 +119,15 @@ def has_permission(self, request, view) -> bool:
112119
return WorkspaceMember.objects.filter(
113120
workspace__slug=view.workspace_slug,
114121
member=request.user,
115-
role__in=[Admin, Member],
122+
role__in=[ROLE.ADMIN.value, ROLE.MEMBER.value],
116123
is_active=True,
117124
).exists()
118125

119126
## Only Project Admins can update project attributes
120127
is_project_member = ProjectMember.objects.filter(
121128
workspace__slug=view.workspace_slug,
122129
member=request.user,
123-
role__in=[Admin, Member],
130+
role__in=[ROLE.ADMIN.value, ROLE.MEMBER.value],
124131
project_id=view.project_id,
125132
is_active=True,
126133
).exists()
@@ -180,7 +187,7 @@ def has_permission(self, request, view) -> bool:
180187
is_project_member = ProjectMember.objects.filter(
181188
workspace__slug=view.workspace_slug,
182189
member=request.user,
183-
role__in=[Admin, Member],
190+
role__in=[ROLE.ADMIN.value, ROLE.MEMBER.value],
184191
project_id=view.project_id,
185192
is_active=True,
186193
).exists()

apps/api/plane/app/views/project/base.py

Lines changed: 53 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,14 @@
55
import uuid
66

77
# Django imports
8-
from django.db import IntegrityError
98
from django.db.models import Exists, F, OuterRef, Prefetch, Q, Subquery
109
from django.core.serializers.json import DjangoJSONEncoder
10+
from django.db import IntegrityError
1111

1212

1313
# Third Party imports
1414
from rest_framework.response import Response
15-
from rest_framework import serializers, status
15+
from rest_framework import status
1616
from rest_framework.permissions import AllowAny
1717

1818
# Module imports
@@ -229,7 +229,6 @@ def list_detail(self, request, slug):
229229
project_projectmember__member=self.request.user,
230230
project_projectmember__is_active=True,
231231
)
232-
233232
teamspace_project_ids = self.get_teamspace_project_ids(request, slug)
234233
teamspace_projects = base_queryset.filter(pk__in=teamspace_project_ids)
235234

@@ -306,7 +305,10 @@ def list(self, request, slug):
306305
)
307306

308307
if WorkspaceMember.objects.filter(
309-
member=request.user, workspace__slug=slug, is_active=True, role=5
308+
member=request.user,
309+
workspace__slug=slug,
310+
is_active=True,
311+
role=ROLE.GUEST.value,
310312
).exists():
311313
# For role 5 (MEMBER): direct memberships + teamspace memberships
312314
direct_projects = base_queryset.filter(
@@ -320,7 +322,10 @@ def list(self, request, slug):
320322
projects = direct_projects.union(teamspace_projects)
321323

322324
elif WorkspaceMember.objects.filter(
323-
member=request.user, workspace__slug=slug, is_active=True, role=15
325+
member=request.user,
326+
workspace__slug=slug,
327+
is_active=True,
328+
role=ROLE.MEMBER.value,
324329
).exists():
325330
# For role 15 (GUEST): direct memberships + public projects + teamspace memberships
326331
direct_projects = base_queryset.filter(
@@ -412,29 +417,31 @@ def create(self, request, slug):
412417
if serializer.is_valid():
413418
serializer.save()
414419

415-
# Add the user as Administrator to the project
416-
_ = ProjectMember.objects.create(
417-
project_id=serializer.data["id"], member=request.user, role=20
420+
# Add the user as Administrator to the project
421+
_ = ProjectMember.objects.create(
422+
project_id=serializer.data["id"],
423+
member=request.user,
424+
role=ROLE.ADMIN.value,
425+
)
426+
# Also create the issue property for the user
427+
_ = IssueUserProperty.objects.create(
428+
project_id=serializer.data["id"], user=request.user
429+
)
430+
431+
if serializer.data["project_lead"] is not None and str(
432+
serializer.data["project_lead"]
433+
) != str(request.user.id):
434+
ProjectMember.objects.create(
435+
project_id=serializer.data["id"],
436+
member_id=serializer.data["project_lead"],
437+
role=ROLE.ADMIN.value,
418438
)
419439
# Also create the issue property for the user
420-
_ = IssueUserProperty.objects.create(
421-
project_id=serializer.data["id"], user=request.user
440+
IssueUserProperty.objects.create(
441+
project_id=serializer.data["id"],
442+
user_id=serializer.data["project_lead"],
422443
)
423444

424-
if serializer.data["project_lead"] is not None and str(
425-
serializer.data["project_lead"]
426-
) != str(request.user.id):
427-
ProjectMember.objects.create(
428-
project_id=serializer.data["id"],
429-
member_id=serializer.data["project_lead"],
430-
role=20,
431-
)
432-
# Also create the issue property for the user
433-
IssueUserProperty.objects.create(
434-
project_id=serializer.data["id"],
435-
user_id=serializer.data["project_lead"],
436-
)
437-
438445
# Default states
439446
states = [
440447
{
@@ -572,14 +579,23 @@ def create(self, request, slug):
572579
)
573580

574581
def partial_update(self, request, slug, pk=None):
582+
is_workspace_admin = WorkspaceMember.objects.filter(
583+
member=request.user,
584+
workspace__slug=slug,
585+
is_active=True,
586+
role=ROLE.ADMIN.value,
587+
).exists()
588+
589+
is_project_admin = ProjectMember.objects.filter(
590+
member=request.user,
591+
workspace__slug=slug,
592+
project_id=pk,
593+
role=ROLE.ADMIN.value,
594+
is_active=True,
595+
).exists()
575596
try:
576-
if not ProjectMember.objects.filter(
577-
member=request.user,
578-
workspace__slug=slug,
579-
project_id=pk,
580-
role=20,
581-
is_active=True,
582-
).exists():
597+
# Return error for if the user is neither workspace admin nor project admin
598+
if not is_project_admin and not is_workspace_admin:
583599
return Response(
584600
{"error": "You don't have the required permissions."},
585601
status=status.HTTP_403_FORBIDDEN,
@@ -633,7 +649,7 @@ def partial_update(self, request, slug, pk=None):
633649
project_id=pk,
634650
workspace_id=workspace.id,
635651
member_id=api_token.user_id,
636-
role=20,
652+
role=ROLE.ADMIN.value,
637653
)
638654

639655
# EE: project_grouping starts
@@ -750,13 +766,16 @@ def partial_update(self, request, slug, pk=None):
750766
def destroy(self, request, slug, pk):
751767
if (
752768
WorkspaceMember.objects.filter(
753-
member=request.user, workspace__slug=slug, is_active=True, role=20
769+
member=request.user,
770+
workspace__slug=slug,
771+
is_active=True,
772+
role=ROLE.ADMIN.value,
754773
).exists()
755774
or ProjectMember.objects.filter(
756775
member=request.user,
757776
workspace__slug=slug,
758777
project_id=pk,
759-
role=20,
778+
role=ROLE.ADMIN.value,
760779
is_active=True,
761780
).exists()
762781
):

apps/api/plane/db/models/project.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,12 @@
2323
ROLE_CHOICES = ((20, "Admin"), (15, "Member"), (5, "Guest"))
2424

2525

26+
class ROLE(Enum):
27+
ADMIN = 20
28+
MEMBER = 15
29+
GUEST = 5
30+
31+
2632
class ProjectNetwork(Enum):
2733
SECRET = 0
2834
PUBLIC = 2

apps/api/plane/utils/path_validator.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,20 @@
33

44

55
def validate_next_path(next_path: str) -> str:
6-
"""Validates that next_path is a valid path and extracts only the path component."""
6+
"""Validates that next_path is a safe relative path for redirection."""
7+
# Browsers interpret backslashes as forward slashes. Remove all backslashes.
8+
next_path = next_path.replace("\\", "")
79
parsed_url = urlparse(next_path)
810

9-
# Ensure next_path is not an absolute URL
11+
# Block absolute URLs or anything with scheme/netloc
1012
if parsed_url.scheme or parsed_url.netloc:
1113
next_path = parsed_url.path # Extract only the path component
1214

13-
# Ensure it starts with a forward slash (indicating a valid relative path)
14-
if not next_path.startswith("/"):
15+
# Must start with a forward slash and not be empty
16+
if not next_path or not next_path.startswith("/"):
1517
return ""
1618

17-
# Ensure it does not contain dangerous path traversal sequences
19+
# Prevent path traversal
1820
if ".." in next_path:
1921
return ""
2022

0 commit comments

Comments
 (0)