mirror of
https://github.com/makeplane/plane.git
synced 2025-12-25 08:09:33 +01:00
[WEB-2460] fix: role permission validation (#5615)
* fix: workspace menu quick action * fix: guest role upgrade flow validation * fix: create issue validation * fix: create issue validation * fix: cmd k permission validation * fix: subscription validation * fix: create label permission validation * fix: build error * chore: guest can comment in their created issues * chore: changed the queryset * chore: code refactor * chore: code refactor --------- Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
This commit is contained in:
committed by
GitHub
parent
2e816656e5
commit
45da70cf6a
@@ -21,6 +21,8 @@ from plane.db.models import (
|
||||
IssueComment,
|
||||
ProjectMember,
|
||||
CommentReaction,
|
||||
Project,
|
||||
Issue,
|
||||
)
|
||||
from plane.bgtasks.issue_activities_task import issue_activity
|
||||
|
||||
@@ -67,9 +69,27 @@ class IssueCommentViewSet(BaseViewSet):
|
||||
[
|
||||
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 @@ class IssueCommentViewSet(BaseViewSet):
|
||||
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 @@ class CommentReactionViewSet(BaseViewSet):
|
||||
[
|
||||
ROLE.ADMIN,
|
||||
ROLE.MEMBER,
|
||||
ROLE.GUEST,
|
||||
]
|
||||
)
|
||||
def create(self, request, slug, project_id, comment_id):
|
||||
@@ -210,6 +231,7 @@ class CommentReactionViewSet(BaseViewSet):
|
||||
[
|
||||
ROLE.ADMIN,
|
||||
ROLE.MEMBER,
|
||||
ROLE.GUEST,
|
||||
]
|
||||
)
|
||||
def destroy(self, request, slug, project_id, comment_id, reaction_code):
|
||||
|
||||
@@ -12,7 +12,7 @@ from rest_framework import status
|
||||
# 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 @@ from plane.bgtasks.issue_activities_task import issue_activity
|
||||
class IssueReactionViewSet(BaseViewSet):
|
||||
serializer_class = IssueReactionSerializer
|
||||
model = IssueReaction
|
||||
permission_classes = [
|
||||
ProjectLitePermission,
|
||||
]
|
||||
|
||||
def get_queryset(self):
|
||||
return (
|
||||
@@ -40,6 +37,7 @@ class IssueReactionViewSet(BaseViewSet):
|
||||
.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 @@ 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)
|
||||
def destroy(self, request, slug, project_id, issue_id, reaction_code):
|
||||
issue_reaction = IssueReaction.objects.get(
|
||||
workspace__slug=slug,
|
||||
|
||||
@@ -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<string[]>([]);
|
||||
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(() => {
|
||||
</Command.Item>
|
||||
</Command.Group>
|
||||
)}
|
||||
|
||||
{workspaceSlug && (
|
||||
{workspaceSlug && canPerformWorkspaceActions && (
|
||||
<Command.Group heading="Project">
|
||||
<Command.Item
|
||||
onSelect={() => {
|
||||
@@ -335,23 +341,26 @@ export const CommandModal: React.FC = observer(() => {
|
||||
)}
|
||||
|
||||
{/* project actions */}
|
||||
{projectId && <CommandPaletteProjectActions closePalette={closePalette} />}
|
||||
|
||||
<Command.Group heading="Workspace Settings">
|
||||
<Command.Item
|
||||
onSelect={() => {
|
||||
setPlaceholder("Search workspace settings...");
|
||||
setSearchTerm("");
|
||||
setPages([...pages, "settings"]);
|
||||
}}
|
||||
className="focus:outline-none"
|
||||
>
|
||||
<div className="flex items-center gap-2 text-custom-text-200">
|
||||
<Settings className="h-3.5 w-3.5" />
|
||||
Search settings...
|
||||
</div>
|
||||
</Command.Item>
|
||||
</Command.Group>
|
||||
{projectId && canPerformAnyCreateAction && (
|
||||
<CommandPaletteProjectActions closePalette={closePalette} />
|
||||
)}
|
||||
{canPerformWorkspaceAction && (
|
||||
<Command.Group heading="Workspace Settings">
|
||||
<Command.Item
|
||||
onSelect={() => {
|
||||
setPlaceholder("Search workspace settings...");
|
||||
setSearchTerm("");
|
||||
setPages([...pages, "settings"]);
|
||||
}}
|
||||
className="focus:outline-none"
|
||||
>
|
||||
<div className="flex items-center gap-2 text-custom-text-200">
|
||||
<Settings className="h-3.5 w-3.5" />
|
||||
Search settings...
|
||||
</div>
|
||||
</Command.Item>
|
||||
</Command.Group>
|
||||
)}
|
||||
<Command.Group heading="Account">
|
||||
<Command.Item onSelect={createNewWorkspace} className="focus:outline-none">
|
||||
<div className="flex items-center gap-2 text-custom-text-200">
|
||||
|
||||
@@ -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<TIssueLabel> = 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<TIssueLabel> = observer((props) => {
|
||||
/>
|
||||
)}
|
||||
|
||||
{!disabled && (
|
||||
{!disabled && canCreateLabel && (
|
||||
<LabelCreate
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
|
||||
@@ -7,7 +7,8 @@ import { Bell, BellOff } from "lucide-react";
|
||||
// UI
|
||||
import { Button, Loader, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
// hooks
|
||||
import { useIssueDetail } from "@/hooks/store";
|
||||
import { useIssueDetail, useUserPermissions } from "@/hooks/store";
|
||||
import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
|
||||
|
||||
export type TIssueSubscription = {
|
||||
workspaceSlug: string;
|
||||
@@ -25,8 +26,16 @@ export const IssueSubscription: FC<TIssueSubscription> = 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<TIssueSubscription> = observer((props) => {
|
||||
variant="outline-primary"
|
||||
className="hover:!bg-custom-primary-100/20"
|
||||
onClick={handleSubscription}
|
||||
disabled={!isEditable}
|
||||
>
|
||||
{loading ? (
|
||||
<span>
|
||||
|
||||
@@ -98,7 +98,8 @@ export const AccountTypeColumn: React.FC<AccountTypeProps> = 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;
|
||||
|
||||
@@ -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<Record<string, Partial<TIssue>>>("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
|
||||
|
||||
@@ -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<boolean>("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 (
|
||||
<Disclosure as="div" defaultOpen>
|
||||
{!sidebarCollapsed && (
|
||||
<div className={
|
||||
cn("flex px-2 bg-custom-sidebar-background-100 group/workspace-button hover:bg-custom-sidebar-background-90 rounded", {
|
||||
"mt-2.5": !sidebarCollapsed,
|
||||
})
|
||||
}>
|
||||
<div
|
||||
className={cn(
|
||||
"flex px-2 bg-custom-sidebar-background-100 group/workspace-button hover:bg-custom-sidebar-background-90 rounded",
|
||||
{
|
||||
"mt-2.5": !sidebarCollapsed,
|
||||
}
|
||||
)}
|
||||
>
|
||||
{" "}
|
||||
<Disclosure.Button
|
||||
as="button"
|
||||
@@ -80,45 +84,47 @@ export const SidebarWorkspaceMenu = observer(() => {
|
||||
>
|
||||
<span>WORKSPACE</span>
|
||||
</Disclosure.Button>
|
||||
<CustomMenu
|
||||
customButton={
|
||||
<span
|
||||
ref={actionSectionRef}
|
||||
className="grid place-items-center p-0.5 text-custom-sidebar-text-400 hover:bg-custom-sidebar-background-80 rounded my-auto"
|
||||
onClick={() => {
|
||||
setIsMenuActive(!isMenuActive);
|
||||
}}
|
||||
>
|
||||
<MoreHorizontal className="size-4" />
|
||||
</span>
|
||||
}
|
||||
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 && (
|
||||
<CustomMenu
|
||||
customButton={
|
||||
<span
|
||||
ref={actionSectionRef}
|
||||
className="grid place-items-center p-0.5 text-custom-sidebar-text-400 hover:bg-custom-sidebar-background-80 rounded my-auto"
|
||||
onClick={() => {
|
||||
setIsMenuActive(!isMenuActive);
|
||||
}}
|
||||
>
|
||||
<MoreHorizontal className="size-4" />
|
||||
</span>
|
||||
}
|
||||
)}
|
||||
customButtonClassName="grid place-items-center"
|
||||
placement="bottom-start"
|
||||
>
|
||||
<CustomMenu.MenuItem>
|
||||
<Link href={`/${workspaceSlug}/projects/archives`}>
|
||||
<div className="flex items-center justify-start gap-2">
|
||||
<ArchiveIcon className="h-3.5 w-3.5 stroke-[1.5]" />
|
||||
<span>Archives</span>
|
||||
</div>
|
||||
</Link>
|
||||
</CustomMenu.MenuItem>
|
||||
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"
|
||||
>
|
||||
<CustomMenu.MenuItem>
|
||||
<Link href={`/${workspaceSlug}/projects/archives`}>
|
||||
<div className="flex items-center justify-start gap-2">
|
||||
<ArchiveIcon className="h-3.5 w-3.5 stroke-[1.5]" />
|
||||
<span>Archives</span>
|
||||
</div>
|
||||
</Link>
|
||||
</CustomMenu.MenuItem>
|
||||
|
||||
<CustomMenu.MenuItem>
|
||||
<Link href={`/${workspaceSlug}/settings`}>
|
||||
<div className="flex items-center justify-start gap-2">
|
||||
<Settings className="h-3.5 w-3.5 stroke-[1.5]" />
|
||||
<span>Settings</span>
|
||||
</div>
|
||||
</Link>
|
||||
</CustomMenu.MenuItem>
|
||||
</CustomMenu>
|
||||
<CustomMenu.MenuItem>
|
||||
<Link href={`/${workspaceSlug}/settings`}>
|
||||
<div className="flex items-center justify-start gap-2">
|
||||
<Settings className="h-3.5 w-3.5 stroke-[1.5]" />
|
||||
<span>Settings</span>
|
||||
</div>
|
||||
</Link>
|
||||
</CustomMenu.MenuItem>
|
||||
</CustomMenu>
|
||||
)}
|
||||
<Disclosure.Button
|
||||
as="button"
|
||||
className="sticky top-0 z-10 group/workspace-button px-0.5 py-1.5 flex items-center justify-between gap-1 text-custom-sidebar-text-400 hover:bg-custom-sidebar-background-90 rounded text-xs font-semibold"
|
||||
|
||||
@@ -48,7 +48,7 @@ export const ProjectAuthWrapper: FC<IProjectAuthWrapper> = 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(
|
||||
|
||||
Reference in New Issue
Block a user