From eef66573e6a3612c3be439ab9905b4f45414900e Mon Sep 17 00:00:00 2001 From: gakshita Date: Mon, 24 Mar 2025 14:46:38 +0530 Subject: [PATCH 1/8] fix: private project join issue --- web/core/layouts/auth-layout/project-wrapper.tsx | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/web/core/layouts/auth-layout/project-wrapper.tsx b/web/core/layouts/auth-layout/project-wrapper.tsx index e79bdd1897a..15466dbaa8b 100644 --- a/web/core/layouts/auth-layout/project-wrapper.tsx +++ b/web/core/layouts/auth-layout/project-wrapper.tsx @@ -7,6 +7,7 @@ import useSWR from "swr"; import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; // components +import { EProjectNetwork } from "@plane/types/src/enums"; import { JoinProject } from "@/components/auth-screens"; import { LogoSpinner } from "@/components/common"; import { ComicBoxButton, DetailedEmptyState } from "@/components/empty-state"; @@ -70,6 +71,11 @@ export const ProjectAuthWrapper: FC = observer((props) => { workspaceSlug.toString(), projectId?.toString() ); + const isWorkspaceAdmin = allowPermissions( + [EUserPermissions.ADMIN], + EUserPermissionsLevel.WORKSPACE, + workspaceSlug.toString() + ); // Initialize module timeline chart useEffect(() => { @@ -168,7 +174,12 @@ export const ProjectAuthWrapper: FC = observer((props) => { ); // check if the user don't have permission to access the project - if (projectExists && projectId && hasPermissionToCurrentProject === false) return ; + if ( + (projectExists?.network !== EProjectNetwork.PRIVATE || isWorkspaceAdmin) && + projectId && + hasPermissionToCurrentProject === false + ) + return ; // check if the project info is not found. if (loader === "loaded" && !projectExists && projectId && !!hasPermissionToCurrentProject === false) From aa388975d2c18d2e9dae533a9f46c86b6a5a0e10 Mon Sep 17 00:00:00 2001 From: sangeethailango Date: Mon, 24 Mar 2025 17:24:41 +0530 Subject: [PATCH 2/8] chore: return network value --- apiserver/plane/app/views/project/base.py | 1 + 1 file changed, 1 insertion(+) diff --git a/apiserver/plane/app/views/project/base.py b/apiserver/plane/app/views/project/base.py index 00e1ad1eb13..b4f0b3aad10 100644 --- a/apiserver/plane/app/views/project/base.py +++ b/apiserver/plane/app/views/project/base.py @@ -179,6 +179,7 @@ def list(self, request, slug): "inbox_view", "guest_view_all_features", "project_lead", + "network", "created_at", "updated_at", "created_by", From 19341d6b64cf362217fd0266edb50b0e1b0380fb Mon Sep 17 00:00:00 2001 From: gakshita Date: Mon, 24 Mar 2025 18:46:02 +0530 Subject: [PATCH 3/8] fix: refactor --- packages/types/src/enums.ts | 6 ++++++ web/core/layouts/auth-layout/project-wrapper.tsx | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/types/src/enums.ts b/packages/types/src/enums.ts index 854c0c61405..53138a1d798 100644 --- a/packages/types/src/enums.ts +++ b/packages/types/src/enums.ts @@ -6,6 +6,12 @@ export enum EUserPermissions { export type TUserPermissions = EUserPermissions.ADMIN | EUserPermissions.MEMBER | EUserPermissions.GUEST; +// project network +export enum EProjectNetwork { + PRIVATE = 0, + PUBLIC = 2, +} + // project pages export enum EPageAccess { PUBLIC = 0, diff --git a/web/core/layouts/auth-layout/project-wrapper.tsx b/web/core/layouts/auth-layout/project-wrapper.tsx index 15466dbaa8b..6e6371849e2 100644 --- a/web/core/layouts/auth-layout/project-wrapper.tsx +++ b/web/core/layouts/auth-layout/project-wrapper.tsx @@ -175,7 +175,7 @@ export const ProjectAuthWrapper: FC = observer((props) => { // check if the user don't have permission to access the project if ( - (projectExists?.network !== EProjectNetwork.PRIVATE || isWorkspaceAdmin) && + ((projectExists?.network && projectExists?.network !== EProjectNetwork.PRIVATE) || isWorkspaceAdmin) && projectId && hasPermissionToCurrentProject === false ) From bdca9f687f88687e30f79ff0c819ec0554147088 Mon Sep 17 00:00:00 2001 From: gakshita Date: Mon, 24 Mar 2025 20:02:21 +0530 Subject: [PATCH 4/8] fix: refactor --- web/core/layouts/auth-layout/project-wrapper.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/core/layouts/auth-layout/project-wrapper.tsx b/web/core/layouts/auth-layout/project-wrapper.tsx index 6e6371849e2..c356cb883ba 100644 --- a/web/core/layouts/auth-layout/project-wrapper.tsx +++ b/web/core/layouts/auth-layout/project-wrapper.tsx @@ -182,7 +182,7 @@ export const ProjectAuthWrapper: FC = observer((props) => { return ; // check if the project info is not found. - if (loader === "loaded" && !projectExists && projectId && !!hasPermissionToCurrentProject === false) + if (loader === "loaded" && projectId && !!hasPermissionToCurrentProject === false) return (
Date: Tue, 25 Mar 2025 14:03:34 +0530 Subject: [PATCH 5/8] fix: type --- packages/types/src/project/projects.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/types/src/project/projects.d.ts b/packages/types/src/project/projects.d.ts index 40562d362d5..e1d9117a1be 100644 --- a/packages/types/src/project/projects.d.ts +++ b/packages/types/src/project/projects.d.ts @@ -27,6 +27,7 @@ export interface IPartialProject { inbox_view: boolean; guest_view_all_features?: boolean; project_lead?: IUserLite | string | null; + network?: number; // Timestamps created_at?: Date; updated_at?: Date; @@ -50,7 +51,6 @@ export interface IProject extends IPartialProject { anchor?: string | null; is_favorite?: boolean; members?: string[]; - network?: number; timezone?: string; } From db792f7f4f0d9341a50a13c31b75996a8039a026 Mon Sep 17 00:00:00 2001 From: NarayanBavisetti Date: Tue, 25 Mar 2025 15:25:13 +0530 Subject: [PATCH 6/8] chore: added restricition for private projects --- apiserver/plane/app/views/project/invite.py | 26 +++++++++++++++------ apiserver/plane/db/models/project.py | 22 +++++++++++++++++ 2 files changed, 41 insertions(+), 7 deletions(-) diff --git a/apiserver/plane/app/views/project/invite.py b/apiserver/plane/app/views/project/invite.py index e4d46e89f4e..409eee18ed0 100644 --- a/apiserver/plane/app/views/project/invite.py +++ b/apiserver/plane/app/views/project/invite.py @@ -16,17 +16,17 @@ # Module imports from .base import BaseViewSet, BaseAPIView from plane.app.serializers import ProjectMemberInviteSerializer - from plane.app.permissions import allow_permission, ROLE - from plane.db.models import ( ProjectMember, Workspace, ProjectMemberInvite, User, WorkspaceMember, + Project, IssueUserProperty, ) +from plane.db.models.project import ProjectNetwork class ProjectInvitationsViewset(BaseViewSet): @@ -128,6 +128,7 @@ def get_queryset(self): .select_related("workspace", "workspace__owner", "project") ) + @allow_permission([ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE") def create(self, request, slug): project_ids = request.data.get("project_ids", []) @@ -136,11 +137,22 @@ def create(self, request, slug): member=request.user, workspace__slug=slug, is_active=True ) - if workspace_member.role not in [ROLE.ADMIN.value, ROLE.MEMBER.value]: - return Response( - {"error": "You do not have permission to join the project"}, - status=status.HTTP_403_FORBIDDEN, - ) + # Get all the projects + projects = ( + Project.objects.filter(id__in=project_ids, workspace__slug=slug) + .only("id", "network") + ) + + # Check if user has permission to join each project + for project in projects: + if ( + project.network == ProjectNetwork.SECRET + and workspace_member.role != ROLE.ADMIN.value + ): + return Response( + {"error": "Only workspace admins can join private project"}, + status=status.HTTP_403_FORBIDDEN, + ) workspace_role = workspace_member.role workspace = workspace_member.workspace diff --git a/apiserver/plane/db/models/project.py b/apiserver/plane/db/models/project.py index c97c550ee21..c337054c3d4 100644 --- a/apiserver/plane/db/models/project.py +++ b/apiserver/plane/db/models/project.py @@ -1,6 +1,7 @@ # Python imports import pytz from uuid import uuid4 +from enum import Enum # Django imports from django.conf import settings @@ -17,6 +18,15 @@ ROLE_CHOICES = ((20, "Admin"), (15, "Member"), (5, "Guest")) +class ProjectNetwork(Enum): + SECRET = 0 + PUBLIC = 2 + + @classmethod + def choices(cls): + return [(0, "Secret"), (2, "Public")] + + def get_default_props(): return { "filters": { @@ -113,6 +123,18 @@ class Project(BaseModel): TIMEZONE_CHOICES = tuple(zip(pytz.all_timezones, pytz.all_timezones)) timezone = models.CharField(max_length=255, default="UTC", choices=TIMEZONE_CHOICES) + @property + def is_secret(self) -> bool: + return self.network == ProjectNetwork.SECRET + + @property + def is_public(self) -> bool: + return self.network == ProjectNetwork.PUBLIC + + @property + def network_type(self) -> ProjectNetwork: + return ProjectNetwork(self.network) + @property def cover_image_url(self): # Return cover image url From 5f9d3ef6a5ac4c07b38880b2d0628412bb7c09bf Mon Sep 17 00:00:00 2001 From: NarayanBavisetti Date: Tue, 25 Mar 2025 15:27:37 +0530 Subject: [PATCH 7/8] chore: removed extra validations --- apiserver/plane/db/models/project.py | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/apiserver/plane/db/models/project.py b/apiserver/plane/db/models/project.py index c337054c3d4..c4d097ac8f3 100644 --- a/apiserver/plane/db/models/project.py +++ b/apiserver/plane/db/models/project.py @@ -123,18 +123,6 @@ class Project(BaseModel): TIMEZONE_CHOICES = tuple(zip(pytz.all_timezones, pytz.all_timezones)) timezone = models.CharField(max_length=255, default="UTC", choices=TIMEZONE_CHOICES) - @property - def is_secret(self) -> bool: - return self.network == ProjectNetwork.SECRET - - @property - def is_public(self) -> bool: - return self.network == ProjectNetwork.PUBLIC - - @property - def network_type(self) -> ProjectNetwork: - return ProjectNetwork(self.network) - @property def cover_image_url(self): # Return cover image url From 832f4769921687fb21471c77089019c363573565 Mon Sep 17 00:00:00 2001 From: NarayanBavisetti Date: Tue, 25 Mar 2025 16:24:26 +0530 Subject: [PATCH 8/8] chore: added value to access enum --- apiserver/plane/app/views/project/invite.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/apiserver/plane/app/views/project/invite.py b/apiserver/plane/app/views/project/invite.py index 409eee18ed0..51eb997f661 100644 --- a/apiserver/plane/app/views/project/invite.py +++ b/apiserver/plane/app/views/project/invite.py @@ -138,15 +138,13 @@ def create(self, request, slug): ) # Get all the projects - projects = ( - Project.objects.filter(id__in=project_ids, workspace__slug=slug) - .only("id", "network") - ) - + projects = Project.objects.filter( + id__in=project_ids, workspace__slug=slug + ).only("id", "network") # Check if user has permission to join each project for project in projects: if ( - project.network == ProjectNetwork.SECRET + project.network == ProjectNetwork.SECRET.value and workspace_member.role != ROLE.ADMIN.value ): return Response(