From 87e1857f53df6dbaf05b03347593e5540e099a84 Mon Sep 17 00:00:00 2001 From: DCCONSTRUCTIONS Date: Tue, 12 May 2026 19:36:01 +0300 Subject: [PATCH] =?UTF-8?q?=D0=A4=D0=A3=D0=9D=D0=9A=D0=A6=D0=98=D0=98=20-?= =?UTF-8?q?=20=D0=9C=D0=95=D0=96=D0=9F=D0=A0=D0=9E=D0=95=D0=9A=D0=A2=D0=9D?= =?UTF-8?q?=D0=90=D0=AF=20=D0=9A=D0=9E=D0=9C=D0=9C=D0=A3=D0=9D=D0=98=D0=9A?= =?UTF-8?q?=D0=90=D0=A6=D0=98=D0=AF:=20guest-=D0=B4=D0=BE=D1=81=D1=82?= =?UTF-8?q?=D1=83=D0=BF=20Operational=20Core?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- plane-src/apps/api/plane/app/nodedc_access.py | 15 +++++++ .../api/plane/app/views/issue/attachment.py | 6 +-- .../apps/api/plane/app/views/issue/base.py | 39 ++++++++++--------- .../apps/api/plane/app/views/issue/comment.py | 6 +-- .../api/plane/app/views/issue/reaction.py | 4 +- .../apps/api/plane/app/views/issue/version.py | 3 +- .../calendar/base-calendar-root.tsx | 8 +++- .../issue-layouts/gantt/base-gantt-root.tsx | 2 +- .../issues/issue-layouts/kanban/block.tsx | 6 +-- .../issues/issue-layouts/list/block.tsx | 6 +-- .../components/issues/issue-layouts/utils.tsx | 2 +- .../web/core/hooks/use-group-dragndrop.ts | 15 +++++-- 12 files changed, 70 insertions(+), 42 deletions(-) create mode 100644 plane-src/apps/api/plane/app/nodedc_access.py diff --git a/plane-src/apps/api/plane/app/nodedc_access.py b/plane-src/apps/api/plane/app/nodedc_access.py new file mode 100644 index 0000000..c53baa0 --- /dev/null +++ b/plane-src/apps/api/plane/app/nodedc_access.py @@ -0,0 +1,15 @@ +import os + + +def nodedc_env_flag(name): + return os.environ.get(name, "").strip().lower() in {"1", "true", "yes", "on"} + + +def nodedc_guest_read_all_issues_enabled(): + return nodedc_env_flag("PLANE_NODEDC_GUEST_READ_ALL_ISSUES") or nodedc_env_flag( + "PLANE_NODEDC_ACCESS_ENFORCEMENT" + ) + + +def should_limit_guest_to_own_issues(project): + return not project.guest_view_all_features and not nodedc_guest_read_all_issues_enabled() diff --git a/plane-src/apps/api/plane/app/views/issue/attachment.py b/plane-src/apps/api/plane/app/views/issue/attachment.py index 928b5c6..cb48645 100644 --- a/plane-src/apps/api/plane/app/views/issue/attachment.py +++ b/plane-src/apps/api/plane/app/views/issue/attachment.py @@ -34,7 +34,7 @@ class IssueAttachmentEndpoint(BaseAPIView): model = FileAsset parser_classes = (MultiPartParser, FormParser) - @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST]) + @allow_permission([ROLE.ADMIN, ROLE.MEMBER]) def post(self, request, slug, project_id, issue_id): serializer = IssueAttachmentSerializer(data=request.data) workspace = Workspace.objects.get(slug=slug) @@ -104,7 +104,7 @@ class IssueAttachmentV2Endpoint(BaseAPIView): serializer_class = IssueAttachmentSerializer model = FileAsset - @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST]) + @allow_permission([ROLE.ADMIN, ROLE.MEMBER]) def post(self, request, slug, project_id, issue_id): name = request.data.get("name") type = request.data.get("type", False) @@ -209,7 +209,7 @@ class IssueAttachmentV2Endpoint(BaseAPIView): serializer = IssueAttachmentSerializer(issue_attachments, many=True) return Response(serializer.data, status=status.HTTP_200_OK) - @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST]) + @allow_permission([ROLE.ADMIN, ROLE.MEMBER]) def patch(self, request, slug, project_id, issue_id, pk): issue_attachment = FileAsset.objects.get(pk=pk, workspace__slug=slug, project_id=project_id) serializer = IssueAttachmentSerializer(issue_attachment) diff --git a/plane-src/apps/api/plane/app/views/issue/base.py b/plane-src/apps/api/plane/app/views/issue/base.py index 86128a5..aac04c8 100644 --- a/plane-src/apps/api/plane/app/views/issue/base.py +++ b/plane-src/apps/api/plane/app/views/issue/base.py @@ -33,6 +33,10 @@ from rest_framework.response import Response # Module imports from plane.app.permissions import ROLE, allow_permission +from plane.app.nodedc_access import ( + nodedc_guest_read_all_issues_enabled, + should_limit_guest_to_own_issues, +) from plane.app.realtime.issue_events import publish_issue_event_on_commit from plane.app.serializers import ( IssueCreateSerializer, @@ -333,7 +337,7 @@ class IssueViewSet(BaseViewSet): role=5, is_active=True, ).exists() - and not project.guest_view_all_features + and should_limit_guest_to_own_issues(project) ): issue_queryset = issue_queryset.filter(created_by=request.user) filtered_issue_queryset = filtered_issue_queryset.filter(created_by=request.user) @@ -641,7 +645,7 @@ class IssueViewSet(BaseViewSet): role=5, is_active=True, ).exists() - and not project.guest_view_all_features + and should_limit_guest_to_own_issues(project) and not issue.created_by == request.user ): return Response( @@ -972,7 +976,7 @@ class IssuePaginatedViewSet(BaseViewSet): role=5, is_active=True, ) - if project_member.exists() and not project.guest_view_all_features: + if project_member.exists() and should_limit_guest_to_own_issues(project): base_queryset = base_queryset.filter(created_by=request.user) queryset = queryset.filter(created_by=request.user) @@ -1093,8 +1097,17 @@ class IssueDetailEndpoint(BaseAPIView): def get(self, request, slug, project_id): filters = issue_filters(request.query_params, "GET") - # check for the project member role, if the role is 5 then check for the guest_view_all_features - # if it is true then show all the issues else show only the issues created by the user + guest_read_filter = Q( + project__project_projectmember__member=self.request.user, + project__project_projectmember__is_active=True, + project__project_projectmember__role=ROLE.GUEST.value, + ) + if not nodedc_guest_read_all_issues_enabled(): + guest_read_filter = guest_read_filter & ( + Q(project__guest_view_all_features=True) + | Q(project__guest_view_all_features=False, created_by=self.request.user) + ) + permission_subquery = ( Issue.issue_objects.filter(workspace__slug=slug, project_id=project_id, id=OuterRef("id")) .filter( @@ -1103,19 +1116,7 @@ class IssueDetailEndpoint(BaseAPIView): project__project_projectmember__is_active=True, project__project_projectmember__role__gt=ROLE.GUEST.value, ) - | Q( - project__project_projectmember__member=self.request.user, - project__project_projectmember__is_active=True, - project__project_projectmember__role=ROLE.GUEST.value, - project__guest_view_all_features=True, - ) - | Q( - project__project_projectmember__member=self.request.user, - project__project_projectmember__is_active=True, - project__project_projectmember__role=ROLE.GUEST.value, - project__guest_view_all_features=False, - created_by=self.request.user, - ) + | guest_read_filter ) .values("id") ) @@ -1418,7 +1419,7 @@ class IssueDetailIdentifierEndpoint(BaseAPIView): role=5, is_active=True, ).exists() - and not project.guest_view_all_features + and should_limit_guest_to_own_issues(project) and not issue.created_by == request.user ): return Response( diff --git a/plane-src/apps/api/plane/app/views/issue/comment.py b/plane-src/apps/api/plane/app/views/issue/comment.py index 34fe0f9..bb08489 100644 --- a/plane-src/apps/api/plane/app/views/issue/comment.py +++ b/plane-src/apps/api/plane/app/views/issue/comment.py @@ -60,7 +60,7 @@ class IssueCommentViewSet(BaseViewSet): .distinct() ) - @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST]) + @allow_permission([ROLE.ADMIN, ROLE.MEMBER]) def create(self, request, slug, project_id, issue_id): project = Project.objects.get(pk=project_id) issue = Issue.objects.get(pk=issue_id) @@ -180,7 +180,7 @@ class CommentReactionViewSet(BaseViewSet): .distinct() ) - @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST]) + @allow_permission([ROLE.ADMIN, ROLE.MEMBER]) def create(self, request, slug, project_id, comment_id): try: serializer = CommentReactionSerializer(data=request.data) @@ -209,7 +209,7 @@ class CommentReactionViewSet(BaseViewSet): status=status.HTTP_400_BAD_REQUEST, ) - @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST]) + @allow_permission([ROLE.ADMIN, ROLE.MEMBER]) def destroy(self, request, slug, project_id, comment_id, reaction_code): comment_reaction = CommentReaction.objects.get( workspace__slug=slug, diff --git a/plane-src/apps/api/plane/app/views/issue/reaction.py b/plane-src/apps/api/plane/app/views/issue/reaction.py index c09e1e9..f93b2a0 100644 --- a/plane-src/apps/api/plane/app/views/issue/reaction.py +++ b/plane-src/apps/api/plane/app/views/issue/reaction.py @@ -42,7 +42,7 @@ class IssueReactionViewSet(BaseViewSet): .distinct() ) - @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST]) + @allow_permission([ROLE.ADMIN, ROLE.MEMBER]) def create(self, request, slug, project_id, issue_id): serializer = IssueReactionSerializer(data=request.data) if serializer.is_valid(): @@ -61,7 +61,7 @@ class IssueReactionViewSet(BaseViewSet): return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST]) + @allow_permission([ROLE.ADMIN, ROLE.MEMBER]) def destroy(self, request, slug, project_id, issue_id, reaction_code): issue_reaction = IssueReaction.objects.get( workspace__slug=slug, diff --git a/plane-src/apps/api/plane/app/views/issue/version.py b/plane-src/apps/api/plane/app/views/issue/version.py index 540c7d6..d49c309 100644 --- a/plane-src/apps/api/plane/app/views/issue/version.py +++ b/plane-src/apps/api/plane/app/views/issue/version.py @@ -20,6 +20,7 @@ from plane.app.serializers import ( IssueDescriptionVersionDetailSerializer, ) from plane.app.permissions import allow_permission, ROLE +from plane.app.nodedc_access import should_limit_guest_to_own_issues from plane.utils.global_paginator import paginate from plane.utils.timezone_converter import user_timezone_converter @@ -96,7 +97,7 @@ class WorkItemDescriptionVersionEndpoint(BaseAPIView): role=ROLE.GUEST.value, is_active=True, ).exists() - and not project.guest_view_all_features + and should_limit_guest_to_own_issues(project) and not issue.created_by == request.user ): return Response( diff --git a/plane-src/apps/web/core/components/issues/issue-layouts/calendar/base-calendar-root.tsx b/plane-src/apps/web/core/components/issues/issue-layouts/calendar/base-calendar-root.tsx index 3f4eef8..400291b 100644 --- a/plane-src/apps/web/core/components/issues/issue-layouts/calendar/base-calendar-root.tsx +++ b/plane-src/apps/web/core/components/issues/issue-layouts/calendar/base-calendar-root.tsx @@ -120,10 +120,14 @@ export const BaseCalendarRoot = observer(function BaseCalendarRoot(props: IBaseC issueProjectId, updateIssue ).catch((err) => { + const message = + err?.detail === "You are not allowed to move this work item" + ? "У вас нет прав перемещать эту карточку" + : "Не удалось выполнить действие"; setToast({ - title: "Error!", + title: "Ошибка", type: TOAST_TYPE.ERROR, - message: err?.detail ?? "Failed to perform this action", + message, }); }); }; diff --git a/plane-src/apps/web/core/components/issues/issue-layouts/gantt/base-gantt-root.tsx b/plane-src/apps/web/core/components/issues/issue-layouts/gantt/base-gantt-root.tsx index 3404b43..b12eb46 100644 --- a/plane-src/apps/web/core/components/issues/issue-layouts/gantt/base-gantt-root.tsx +++ b/plane-src/apps/web/core/components/issues/issue-layouts/gantt/base-gantt-root.tsx @@ -103,7 +103,7 @@ export const BaseGanttRoot = observer(function BaseGanttRoot(props: IBaseGanttRo setToast({ type: TOAST_TYPE.ERROR, title: t("toast.error"), - message: "Error while updating work item dates, Please try again Later", + message: "Не удалось обновить даты карточки. Попробуйте позже.", }); }), [issues, projectId, workspaceSlug] diff --git a/plane-src/apps/web/core/components/issues/issue-layouts/kanban/block.tsx b/plane-src/apps/web/core/components/issues/issue-layouts/kanban/block.tsx index a731e3e..8e111b9 100644 --- a/plane-src/apps/web/core/components/issues/issue-layouts/kanban/block.tsx +++ b/plane-src/apps/web/core/components/issues/issue-layouts/kanban/block.tsx @@ -305,10 +305,10 @@ export const KanbanIssueBlock = observer(function KanbanIssueBlock(props: IssueB else { setToast({ type: TOAST_TYPE.WARNING, - title: "Cannot move work item", + title: "Нельзя переместить карточку", message: !canEditIssueProperties - ? "You are not allowed to move this work item" - : "Drag and drop is disabled for the current grouping", + ? "У вас нет прав перемещать эту карточку" + : "Перетаскивание отключено для текущей группировки", }); } }} diff --git a/plane-src/apps/web/core/components/issues/issue-layouts/list/block.tsx b/plane-src/apps/web/core/components/issues/issue-layouts/list/block.tsx index d2c5afa..1806084 100644 --- a/plane-src/apps/web/core/components/issues/issue-layouts/list/block.tsx +++ b/plane-src/apps/web/core/components/issues/issue-layouts/list/block.tsx @@ -243,10 +243,10 @@ export const IssueBlock = observer(function IssueBlock(props: IssueBlockProps) { if (!isDraggingAllowed) { setToast({ type: TOAST_TYPE.WARNING, - title: "Cannot move work item", + title: "Нельзя переместить карточку", message: !canEditIssueProperties - ? "You are not allowed to move this work item" - : "Drag and drop is disabled for the current grouping", + ? "У вас нет прав перемещать эту карточку" + : "Перетаскивание отключено для текущей группировки", }); } }} diff --git a/plane-src/apps/web/core/components/issues/issue-layouts/utils.tsx b/plane-src/apps/web/core/components/issues/issue-layouts/utils.tsx index 0c5db7e..fc4f9d2 100644 --- a/plane-src/apps/web/core/components/issues/issue-layouts/utils.tsx +++ b/plane-src/apps/web/core/components/issues/issue-layouts/utils.tsx @@ -183,7 +183,7 @@ const getCycleColumns = (): IGroupByColumn[] | undefined => { icon: , payload: { cycle_id: cycle.id }, isDropDisabled, - dropErrorMessage: isDropDisabled ? "Work item cannot be moved to completed cycles" : undefined, + dropErrorMessage: isDropDisabled ? "Карточку нельзя перенести в завершённый цикл" : undefined, }); }); cycles.push({ diff --git a/plane-src/apps/web/core/hooks/use-group-dragndrop.ts b/plane-src/apps/web/core/hooks/use-group-dragndrop.ts index b21cdcb..60aec72 100644 --- a/plane-src/apps/web/core/hooks/use-group-dragndrop.ts +++ b/plane-src/apps/web/core/hooks/use-group-dragndrop.ts @@ -27,6 +27,13 @@ type DNDStoreType = | EIssuesStoreType.EPIC | EIssuesStoreType.TEAM_PROJECT_WORK_ITEMS; +const resolveDragDropErrorMessage = (message?: string) => { + if (message === "You are not allowed to move this work item") return "У вас нет прав перемещать эту карточку"; + if (message === "Work item cannot be moved to completed cycles") return "Карточку нельзя перенести в завершённый цикл"; + if (message === "Failed to perform this action") return "Не удалось выполнить действие"; + return message || "Не удалось выполнить действие"; +}; + export const useGroupIssuesDragNDrop = ( storeType: DNDStoreType, orderBy: TIssueOrderByOptions | undefined, @@ -63,8 +70,8 @@ export const useGroupIssuesDragNDrop = ( ) => { const errorToastProps = { type: TOAST_TYPE.ERROR, - title: "Error!", - message: "Error while updating work item", + title: "Ошибка", + message: "Не удалось обновить карточку", }; const moduleKey = ISSUE_FILTER_DEFAULT_DATA["module"]; const cycleKey = ISSUE_FILTER_DEFAULT_DATA["cycle"]; @@ -117,9 +124,9 @@ export const useGroupIssuesDragNDrop = ( orderBy !== "sort_order" ).catch((err) => { setToast({ - title: "Error!", + title: "Ошибка", type: TOAST_TYPE.ERROR, - message: err?.detail ?? "Failed to perform this action", + message: resolveDragDropErrorMessage(err?.detail), }); }); };