SECURITY - МЕЖПРОЕКТНАЯ КОММУНИКАЦИЯ: ограничение создания проектов

This commit is contained in:
DCCONSTRUCTIONS 2026-05-12 22:24:36 +03:00
parent 87e1857f53
commit 5e7c9e08a0
18 changed files with 251 additions and 70 deletions

View File

@ -34,7 +34,7 @@ class ProjectBasePermission(BasePermission):
return WorkspaceMember.objects.filter( return WorkspaceMember.objects.filter(
workspace__slug=view.workspace_slug, workspace__slug=view.workspace_slug,
member=request.user, member=request.user,
role__in=[ROLE.ADMIN.value, ROLE.MEMBER.value], role=ROLE.ADMIN.value,
is_active=True, is_active=True,
).exists() ).exists()
@ -78,7 +78,7 @@ class ProjectMemberPermission(BasePermission):
return WorkspaceMember.objects.filter( return WorkspaceMember.objects.filter(
workspace__slug=view.workspace_slug, workspace__slug=view.workspace_slug,
member=request.user, member=request.user,
role__in=[ROLE.ADMIN.value, ROLE.MEMBER.value], role=ROLE.ADMIN.value,
is_active=True, is_active=True,
).exists() ).exists()

View File

@ -25,6 +25,10 @@ from plane.app.serializers import (
from plane.app.views.base import BaseAPIView, BaseViewSet from plane.app.views.base import BaseAPIView, BaseViewSet
from plane.bgtasks.recent_visited_task import recent_visited_task from plane.bgtasks.recent_visited_task import recent_visited_task
from plane.bgtasks.webhook_task import model_activity, webhook_activity from plane.bgtasks.webhook_task import model_activity, webhook_activity
from plane.authentication.nodedc_project_memberships import (
ensure_project_admin_membership,
ensure_workspace_admin_project_memberships,
)
from plane.db.models import ( from plane.db.models import (
UserFavorite, UserFavorite,
DeployBoard, DeployBoard,
@ -49,6 +53,20 @@ class ProjectViewSet(BaseViewSet):
webhook_event = "project" webhook_event = "project"
use_read_replica = True use_read_replica = True
def ensure_workspace_admin_project_access(self, request, slug):
workspace_member = (
WorkspaceMember.objects.filter(
member=request.user,
workspace__slug=slug,
is_active=True,
role=ROLE.ADMIN.value,
)
.select_related("workspace")
.first()
)
if workspace_member is not None:
ensure_workspace_admin_project_memberships(workspace_member.workspace)
def get_queryset(self): def get_queryset(self):
sort_order = ProjectUserProperty.objects.filter( sort_order = ProjectUserProperty.objects.filter(
user=self.request.user, user=self.request.user,
@ -99,6 +117,7 @@ class ProjectViewSet(BaseViewSet):
@allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE") @allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE")
def list_detail(self, request, slug): def list_detail(self, request, slug):
self.ensure_workspace_admin_project_access(request, slug)
fields = [field for field in request.GET.get("fields", "").split(",") if field] fields = [field for field in request.GET.get("fields", "").split(",") if field]
projects = self.get_queryset().order_by("sort_order", "name") projects = self.get_queryset().order_by("sort_order", "name")
if WorkspaceMember.objects.filter( if WorkspaceMember.objects.filter(
@ -119,11 +138,8 @@ class ProjectViewSet(BaseViewSet):
role=ROLE.MEMBER.value, role=ROLE.MEMBER.value,
).exists(): ).exists():
projects = projects.filter( projects = projects.filter(
Q( project_projectmember__member=self.request.user,
project_projectmember__member=self.request.user, project_projectmember__is_active=True,
project_projectmember__is_active=True,
)
| Q(network=2)
) )
if request.GET.get("per_page", False) and request.GET.get("cursor", False): if request.GET.get("per_page", False) and request.GET.get("cursor", False):
@ -139,6 +155,7 @@ class ProjectViewSet(BaseViewSet):
@allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE") @allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE")
def list(self, request, slug): def list(self, request, slug):
self.ensure_workspace_admin_project_access(request, slug)
sort_order = ProjectUserProperty.objects.filter( sort_order = ProjectUserProperty.objects.filter(
user=self.request.user, user=self.request.user,
project_id=OuterRef("pk"), project_id=OuterRef("pk"),
@ -209,11 +226,8 @@ class ProjectViewSet(BaseViewSet):
role=ROLE.MEMBER.value, role=ROLE.MEMBER.value,
).exists(): ).exists():
projects = projects.filter( projects = projects.filter(
Q( project_projectmember__member=self.request.user,
project_projectmember__member=self.request.user, project_projectmember__is_active=True,
project_projectmember__is_active=True,
)
| Q(network=2)
) )
return Response(projects, status=status.HTTP_200_OK) return Response(projects, status=status.HTTP_200_OK)
@ -227,7 +241,14 @@ class ProjectViewSet(BaseViewSet):
member_ids = [str(project_member.member_id) for project_member in project.members_list] member_ids = [str(project_member.member_id) for project_member in project.members_list]
if str(request.user.id) not in member_ids: if str(request.user.id) not in member_ids:
if project.network == ProjectNetwork.SECRET.value: if WorkspaceMember.objects.filter(
member=request.user,
workspace__slug=slug,
is_active=True,
role=ROLE.ADMIN.value,
).exists():
ensure_project_admin_membership(project, request.user)
elif project.network == ProjectNetwork.SECRET.value:
return Response( return Response(
{"error": "You do not have permission"}, {"error": "You do not have permission"},
status=status.HTTP_403_FORBIDDEN, status=status.HTTP_403_FORBIDDEN,
@ -249,7 +270,7 @@ class ProjectViewSet(BaseViewSet):
serializer = ProjectListSerializer(project) serializer = ProjectListSerializer(project)
return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.data, status=status.HTTP_200_OK)
@allow_permission([ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE") @allow_permission([ROLE.ADMIN], level="WORKSPACE")
def create(self, request, slug): def create(self, request, slug):
workspace = Workspace.objects.get(slug=slug) workspace = Workspace.objects.get(slug=slug)
@ -290,6 +311,7 @@ class ProjectViewSet(BaseViewSet):
) )
project = self.get_queryset().filter(pk=serializer.data["id"]).first() project = self.get_queryset().filter(pk=serializer.data["id"]).first()
ensure_workspace_admin_project_memberships(workspace, project=project)
# Create the model activity # Create the model activity
model_activity.delay( model_activity.delay(

View File

@ -11,6 +11,10 @@ from django.db.models import Min
from .base import BaseViewSet, BaseAPIView from .base import BaseViewSet, BaseAPIView
from plane.app.realtime.issue_events import publish_assignee_cleanup_issue_events_on_commit from plane.app.realtime.issue_events import publish_assignee_cleanup_issue_events_on_commit
from plane.app.realtime.nodedc_events import publish_nodedc_workspace_event_on_commit from plane.app.realtime.nodedc_events import publish_nodedc_workspace_event_on_commit
from plane.authentication.nodedc_project_memberships import (
ensure_project_admin_membership,
ensure_workspace_admin_project_memberships,
)
from plane.authentication.nodedc_workspace_policy import ( from plane.authentication.nodedc_workspace_policy import (
is_nodedc_launcher_managed_workspace, is_nodedc_launcher_managed_workspace,
nodedc_launcher_managed_workspace_response, nodedc_launcher_managed_workspace_response,
@ -431,12 +435,32 @@ class ProjectMemberViewSet(BaseViewSet):
class ProjectMemberUserEndpoint(BaseAPIView): class ProjectMemberUserEndpoint(BaseAPIView):
def get(self, request, slug, project_id): def get(self, request, slug, project_id):
project_member = ProjectMember.objects.get( project_member = ProjectMember.objects.filter(
project_id=project_id, project_id=project_id,
workspace__slug=slug, workspace__slug=slug,
member=request.user, member=request.user,
is_active=True, is_active=True,
) ).first()
if project_member is None:
project = Project.objects.filter(pk=project_id, workspace__slug=slug).select_related("workspace").first()
is_workspace_admin = WorkspaceMember.objects.filter(
workspace__slug=slug,
member=request.user,
is_active=True,
role=ROLE.ADMIN.value,
).exists()
if project is not None and is_workspace_admin:
ensure_project_admin_membership(project, request.user)
project_member = ProjectMember.objects.filter(
project=project,
member=request.user,
is_active=True,
).first()
if project_member is None:
return Response({"error": "Project member not found"}, status=status.HTTP_404_NOT_FOUND)
serializer = ProjectMemberSerializer(project_member) serializer = ProjectMemberSerializer(project_member)
return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.data, status=status.HTTP_200_OK)
@ -447,6 +471,19 @@ class UserProjectRolesEndpoint(BaseAPIView):
use_read_replica = True use_read_replica = True
def get(self, request, slug): def get(self, request, slug):
workspace_member = (
WorkspaceMember.objects.filter(
workspace__slug=slug,
member=request.user,
is_active=True,
)
.select_related("workspace")
.first()
)
if workspace_member is not None and workspace_member.role == ROLE.ADMIN.value:
ensure_workspace_admin_project_memberships(workspace_member.workspace)
project_members = ProjectMember.objects.filter( project_members = ProjectMember.objects.filter(
workspace__slug=slug, workspace__slug=slug,
member_id=request.user.id, member_id=request.user.id,

View File

@ -0,0 +1,113 @@
ADMIN_ROLE = 20
AUTO_ADMIN_COMMENT = "nodedc:workspace-admin"
AUTO_ADMIN_CREATED_COMMENT = f"{AUTO_ADMIN_COMMENT}:created"
AUTO_ADMIN_PREVIOUS_PREFIX = f"{AUTO_ADMIN_COMMENT}:previous:"
def get_auto_admin_previous_role(comment):
if not isinstance(comment, str) or not comment.startswith(AUTO_ADMIN_PREVIOUS_PREFIX):
return None
try:
previous_role = int(comment.replace(AUTO_ADMIN_PREVIOUS_PREFIX, "", 1))
except ValueError:
return None
return previous_role if previous_role in {5, 15, ADMIN_ROLE} else None
def ensure_project_admin_membership(project, user):
from plane.db.models import ProjectMember
project_member = ProjectMember.objects.filter(
project=project,
member=user,
deleted_at__isnull=True,
).first()
if project_member is None:
ProjectMember.objects.create(
workspace=project.workspace,
project=project,
member=user,
role=ADMIN_ROLE,
is_active=True,
comment=AUTO_ADMIN_CREATED_COMMENT,
)
return 1
update_fields = []
if project_member.role != ADMIN_ROLE:
project_member.comment = f"{AUTO_ADMIN_PREVIOUS_PREFIX}{project_member.role}"
project_member.role = ADMIN_ROLE
update_fields.extend(["comment", "role"])
if not project_member.is_active:
project_member.is_active = True
update_fields.append("is_active")
if update_fields:
update_fields.append("updated_at")
project_member.save(update_fields=update_fields)
return 1
return 0
def revoke_auto_project_admin_memberships(workspace, user):
from plane.db.models import ProjectMember
revoked = 0
project_memberships = ProjectMember.objects.filter(
project__workspace=workspace,
member=user,
role=ADMIN_ROLE,
deleted_at__isnull=True,
comment__startswith=AUTO_ADMIN_COMMENT,
)
for project_member in project_memberships:
previous_role = get_auto_admin_previous_role(project_member.comment)
if project_member.comment == AUTO_ADMIN_CREATED_COMMENT or previous_role is None:
project_member.is_active = False
project_member.save(update_fields=["is_active", "updated_at"])
else:
project_member.role = previous_role
project_member.comment = None
project_member.is_active = True
project_member.save(update_fields=["role", "comment", "is_active", "updated_at"])
revoked += 1
return revoked
def ensure_user_admin_project_memberships(workspace, user):
from plane.db.models import Project
restored = 0
for project in Project.objects.filter(workspace=workspace, deleted_at__isnull=True).select_related("workspace"):
restored += ensure_project_admin_membership(project, user)
return restored
def ensure_workspace_admin_project_memberships(workspace, project=None):
from plane.db.models import WorkspaceMember
admin_memberships = (
WorkspaceMember.objects.filter(
workspace=workspace,
role=ADMIN_ROLE,
is_active=True,
deleted_at__isnull=True,
member__is_bot=False,
)
.select_related("member")
.order_by("created_at")
)
restored = 0
for workspace_member in admin_memberships:
if project is not None:
restored += ensure_project_admin_membership(project, workspace_member.member)
else:
restored += ensure_user_admin_project_memberships(workspace, workspace_member.member)
return restored

View File

@ -9,6 +9,10 @@ from django.utils.decorators import method_decorator
from django.views import View from django.views import View
from django.views.decorators.csrf import csrf_exempt from django.views.decorators.csrf import csrf_exempt
from plane.authentication.nodedc_project_memberships import (
ensure_user_admin_project_memberships,
revoke_auto_project_admin_memberships,
)
from plane.authentication.views.nodedc_logout import is_internal_logout_request_authorized from plane.authentication.views.nodedc_logout import is_internal_logout_request_authorized
from plane.app.realtime.issue_events import publish_assignee_cleanup_issue_events_on_commit from plane.app.realtime.issue_events import publish_assignee_cleanup_issue_events_on_commit
from plane.app.realtime.nodedc_events import ( from plane.app.realtime.nodedc_events import (
@ -332,24 +336,7 @@ def serialize_project_membership(project_member, created):
def restore_admin_project_memberships(workspace, user): def restore_admin_project_memberships(workspace, user):
restored = 0 return ensure_user_admin_project_memberships(workspace, user)
for project_member in ProjectMember.objects.filter(
project__workspace=workspace,
member=user,
deleted_at__isnull=True,
):
update_fields = []
if project_member.role != ADMIN_ROLE:
project_member.role = ADMIN_ROLE
update_fields.append("role")
if not project_member.is_active:
project_member.is_active = True
update_fields.append("is_active")
if update_fields:
update_fields.append("updated_at")
project_member.save(update_fields=update_fields)
restored += 1
return restored
@method_decorator(csrf_exempt, name="dispatch") @method_decorator(csrf_exempt, name="dispatch")
@ -441,6 +428,7 @@ class NodeDCInternalWorkspaceMembershipEnsureEndpoint(View):
deleted_at__isnull=True, deleted_at__isnull=True,
).first() ).first()
created = membership is None created = membership is None
previous_role = membership.role if membership is not None else None
if membership is None: if membership is None:
membership = WorkspaceMember.objects.create( membership = WorkspaceMember.objects.create(
@ -463,6 +451,8 @@ class NodeDCInternalWorkspaceMembershipEnsureEndpoint(View):
if role == ADMIN_ROLE: if role == ADMIN_ROLE:
restore_admin_project_memberships(workspace, user) restore_admin_project_memberships(workspace, user)
elif previous_role == ADMIN_ROLE:
revoke_auto_project_admin_memberships(workspace, user)
if set_last_workspace: if set_last_workspace:
profile, _ = Profile.objects.get_or_create(user=user) profile, _ = Profile.objects.get_or_create(user=user)

View File

@ -26,7 +26,7 @@ class ProjectBasePermission(BasePermission):
return WorkspaceMember.objects.filter( return WorkspaceMember.objects.filter(
workspace__slug=view.workspace_slug, workspace__slug=view.workspace_slug,
member=request.user, member=request.user,
role__in=[ROLE.ADMIN.value, ROLE.MEMBER.value], role=ROLE.ADMIN.value,
is_active=True, is_active=True,
).exists() ).exists()
@ -68,7 +68,7 @@ class ProjectMemberPermission(BasePermission):
return WorkspaceMember.objects.filter( return WorkspaceMember.objects.filter(
workspace__slug=view.workspace_slug, workspace__slug=view.workspace_slug,
member=request.user, member=request.user,
role__in=[ROLE.ADMIN.value, ROLE.MEMBER.value], role=ROLE.ADMIN.value,
is_active=True, is_active=True,
).exists() ).exists()

View File

@ -45,7 +45,7 @@ function AnalyticsPage({ params }: Route.ComponentProps) {
// permissions // permissions
const canPerformEmptyStateActions = allowPermissions( const canPerformEmptyStateActions = allowPermissions(
[EUserPermissions.ADMIN, EUserPermissions.MEMBER], [EUserPermissions.ADMIN],
EUserPermissionsLevel.WORKSPACE EUserPermissionsLevel.WORKSPACE
); );

View File

@ -78,7 +78,7 @@ export const ExtendedProjectSidebar = observer(function ExtendedProjectSidebar()
// auth // auth
const isAuthorizedUser = allowPermissions( const isAuthorizedUser = allowPermissions(
[EUserPermissions.ADMIN, EUserPermissions.MEMBER], [EUserPermissions.ADMIN],
EUserPermissionsLevel.WORKSPACE EUserPermissionsLevel.WORKSPACE
); );

View File

@ -9,6 +9,7 @@
import { Menu } from "@headlessui/react"; import { Menu } from "@headlessui/react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { useParams, usePathname } from "next/navigation"; import { useParams, usePathname } from "next/navigation";
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
import { useTranslation } from "@plane/i18n"; import { useTranslation } from "@plane/i18n";
import { PlusIcon, ProjectIcon } from "@plane/propel/icons"; import { PlusIcon, ProjectIcon } from "@plane/propel/icons";
import { TOAST_TYPE, setToast } from "@plane/propel/toast"; import { TOAST_TYPE, setToast } from "@plane/propel/toast";
@ -16,6 +17,7 @@ import { cn, copyUrlToClipboard } from "@plane/utils";
// hooks // hooks
import { useCommandPalette } from "@/hooks/store/use-command-palette"; import { useCommandPalette } from "@/hooks/store/use-command-palette";
import { useProject } from "@/hooks/store/use-project"; import { useProject } from "@/hooks/store/use-project";
import { useUserPermissions } from "@/hooks/store/user";
// components // components
import { SidebarProjectsListItem } from "@/components/workspace/sidebar/projects-list-item"; import { SidebarProjectsListItem } from "@/components/workspace/sidebar/projects-list-item";
@ -29,6 +31,8 @@ export const ProjectsToolbarMenu = observer(function ProjectsToolbarMenu({
const { workspaceSlug } = useParams(); const { workspaceSlug } = useParams();
const { joinedProjectIds } = useProject(); const { joinedProjectIds } = useProject();
const { toggleCreateProjectModal } = useCommandPalette(); const { toggleCreateProjectModal } = useCommandPalette();
const { allowPermissions } = useUserPermissions();
const canCreateProject = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.WORKSPACE);
const handleCopyText = (projectId: string) => const handleCopyText = (projectId: string) =>
copyUrlToClipboard(`${workspaceSlug}/projects/${projectId}/issues`).then(() => { copyUrlToClipboard(`${workspaceSlug}/projects/${projectId}/issues`).then(() => {
@ -81,20 +85,22 @@ export const ProjectsToolbarMenu = observer(function ProjectsToolbarMenu({
/> />
))} ))}
</div> </div>
<div className="mt-2 border-t border-white/8 px-1 pt-2"> {canCreateProject && (
<Menu.Item> <div className="mt-2 border-t border-white/8 px-1 pt-2">
<button <Menu.Item>
type="button" <button
className="flex w-full items-center gap-3 rounded-md px-2 py-2 text-left text-13 font-medium text-secondary transition-colors hover:bg-layer-transparent-hover hover:text-primary" type="button"
onClick={() => toggleCreateProjectModal(true)} className="flex w-full items-center gap-3 rounded-md px-2 py-2 text-left text-13 font-medium text-secondary transition-colors hover:bg-layer-transparent-hover hover:text-primary"
> onClick={() => toggleCreateProjectModal(true)}
<span className="grid size-8 flex-shrink-0 place-items-center"> >
<PlusIcon className="size-4" /> <span className="grid size-8 flex-shrink-0 place-items-center">
</span> <PlusIcon className="size-4" />
<span>{t("create_project")}</span> </span>
</button> <span>{t("create_project")}</span>
</Menu.Item> </button>
</div> </Menu.Item>
</div>
)}
</div> </div>
</Menu.Items> </Menu.Items>
</Menu> </Menu>

View File

@ -8,7 +8,7 @@ import { observer } from "mobx-react";
import Link from "next/link"; import Link from "next/link";
import { useTheme } from "next-themes"; import { useTheme } from "next-themes";
// plane imports // plane imports
import { PROJECT_TRACKER_ELEMENTS } from "@plane/constants"; import { EUserPermissions, EUserPermissionsLevel, PROJECT_TRACKER_ELEMENTS } from "@plane/constants";
import { Button, getButtonStyling } from "@plane/propel/button"; import { Button, getButtonStyling } from "@plane/propel/button";
import { cn } from "@plane/utils"; import { cn } from "@plane/utils";
// assets // assets
@ -16,11 +16,14 @@ import ProjectDarkEmptyState from "@/app/assets/empty-state/project-settings/no-
import ProjectLightEmptyState from "@/app/assets/empty-state/project-settings/no-projects-light.png?url"; import ProjectLightEmptyState from "@/app/assets/empty-state/project-settings/no-projects-light.png?url";
// hooks // hooks
import { useCommandPalette } from "@/hooks/store/use-command-palette"; import { useCommandPalette } from "@/hooks/store/use-command-palette";
import { useUserPermissions } from "@/hooks/store/user";
function ProjectSettingsPage() { function ProjectSettingsPage() {
// store hooks // store hooks
const { resolvedTheme } = useTheme(); const { resolvedTheme } = useTheme();
const { toggleCreateProjectModal } = useCommandPalette(); const { toggleCreateProjectModal } = useCommandPalette();
const { allowPermissions } = useUserPermissions();
const canCreateProject = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.WORKSPACE);
// derived values // derived values
const resolvedPath = resolvedTheme === "dark" ? ProjectDarkEmptyState : ProjectLightEmptyState; const resolvedPath = resolvedTheme === "dark" ? ProjectDarkEmptyState : ProjectLightEmptyState;
return ( return (
@ -35,12 +38,14 @@ function ProjectSettingsPage() {
<Link href="https://plane.so/" target="_blank" className={cn(getButtonStyling("secondary", "base"))}> <Link href="https://plane.so/" target="_blank" className={cn(getButtonStyling("secondary", "base"))}>
Learn more about projects Learn more about projects
</Link> </Link>
<Button {canCreateProject && (
onClick={() => toggleCreateProjectModal(true)} <Button
data-ph-element={PROJECT_TRACKER_ELEMENTS.EMPTY_STATE_CREATE_PROJECT_BUTTON} onClick={() => toggleCreateProjectModal(true)}
> data-ph-element={PROJECT_TRACKER_ELEMENTS.EMPTY_STATE_CREATE_PROJECT_BUTTON}
Start your first project >
</Button> Start your first project
</Button>
)}
</div> </div>
</div> </div>
); );

View File

@ -42,7 +42,7 @@ export const NoProjectsEmptyState = observer(function NoProjectsEmptyState() {
const { t } = useTranslation(); const { t } = useTranslation();
// derived values // derived values
const canCreateProject = allowPermissions( const canCreateProject = allowPermissions(
[EUserPermissions.ADMIN, EUserPermissions.MEMBER], [EUserPermissions.ADMIN],
EUserPermissionsLevel.WORKSPACE EUserPermissionsLevel.WORKSPACE
); );
const isWorkspaceAdmin = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.WORKSPACE); const isWorkspaceAdmin = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.WORKSPACE);

View File

@ -23,6 +23,7 @@ export const GlobalViewEmptyState = observer(function GlobalViewEmptyState() {
const { toggleCreateIssueModal, toggleCreateProjectModal } = useCommandPalette(); const { toggleCreateIssueModal, toggleCreateProjectModal } = useCommandPalette();
const { allowPermissions } = useUserPermissions(); const { allowPermissions } = useUserPermissions();
// derived values // derived values
const canCreateProject = allowPermissions([EUserWorkspaceRoles.ADMIN], EUserPermissionsLevel.WORKSPACE);
const hasMemberLevelPermission = allowPermissions( const hasMemberLevelPermission = allowPermissions(
[EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER], [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER],
EUserPermissionsLevel.WORKSPACE EUserPermissionsLevel.WORKSPACE
@ -41,7 +42,7 @@ export const GlobalViewEmptyState = observer(function GlobalViewEmptyState() {
onClick: () => { onClick: () => {
toggleCreateProjectModal(true); toggleCreateProjectModal(true);
}, },
disabled: !hasMemberLevelPermission, disabled: !canCreateProject,
variant: "primary", variant: "primary",
}, },
]} ]}

View File

@ -39,10 +39,7 @@ export const WorkspaceDraftIssuesRoot = observer(function WorkspaceDraftIssuesRo
const { toggleCreateProjectModal } = useCommandPalette(); const { toggleCreateProjectModal } = useCommandPalette();
const { allowPermissions } = useUserPermissions(); const { allowPermissions } = useUserPermissions();
// derived values // derived values
const hasMemberLevelPermission = allowPermissions( const canCreateProject = allowPermissions([EUserWorkspaceRoles.ADMIN], EUserPermissionsLevel.WORKSPACE);
[EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER],
EUserPermissionsLevel.WORKSPACE
);
//swr hook for fetching issue properties //swr hook for fetching issue properties
useWorkspaceIssueProperties(workspaceSlug); useWorkspaceIssueProperties(workspaceSlug);
@ -77,7 +74,7 @@ export const WorkspaceDraftIssuesRoot = observer(function WorkspaceDraftIssuesRo
onClick: () => { onClick: () => {
toggleCreateProjectModal(true); toggleCreateProjectModal(true);
}, },
disabled: !hasMemberLevelPermission, disabled: !canCreateProject,
variant: "primary", variant: "primary",
}, },
]} ]}

View File

@ -48,7 +48,7 @@ export const usePowerKCreationCommandsRecord = (): Record<TPowerKCreationCommand
// derived values // derived values
const canCreateWorkItem = canPerformAnyCreateAction && workspaceProjectIds && workspaceProjectIds.length > 0; const canCreateWorkItem = canPerformAnyCreateAction && workspaceProjectIds && workspaceProjectIds.length > 0;
const canCreateProject = allowPermissions( const canCreateProject = allowPermissions(
[EUserPermissions.ADMIN, EUserPermissions.MEMBER], [EUserPermissions.ADMIN],
EUserPermissionsLevel.WORKSPACE EUserPermissionsLevel.WORKSPACE
); );
const hasProjectMemberLevelPermissions = (ctx: TPowerKContext) => const hasProjectMemberLevelPermissions = (ctx: TPowerKContext) =>

View File

@ -48,7 +48,7 @@ export const ProjectCardList = observer(function ProjectCardList(props: TProject
// permissions // permissions
const canPerformEmptyStateActions = allowPermissions( const canPerformEmptyStateActions = allowPermissions(
[EUserPermissions.ADMIN, EUserPermissions.MEMBER], [EUserPermissions.ADMIN],
EUserPermissionsLevel.WORKSPACE EUserPermissionsLevel.WORKSPACE
); );

View File

@ -5,12 +5,14 @@
*/ */
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
import { EModalPosition, EModalWidth, ModalCore } from "@plane/ui"; import { EModalPosition, EModalWidth, ModalCore } from "@plane/ui";
import { getAssetIdFromUrl, checkURLValidity } from "@plane/utils"; import { getAssetIdFromUrl, checkURLValidity } from "@plane/utils";
// plane ui // plane ui
// helpers // helpers
// hooks // hooks
import useKeypress from "@/hooks/use-keypress"; import useKeypress from "@/hooks/use-keypress";
import { useUserPermissions } from "@/hooks/store/user";
// plane web components // plane web components
import { CreateProjectForm } from "@/plane-web/components/projects/create/root"; import { CreateProjectForm } from "@/plane-web/components/projects/create/root";
// plane web types // plane web types
@ -36,9 +38,11 @@ enum EProjectCreationSteps {
export function CreateProjectModal(props: Props) { export function CreateProjectModal(props: Props) {
const { isOpen, onClose, setToFavorite = false, workspaceSlug, data, templateId } = props; const { isOpen, onClose, setToFavorite = false, workspaceSlug, data, templateId } = props;
const { allowPermissions } = useUserPermissions();
// states // states
const [currentStep, setCurrentStep] = useState<EProjectCreationSteps>(EProjectCreationSteps.CREATE_PROJECT); const [currentStep, setCurrentStep] = useState<EProjectCreationSteps>(EProjectCreationSteps.CREATE_PROJECT);
const [createdProjectId, setCreatedProjectId] = useState<string | null>(null); const [createdProjectId, setCreatedProjectId] = useState<string | null>(null);
const canCreateProject = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.WORKSPACE, workspaceSlug);
useEffect(() => { useEffect(() => {
if (isOpen) { if (isOpen) {
@ -47,6 +51,10 @@ export function CreateProjectModal(props: Props) {
} }
}, [isOpen]); }, [isOpen]);
useEffect(() => {
if (isOpen && !canCreateProject) onClose();
}, [canCreateProject, isOpen, onClose]);
const handleNextStep = (projectId: string) => { const handleNextStep = (projectId: string) => {
if (!projectId) return; if (!projectId) return;
setCreatedProjectId(projectId); setCreatedProjectId(projectId);
@ -65,6 +73,8 @@ export function CreateProjectModal(props: Props) {
if (isOpen) onClose(); if (isOpen) onClose();
}); });
if (!canCreateProject) return null;
return ( return (
<ModalCore <ModalCore
isOpen={isOpen} isOpen={isOpen}

View File

@ -33,7 +33,7 @@ export const ProjectsBaseHeader = observer(function ProjectsBaseHeader() {
const pathname = usePathname(); const pathname = usePathname();
// auth // auth
const isAuthorizedUser = allowPermissions( const isAuthorizedUser = allowPermissions(
[EUserPermissions.ADMIN, EUserPermissions.MEMBER], [EUserPermissions.ADMIN],
EUserPermissionsLevel.WORKSPACE EUserPermissionsLevel.WORKSPACE
); );
const isArchived = pathname.includes("/archives"); const isArchived = pathname.includes("/archives");

View File

@ -55,7 +55,7 @@ export const SidebarProjectsList = observer(function SidebarProjectsList() {
// auth // auth
const isAuthorizedUser = allowPermissions( const isAuthorizedUser = allowPermissions(
[EUserPermissions.ADMIN, EUserPermissions.MEMBER], [EUserPermissions.ADMIN],
EUserPermissionsLevel.WORKSPACE EUserPermissionsLevel.WORKSPACE
); );