diff --git a/apiserver/plane/app/views/issue/comment.py b/apiserver/plane/app/views/issue/comment.py index 8bd7daed367..12964dc3a81 100644 --- a/apiserver/plane/app/views/issue/comment.py +++ b/apiserver/plane/app/views/issue/comment.py @@ -21,6 +21,8 @@ IssueComment, ProjectMember, CommentReaction, + Project, + Issue, ) from plane.bgtasks.issue_activities_task import issue_activity @@ -67,9 +69,27 @@ def get_queryset(self): [ ROLE.ADMIN, ROLE.MEMBER, + ROLE.GUEST, ] ) def create(self, request, slug, project_id, issue_id): + project = Project.objects.get(pk=project_id) + issue = Issue.objects.get(pk=issue_id) + if ( + ProjectMember.objects.filter( + workspace__slug=slug, + project_id=project_id, + member=request.user, + role=5, + is_active=True, + ).exists() + and not project.guest_view_all_features + and not issue.created_by == request.user + ): + return Response( + {"error": "You are not allowed to comment on the issue"}, + status=status.HTTP_400_BAD_REQUEST, + ) serializer = IssueCommentSerializer(data=request.data) if serializer.is_valid(): serializer.save( @@ -94,7 +114,7 @@ def create(self, request, slug, project_id, issue_id): return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) @allow_permission( - allowed_roles=[ROLE.ADMIN, ROLE.MEMBER], + allowed_roles=[ROLE.ADMIN], creator=True, model=IssueComment, ) @@ -182,6 +202,7 @@ def get_queryset(self): [ ROLE.ADMIN, ROLE.MEMBER, + ROLE.GUEST, ] ) def create(self, request, slug, project_id, comment_id): @@ -210,6 +231,7 @@ def create(self, request, slug, project_id, comment_id): [ ROLE.ADMIN, ROLE.MEMBER, + ROLE.GUEST, ] ) def destroy(self, request, slug, project_id, comment_id, reaction_code): diff --git a/apiserver/plane/app/views/issue/reaction.py b/apiserver/plane/app/views/issue/reaction.py index 655771013ce..5146118f302 100644 --- a/apiserver/plane/app/views/issue/reaction.py +++ b/apiserver/plane/app/views/issue/reaction.py @@ -12,7 +12,7 @@ # Module imports from .. import BaseViewSet from plane.app.serializers import IssueReactionSerializer -from plane.app.permissions import ProjectLitePermission +from plane.app.permissions import allow_permission, ROLE from plane.db.models import IssueReaction from plane.bgtasks.issue_activities_task import issue_activity @@ -20,9 +20,6 @@ class IssueReactionViewSet(BaseViewSet): serializer_class = IssueReactionSerializer model = IssueReaction - permission_classes = [ - ProjectLitePermission, - ] def get_queryset(self): return ( @@ -40,6 +37,7 @@ def get_queryset(self): .distinct() ) + @allow_permission(ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST) def create(self, request, slug, project_id, issue_id): serializer = IssueReactionSerializer(data=request.data) if serializer.is_valid(): @@ -62,6 +60,7 @@ def create(self, request, slug, project_id, issue_id): 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) def destroy(self, request, slug, project_id, issue_id, reaction_code): issue_reaction = IssueReaction.objects.get( workspace__slug=slug, diff --git a/web/core/components/command-palette/command-modal.tsx b/web/core/components/command-palette/command-modal.tsx index d0e5010e8eb..87c86477248 100644 --- a/web/core/components/command-palette/command-modal.tsx +++ b/web/core/components/command-palette/command-modal.tsx @@ -31,7 +31,7 @@ import { ISSUE_DETAILS } from "@/constants/fetch-keys"; // helpers import { getTabIndex } from "@/helpers/tab-indices.helper"; // hooks -import { useCommandPalette, useEventTracker, useProject, useUser } from "@/hooks/store"; +import { useCommandPalette, useEventTracker, useProject, useUser, useUserPermissions } from "@/hooks/store"; import { useAppRouter } from "@/hooks/use-app-router"; import useDebounce from "@/hooks/use-debounce"; import { usePlatformOS } from "@/hooks/use-platform-os"; @@ -41,6 +41,7 @@ import { IssueIdentifier } from "@/plane-web/components/issues"; import { WorkspaceService } from "@/plane-web/services"; // services import { IssueService } from "@/services/issue"; +import { EUserPermissions, EUserPermissionsLevel } from "ee/constants/user-permissions"; const workspaceService = new WorkspaceService(); const issueService = new IssueService(); @@ -71,6 +72,7 @@ export const CommandModal: React.FC = observer(() => { const [pages, setPages] = useState([]); const { isCommandPaletteOpen, toggleCommandPaletteModal, toggleCreateIssueModal, toggleCreateProjectModal } = useCommandPalette(); + const { allowPermissions } = useUserPermissions(); const { setTrackElement } = useEventTracker(); // router @@ -84,6 +86,11 @@ export const CommandModal: React.FC = observer(() => { const { baseTabIndex } = getTabIndex(undefined, isMobile); + const canPerformWorkspaceActions = allowPermissions( + [EUserPermissions.ADMIN, EUserPermissions.MEMBER], + EUserPermissionsLevel.WORKSPACE + ); + // TODO: update this to mobx store const { data: issueDetails } = useSWR( workspaceSlug && projectId && issueId ? ISSUE_DETAILS(issueId.toString()) : null, @@ -314,8 +321,7 @@ export const CommandModal: React.FC = observer(() => { )} - - {workspaceSlug && ( + {workspaceSlug && canPerformWorkspaceActions && ( { @@ -335,23 +341,26 @@ export const CommandModal: React.FC = observer(() => { )} {/* project actions */} - {projectId && } - - - { - setPlaceholder("Search workspace settings..."); - setSearchTerm(""); - setPages([...pages, "settings"]); - }} - className="focus:outline-none" - > -
- - Search settings... -
-
-
+ {projectId && canPerformAnyCreateAction && ( + + )} + {canPerformWorkspaceAction && ( + + { + setPlaceholder("Search workspace settings..."); + setSearchTerm(""); + setPages([...pages, "settings"]); + }} + className="focus:outline-none" + > +
+ + Search settings... +
+
+
+ )}
diff --git a/web/core/components/issues/issue-detail/label/root.tsx b/web/core/components/issues/issue-detail/label/root.tsx index afff6c0f6a0..464a4b6c262 100644 --- a/web/core/components/issues/issue-detail/label/root.tsx +++ b/web/core/components/issues/issue-detail/label/root.tsx @@ -6,10 +6,11 @@ import { IIssueLabel, TIssue } from "@plane/types"; // components import { TOAST_TYPE, setToast } from "@plane/ui"; // hooks -import { useIssueDetail, useLabel, useProjectInbox } from "@/hooks/store"; +import { useIssueDetail, useLabel, useProjectInbox, useUserPermissions } from "@/hooks/store"; // ui // types import { LabelList, LabelCreate, IssueLabelSelectRoot } from "./"; +import { EUserPermissions, EUserPermissionsLevel } from "ee/constants/user-permissions"; export type TIssueLabel = { workspaceSlug: string; @@ -34,7 +35,9 @@ export const IssueLabel: FC = observer((props) => { issue: { getIssueById }, } = useIssueDetail(); const { getIssueInboxByIssueId } = useProjectInbox(); + const { allowPermissions } = useUserPermissions(); + const canCreateLabel = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.PROJECT); const issue = isInboxIssue ? getIssueInboxByIssueId(issueId)?.issue : getIssueById(issueId); const labelOperations: TLabelOperations = useMemo( @@ -99,7 +102,7 @@ export const IssueLabel: FC = observer((props) => { /> )} - {!disabled && ( + {!disabled && canCreateLabel && ( = observer((props) => { } = useIssueDetail(); // state const [loading, setLoading] = useState(false); + // hooks + const { allowPermissions } = useUserPermissions(); const isSubscribed = getSubscriptionByIssueId(issueId); + const isEditable = allowPermissions( + [EUserPermissions.ADMIN, EUserPermissions.MEMBER], + EUserPermissionsLevel.PROJECT, + workspaceSlug, + projectId + ); const handleSubscription = async () => { setLoading(true); @@ -64,6 +73,7 @@ export const IssueSubscription: FC = observer((props) => { variant="outline-primary" className="hover:!bg-custom-primary-100/20" onClick={handleSubscription} + disabled={!isEditable} > {loading ? ( diff --git a/web/core/components/project/settings/member-columns.tsx b/web/core/components/project/settings/member-columns.tsx index dae12532385..f6699d71188 100644 --- a/web/core/components/project/settings/member-columns.tsx +++ b/web/core/components/project/settings/member-columns.tsx @@ -98,7 +98,8 @@ export const AccountTypeColumn: React.FC = observer((props) => // derived values const isCurrentUser = currentUser?.id === rowData.member.id; const isAdminOrGuest = [EUserPermissions.ADMIN, EUserPermissions.GUEST].includes(rowData.role); - const isRoleNonEditable = isCurrentUser || isAdminOrGuest; + const userWorkspaceRole = getWorkspaceMemberDetails(rowData.member.id)?.role; + const isRoleNonEditable = isCurrentUser || (isAdminOrGuest && userWorkspaceRole !== EUserPermissions.MEMBER); const checkCurrentOptionWorkspaceRole = (value: string) => { const currentMemberWorkspaceRole = getWorkspaceMemberDetails(value)?.role as EUserPermissions | undefined; diff --git a/web/core/components/workspace/sidebar/quick-actions.tsx b/web/core/components/workspace/sidebar/quick-actions.tsx index f0eca5cf091..e416c537ece 100644 --- a/web/core/components/workspace/sidebar/quick-actions.tsx +++ b/web/core/components/workspace/sidebar/quick-actions.tsx @@ -10,8 +10,9 @@ import { CreateUpdateIssueModal } from "@/components/issues"; // helpers import { cn } from "@/helpers/common.helper"; // hooks -import { useAppTheme, useCommandPalette, useEventTracker, useProject } from "@/hooks/store"; +import { useAppTheme, useCommandPalette, useEventTracker, useProject, useUserPermissions } from "@/hooks/store"; import useLocalStorage from "@/hooks/use-local-storage"; +import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions"; export const SidebarQuickActions = observer(() => { // states @@ -28,11 +29,16 @@ export const SidebarQuickActions = observer(() => { const { sidebarCollapsed: isSidebarCollapsed } = useAppTheme(); const { setTrackElement } = useEventTracker(); const { joinedProjectIds } = useProject(); + const { allowPermissions } = useUserPermissions(); // local storage const { storedValue, setValue } = useLocalStorage>>("draftedIssue", {}); // derived values - const disabled = joinedProjectIds.length === 0; - const workspaceDraftIssue = workspaceSlug ? storedValue?.[workspaceSlug] ?? undefined : undefined; + const canCreateIssue = allowPermissions( + [EUserPermissions.ADMIN, EUserPermissions.MEMBER], + EUserPermissionsLevel.WORKSPACE + ); + const disabled = joinedProjectIds.length === 0 || !canCreateIssue; + const workspaceDraftIssue = workspaceSlug ? (storedValue?.[workspaceSlug] ?? undefined) : undefined; const handleMouseEnter = () => { // if enter before time out clear the timeout diff --git a/web/core/components/workspace/sidebar/workspace-menu.tsx b/web/core/components/workspace/sidebar/workspace-menu.tsx index 62cf672cfef..ff80f4d94df 100644 --- a/web/core/components/workspace/sidebar/workspace-menu.tsx +++ b/web/core/components/workspace/sidebar/workspace-menu.tsx @@ -23,7 +23,7 @@ import useLocalStorage from "@/hooks/use-local-storage"; import { usePlatformOS } from "@/hooks/use-platform-os"; // plane web components import { UpgradeBadge } from "@/plane-web/components/workspace"; -import { EUserPermissionsLevel } from "@/plane-web/constants/user-permissions"; +import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions"; export const SidebarWorkspaceMenu = observer(() => { // state @@ -43,6 +43,7 @@ export const SidebarWorkspaceMenu = observer(() => { const { setValue: toggleWorkspaceMenu, storedValue } = useLocalStorage("is_workspace_menu_open", true); // derived values const isWorkspaceMenuOpen = !!storedValue; + const isAdmin = allowPermissions([EUserPermissions.ADMIN, EUserPermissions.MEMBER], EUserPermissionsLevel.WORKSPACE); const handleLinkClick = (itemKey: string) => { if (window.innerWidth < 768) { @@ -67,11 +68,14 @@ export const SidebarWorkspaceMenu = observer(() => { return ( {!sidebarCollapsed && ( -
+
{" "} { > WORKSPACE - { - setIsMenuActive(!isMenuActive); - }} - > - - - } - className={cn( - "h-full flex items-center opacity-0 z-20 pointer-events-none flex-shrink-0 group-hover/workspace-button:opacity-100 group-hover/workspace-button:pointer-events-auto my-auto", - { - "opacity-100 pointer-events-auto": isMenuActive, + {isAdmin && ( + { + setIsMenuActive(!isMenuActive); + }} + > + + } - )} - customButtonClassName="grid place-items-center" - placement="bottom-start" - > - - -
- - Archives -
- -
+ className={cn( + "h-full flex items-center opacity-0 z-20 pointer-events-none flex-shrink-0 group-hover/workspace-button:opacity-100 group-hover/workspace-button:pointer-events-auto my-auto", + { + "opacity-100 pointer-events-auto": isMenuActive, + } + )} + customButtonClassName="grid place-items-center" + placement="bottom-start" + > + + +
+ + Archives +
+ +
- - -
- - Settings -
- -
-
+ + +
+ + Settings +
+ +
+
+ )} = observer((props) => { // router const { workspaceSlug, projectId } = useParams(); - const projectMemberInfo = projectUserInfo?.[workspaceSlug.toString()]?.[projectId.toString()]; + const projectMemberInfo = projectUserInfo?.[workspaceSlug?.toString()]?.[projectId?.toString()]; // fetching project details useSWR(