From e724194161c614342c2a757e1e0abc4bc400206c Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia Date: Sat, 10 Aug 2024 15:29:12 +0530 Subject: [PATCH 01/30] chore: user store code refactor --- .../command-palette/command-palette.tsx | 16 ++-- web/core/store/user/index.ts | 80 ++++++++++++++++--- 2 files changed, 77 insertions(+), 19 deletions(-) diff --git a/web/core/components/command-palette/command-palette.tsx b/web/core/components/command-palette/command-palette.tsx index 099d53f909b..49a7f999fdb 100644 --- a/web/core/components/command-palette/command-palette.tsx +++ b/web/core/components/command-palette/command-palette.tsx @@ -43,8 +43,8 @@ export const CommandPalette: FC = observer(() => { const { platform } = usePlatformOS(); const { data: currentUser, - canPerformProjectCreateActions, - canPerformWorkspaceCreateActions, + canPerformProjectMemberActions, + canPerformWorkspaceMemberActions, canPerformAnyCreateAction, canPerformProjectAdminActions, } = useUser(); @@ -103,15 +103,15 @@ export const CommandPalette: FC = observer(() => { // auth const performProjectCreateActions = useCallback( (showToast: boolean = true) => { - if (!canPerformProjectCreateActions && showToast) + if (!canPerformProjectMemberActions && showToast) setToast({ type: TOAST_TYPE.ERROR, title: "You don't have permission to perform this action.", }); - return canPerformProjectCreateActions; + return canPerformProjectMemberActions; }, - [canPerformProjectCreateActions] + [canPerformProjectMemberActions] ); const performProjectBulkDeleteActions = useCallback( @@ -129,14 +129,14 @@ export const CommandPalette: FC = observer(() => { const performWorkspaceCreateActions = useCallback( (showToast: boolean = true) => { - if (!canPerformWorkspaceCreateActions && showToast) + if (!canPerformWorkspaceMemberActions && showToast) setToast({ type: TOAST_TYPE.ERROR, title: "You don't have permission to perform this action.", }); - return canPerformWorkspaceCreateActions; + return canPerformWorkspaceMemberActions; }, - [canPerformWorkspaceCreateActions] + [canPerformWorkspaceMemberActions] ); const performAnyProjectCreateActions = useCallback( diff --git a/web/core/store/user/index.ts b/web/core/store/user/index.ts index d087811a76b..56eee659aa4 100644 --- a/web/core/store/user/index.ts +++ b/web/core/store/user/index.ts @@ -42,9 +42,18 @@ export interface IUserStore { reset: () => void; signOut: () => Promise; // computed - canPerformProjectCreateActions: boolean; + + // workspace level + canPerformWorkspaceAdminActions: boolean; + canPerformWorkspaceMemberActions: boolean; + canPerformWorkspaceViewerActions: boolean; + canPerformWorkspaceGuestActions: boolean; + + // project level canPerformProjectAdminActions: boolean; - canPerformWorkspaceCreateActions: boolean; + canPerformProjectMemberActions: boolean; + canPerformProjectViewerActions: boolean; + canPerformProjectGuestActions: boolean; canPerformAnyCreateAction: boolean; projectsWithCreatePermissions: { [projectId: string]: number } | null; } @@ -92,9 +101,16 @@ export class UserStore implements IUserStore { reset: action, signOut: action, // computed - canPerformProjectCreateActions: computed, + canPerformWorkspaceAdminActions: computed, + canPerformWorkspaceMemberActions: computed, + canPerformWorkspaceViewerActions: computed, + canPerformWorkspaceGuestActions: computed, + canPerformProjectAdminActions: computed, - canPerformWorkspaceCreateActions: computed, + canPerformProjectMemberActions: computed, + canPerformProjectViewerActions: computed, + canPerformProjectGuestActions: computed, + canPerformAnyCreateAction: computed, projectsWithCreatePermissions: computed, }); @@ -273,15 +289,40 @@ export class UserStore implements IUserStore { } /** - * @description tells if user has project create actions permissions + * @description returns true if user has workspace admin actions permissions * @returns {boolean} */ - get canPerformProjectCreateActions() { - return !!this.membership.currentProjectRole && this.membership.currentProjectRole >= EUserProjectRoles.MEMBER; + get canPerformWorkspaceAdminActions() { + return !!this.membership.currentWorkspaceRole && this.membership.currentWorkspaceRole === EUserWorkspaceRoles.ADMIN; + } + + /** + * @description returns true if user has workspace member actions permissions + * @returns {boolean} + */ + get canPerformWorkspaceMemberActions() { + return !!this.membership.currentWorkspaceRole && this.membership.currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER; } /** - * @description tells if user has project admin actions permissions + * @description returns true if user has workspace viewer actions permissions + * @returns {boolean} + */ + + get canPerformWorkspaceViewerActions() { + return !!this.membership.currentWorkspaceRole && this.membership.currentWorkspaceRole >= EUserWorkspaceRoles.VIEWER; + } + + /** + * @description returns true if user has workspace guest actions permissions + * @returns {boolean} + */ + get canPerformWorkspaceGuestActions() { + return !!this.membership.currentWorkspaceRole && this.membership.currentWorkspaceRole >= EUserWorkspaceRoles.GUEST; + } + + /** + * @description returns true if user has project admin actions permissions * @returns {boolean} */ get canPerformProjectAdminActions() { @@ -289,10 +330,27 @@ export class UserStore implements IUserStore { } /** - * @description tells if user has workspace create actions permissions + * @description returns true if user has project member actions permissions * @returns {boolean} */ - get canPerformWorkspaceCreateActions() { - return !!this.membership.currentWorkspaceRole && this.membership.currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER; + get canPerformProjectMemberActions() { + return !!this.membership.currentProjectRole && this.membership.currentProjectRole >= EUserProjectRoles.MEMBER; + } + + /** + * @description returns true if user has project viewer actions permissions + * @returns {boolean} + */ + + get canPerformProjectViewerActions() { + return !!this.membership.currentProjectRole && this.membership.currentProjectRole >= EUserProjectRoles.VIEWER; + } + + /** + * @description returns true if user has project guest actions permissions + * @returns {boolean} + */ + get canPerformProjectGuestActions() { + return !!this.membership.currentProjectRole && this.membership.currentProjectRole >= EUserProjectRoles.GUEST; } } From e1471fe4a2c999f6b19f247d394dc24819d37023 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia Date: Sat, 10 Aug 2024 16:06:00 +0530 Subject: [PATCH 02/30] chore: general unauthorized screen asset added --- web/public/auth/unauthorized.svg | 44 ++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 web/public/auth/unauthorized.svg diff --git a/web/public/auth/unauthorized.svg b/web/public/auth/unauthorized.svg new file mode 100644 index 00000000000..2bad48a570f --- /dev/null +++ b/web/public/auth/unauthorized.svg @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Error unauthorized + \ No newline at end of file From aa73ed1549c7d4a8cab6d154ecd24fc57b92bed4 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia Date: Sat, 10 Aug 2024 16:07:22 +0530 Subject: [PATCH 03/30] chore: workspace setting sidebar options updated for guest and viewer --- web/ce/constants/workspace.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/ce/constants/workspace.ts b/web/ce/constants/workspace.ts index b89ced416db..109a7a4d147 100644 --- a/web/ce/constants/workspace.ts +++ b/web/ce/constants/workspace.ts @@ -17,7 +17,7 @@ export const WORKSPACE_SETTINGS = { key: "members", label: "Members", href: `/settings/members`, - access: EUserWorkspaceRoles.GUEST, + access: EUserWorkspaceRoles.VIEWER, highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/settings/members/`, Icon: SettingIcon, }, @@ -33,7 +33,7 @@ export const WORKSPACE_SETTINGS = { key: "export", label: "Exports", href: `/settings/exports`, - access: EUserWorkspaceRoles.MEMBER, + access: EUserWorkspaceRoles.VIEWER, highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/settings/exports/`, Icon: SettingIcon, }, From 1089c2fa1f3eec9bcb76e32147d45b951d7c0805 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia Date: Sat, 10 Aug 2024 16:08:41 +0530 Subject: [PATCH 04/30] chore: NotAuthorizedView component code updated --- .../(detail)/[projectId]/settings/layout.tsx | 3 +- .../auth-screens/not-authorized-view.tsx | 47 ++++--------------- 2 files changed, 11 insertions(+), 39 deletions(-) diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/layout.tsx b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/layout.tsx index 8553d86aa51..ad498c837aa 100644 --- a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/layout.tsx +++ b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/layout.tsx @@ -35,7 +35,7 @@ const ProjectSettingLayout: FC = observer((props) => { if (restrictViewSettings) { return ( @@ -44,6 +44,7 @@ const ProjectSettingLayout: FC = observer((props) => { } + isProjectView /> ); } diff --git a/web/core/components/auth-screens/not-authorized-view.tsx b/web/core/components/auth-screens/not-authorized-view.tsx index f8f101dd30b..fe344f468f8 100644 --- a/web/core/components/auth-screens/not-authorized-view.tsx +++ b/web/core/components/auth-screens/not-authorized-view.tsx @@ -1,62 +1,33 @@ import React from "react"; import { observer } from "mobx-react"; import Image from "next/image"; -import Link from "next/link"; -import { useSearchParams } from "next/navigation"; -// hooks -import { useUser } from "@/hooks/store"; // layouts import DefaultLayout from "@/layouts/default-layout"; // images import ProjectNotAuthorizedImg from "@/public/auth/project-not-authorized.svg"; +import Unauthorized from "@/public/auth/unauthorized.svg"; import WorkspaceNotAuthorizedImg from "@/public/auth/workspace-not-authorized.svg"; type Props = { actionButton?: React.ReactNode; - type: "project" | "workspace"; + section?: "settings" | "general"; + isProjectView?: boolean; }; export const NotAuthorizedView: React.FC = observer((props) => { - const { actionButton, type } = props; - // router - const searchParams = useSearchParams(); - const next_path = searchParams.get("next_path"); - // hooks - const { data: currentUser } = useUser(); + const { actionButton, section = "general", isProjectView = false } = props; + + // assets + const settingAsset = isProjectView ? ProjectNotAuthorizedImg : WorkspaceNotAuthorizedImg; + const asset = section === "settings" ? settingAsset : Unauthorized; return (
- ProjectSettingImg + ProjectSettingImg

Oops! You are not authorized to view this page

- -
- {currentUser ? ( -

- You have signed in as {currentUser.email}.
- - Sign in - {" "} - with different account that has access to this page. -

- ) : ( -

- You need to{" "} - - Sign in - {" "} - with an account that has access to this page. -

- )} -
- {actionButton}
From acac14ce8fcbf214e59245681da3fd06664bcbe1 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia Date: Sat, 10 Aug 2024 16:23:19 +0530 Subject: [PATCH 05/30] chore: project setting layout code refactor --- .../(detail)/[projectId]/settings/layout.tsx | 40 +------------------ 1 file changed, 2 insertions(+), 38 deletions(-) diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/layout.tsx b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/layout.tsx index ad498c837aa..743c14d1030 100644 --- a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/layout.tsx +++ b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/layout.tsx @@ -1,18 +1,8 @@ "use client"; import { FC, ReactNode } from "react"; -import { observer } from "mobx-react"; -import Link from "next/link"; -import { useParams } from "next/navigation"; -// ui -import { Button, LayersIcon } from "@plane/ui"; // components -import { NotAuthorizedView } from "@/components/auth-screens"; import { AppHeader, ContentWrapper } from "@/components/core"; -// constants -import { EUserProjectRoles } from "@/constants/project"; -// hooks -import { useUser } from "@/hooks/store"; // local components import { ProjectSettingHeader } from "./header"; import { ProjectSettingsSidebar } from "./sidebar"; @@ -21,34 +11,8 @@ export interface IProjectSettingLayout { children: ReactNode; } -const ProjectSettingLayout: FC = observer((props) => { +const ProjectSettingLayout: FC = (props) => { const { children } = props; - // router - const { workspaceSlug, projectId } = useParams(); - // store hooks - const { - membership: { currentProjectRole }, - } = useUser(); - - const restrictViewSettings = currentProjectRole && currentProjectRole <= EUserProjectRoles.VIEWER; - - if (restrictViewSettings) { - return ( - - - - } - isProjectView - /> - ); - } - return ( <> } /> @@ -64,6 +28,6 @@ const ProjectSettingLayout: FC = observer((props) => { ); -}); +}; export default ProjectSettingLayout; From 0ff7f12b63bf8385c74b50088a62f9982b6c0923 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia Date: Sat, 10 Aug 2024 16:30:44 +0530 Subject: [PATCH 06/30] chore: workspace setting members and exports page permission validation added --- .../(projects)/settings/exports/page.tsx | 24 ++++++------------- .../(projects)/settings/members/page.tsx | 16 +++++++------ 2 files changed, 16 insertions(+), 24 deletions(-) diff --git a/web/app/[workspaceSlug]/(projects)/settings/exports/page.tsx b/web/app/[workspaceSlug]/(projects)/settings/exports/page.tsx index 59fd4d2c754..0121b5edf22 100644 --- a/web/app/[workspaceSlug]/(projects)/settings/exports/page.tsx +++ b/web/app/[workspaceSlug]/(projects)/settings/exports/page.tsx @@ -2,34 +2,24 @@ import { observer } from "mobx-react"; // components +import { NotAuthorizedView } from "@/components/auth-screens"; import { PageHead } from "@/components/core"; import ExportGuide from "@/components/exporter/guide"; -// constants -import { EUserWorkspaceRoles } from "@/constants/workspace"; // hooks import { useUser, useWorkspace } from "@/hooks/store"; const ExportsPage = observer(() => { // store hooks - const { - membership: { currentWorkspaceRole }, - } = useUser(); + const { canPerformWorkspaceViewerActions } = useUser(); const { currentWorkspace } = useWorkspace(); // derived values - const hasPageAccess = - currentWorkspaceRole && [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER].includes(currentWorkspaceRole); const pageTitle = currentWorkspace?.name ? `${currentWorkspace.name} - Exports` : undefined; - if (!hasPageAccess) - return ( - <> - -
-

You are not authorized to access this page.

-
- - ); + // if user is not authorized to view this page + if (!canPerformWorkspaceViewerActions) { + return ; + } return ( <> @@ -44,4 +34,4 @@ const ExportsPage = observer(() => { ); }); -export default ExportsPage; \ No newline at end of file +export default ExportsPage; diff --git a/web/app/[workspaceSlug]/(projects)/settings/members/page.tsx b/web/app/[workspaceSlug]/(projects)/settings/members/page.tsx index 52b2256398f..b445b0355de 100644 --- a/web/app/[workspaceSlug]/(projects)/settings/members/page.tsx +++ b/web/app/[workspaceSlug]/(projects)/settings/members/page.tsx @@ -9,11 +9,11 @@ import { IWorkspaceBulkInviteFormData } from "@plane/types"; // ui import { Button, TOAST_TYPE, setToast } from "@plane/ui"; // components +import { NotAuthorizedView } from "@/components/auth-screens"; import { PageHead } from "@/components/core"; import { SendWorkspaceInvitationModal, WorkspaceMembersList } from "@/components/workspace"; // constants import { MEMBER_INVITED } from "@/constants/event-tracker"; -import { EUserWorkspaceRoles } from "@/constants/workspace"; // helpers import { getUserRole } from "@/helpers/user.helper"; // hooks @@ -27,9 +27,7 @@ const WorkspaceMembersSettingsPage = observer(() => { const { workspaceSlug } = useParams(); // store hooks const { captureEvent } = useEventTracker(); - const { - membership: { currentWorkspaceRole }, - } = useUser(); + const { canPerformWorkspaceAdminActions, canPerformWorkspaceViewerActions } = useUser(); const { workspace: { inviteMembersToWorkspace }, } = useMember(); @@ -79,9 +77,13 @@ const WorkspaceMembersSettingsPage = observer(() => { }; // derived values - const isAdmin = currentWorkspaceRole && [EUserWorkspaceRoles.ADMIN].includes(currentWorkspaceRole); const pageTitle = currentWorkspace?.name ? `${currentWorkspace.name} - Members` : undefined; + // if user is not authorized to view this page + if (!canPerformWorkspaceViewerActions) { + return ; + } + return ( <> @@ -103,13 +105,13 @@ const WorkspaceMembersSettingsPage = observer(() => { onChange={(e) => setSearchQuery(e.target.value)} /> - {isAdmin && ( + {canPerformWorkspaceAdminActions && ( )} - + ); From b50ebd321c121b98cc89e1a222207fa9a18213f4 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia Date: Sat, 10 Aug 2024 16:37:02 +0530 Subject: [PATCH 07/30] chore: workspace members and exports settings page improvement --- .../(projects)/settings/exports/page.tsx | 10 ++++++++-- .../(projects)/settings/members/page.tsx | 10 ++++++++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/web/app/[workspaceSlug]/(projects)/settings/exports/page.tsx b/web/app/[workspaceSlug]/(projects)/settings/exports/page.tsx index 0121b5edf22..e298a4c3048 100644 --- a/web/app/[workspaceSlug]/(projects)/settings/exports/page.tsx +++ b/web/app/[workspaceSlug]/(projects)/settings/exports/page.tsx @@ -5,12 +5,14 @@ import { observer } from "mobx-react"; import { NotAuthorizedView } from "@/components/auth-screens"; import { PageHead } from "@/components/core"; import ExportGuide from "@/components/exporter/guide"; +// helpers +import { cn } from "@/helpers/common.helper"; // hooks import { useUser, useWorkspace } from "@/hooks/store"; const ExportsPage = observer(() => { // store hooks - const { canPerformWorkspaceViewerActions } = useUser(); + const { canPerformWorkspaceViewerActions, canPerformWorkspaceMemberActions } = useUser(); const { currentWorkspace } = useWorkspace(); // derived values @@ -24,7 +26,11 @@ const ExportsPage = observer(() => { return ( <> -
+

Exports

diff --git a/web/app/[workspaceSlug]/(projects)/settings/members/page.tsx b/web/app/[workspaceSlug]/(projects)/settings/members/page.tsx index b445b0355de..75334832a15 100644 --- a/web/app/[workspaceSlug]/(projects)/settings/members/page.tsx +++ b/web/app/[workspaceSlug]/(projects)/settings/members/page.tsx @@ -15,6 +15,7 @@ import { SendWorkspaceInvitationModal, WorkspaceMembersList } from "@/components // constants import { MEMBER_INVITED } from "@/constants/event-tracker"; // helpers +import { cn } from "@/helpers/common.helper"; import { getUserRole } from "@/helpers/user.helper"; // hooks import { useEventTracker, useMember, useUser, useWorkspace } from "@/hooks/store"; @@ -27,7 +28,8 @@ const WorkspaceMembersSettingsPage = observer(() => { const { workspaceSlug } = useParams(); // store hooks const { captureEvent } = useEventTracker(); - const { canPerformWorkspaceAdminActions, canPerformWorkspaceViewerActions } = useUser(); + const { canPerformWorkspaceAdminActions, canPerformWorkspaceViewerActions, canPerformWorkspaceMemberActions } = + useUser(); const { workspace: { inviteMembersToWorkspace }, } = useMember(); @@ -92,7 +94,11 @@ const WorkspaceMembersSettingsPage = observer(() => { onClose={() => setInviteModal(false)} onSubmit={handleWorkspaceInvite} /> -
+

Members

From 8ae4c7c551709277f9c30a58ede94b3a494ea84f Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia Date: Mon, 12 Aug 2024 13:59:37 +0530 Subject: [PATCH 08/30] chore: project invite modal updated --- .../project/send-project-invitation-modal.tsx | 35 +++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/web/core/components/project/send-project-invitation-modal.tsx b/web/core/components/project/send-project-invitation-modal.tsx index c0281e840dc..8a2297335fa 100644 --- a/web/core/components/project/send-project-invitation-modal.tsx +++ b/web/core/components/project/send-project-invitation-modal.tsx @@ -12,7 +12,7 @@ import { Avatar, Button, CustomSelect, CustomSearchSelect, TOAST_TYPE, setToast // helpers import { PROJECT_MEMBER_ADDED } from "@/constants/event-tracker"; import { EUserProjectRoles } from "@/constants/project"; -import { ROLE } from "@/constants/workspace"; +import { EUserWorkspaceRoles, ROLE } from "@/constants/workspace"; import { useEventTracker, useMember, useUser } from "@/hooks/store"; // constants @@ -56,6 +56,8 @@ export const SendProjectInvitationModal: React.FC = observer((props) => { // form info const { formState: { errors, isSubmitting }, + watch, + setValue, reset, handleSubmit, control, @@ -167,6 +169,25 @@ export const SendProjectInvitationModal: React.FC = observer((props) => { }[] | undefined; + const checkCurrentOptionWorkspaceRole = (value: string) => { + const selectedMemberWorkspaceRole = getWorkspaceMemberDetails(value)?.role; + if (!value || !selectedMemberWorkspaceRole) return ROLE; + + // Filter roles based on the selected member's workspace role + const isGuestOrViewer = [EUserWorkspaceRoles.GUEST, EUserWorkspaceRoles.VIEWER].includes( + selectedMemberWorkspaceRole + ); + + const filteredRoles = Object.fromEntries( + Object.entries(ROLE).filter(([key]) => { + const roleKey = parseInt(key); + return isGuestOrViewer ? roleKey === selectedMemberWorkspaceRole : roleKey <= selectedMemberWorkspaceRole; + }) + ); + + return filteredRoles; + }; + return ( @@ -237,6 +258,14 @@ export const SendProjectInvitationModal: React.FC = observer((props) => { } onChange={(val: string) => { onChange(val); + // Update the role to the workspace role when member ID changes + const workspaceMemberDetails = getWorkspaceMemberDetails(val); + const workspaceRole = workspaceMemberDetails?.role ?? 5; + const newValue = ROLE[workspaceRole].toUpperCase(); + setValue( + `members.${index}.role`, + EUserProjectRoles[newValue as keyof typeof EUserProjectRoles] + ); }} options={options} optionsClassName="w-full" @@ -271,7 +300,9 @@ export const SendProjectInvitationModal: React.FC = observer((props) => { input optionsClassName="w-full" > - {Object.entries(ROLE).map(([key, label]) => { + {Object.entries( + checkCurrentOptionWorkspaceRole(watch(`members.${index}.member_id`)) + ).map(([key, label]) => { if (parseInt(key) > (currentProjectRole ?? EUserProjectRoles.GUEST)) return null; return ( From d2cf355c5794102015737809390fe3850e790924 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia Date: Mon, 12 Aug 2024 14:10:57 +0530 Subject: [PATCH 09/30] chore: workspace setting unauthorized access empty state --- .../(projects)/settings/api-tokens/page.tsx | 26 +++++++------------ .../(projects)/settings/billing/page.tsx | 20 ++++---------- .../(projects)/settings/webhooks/page.tsx | 23 +++++----------- 3 files changed, 21 insertions(+), 48 deletions(-) diff --git a/web/app/[workspaceSlug]/(projects)/settings/api-tokens/page.tsx b/web/app/[workspaceSlug]/(projects)/settings/api-tokens/page.tsx index 906fee32845..7c9241b3937 100644 --- a/web/app/[workspaceSlug]/(projects)/settings/api-tokens/page.tsx +++ b/web/app/[workspaceSlug]/(projects)/settings/api-tokens/page.tsx @@ -8,13 +8,13 @@ import useSWR from "swr"; import { Button } from "@plane/ui"; // component import { ApiTokenListItem, CreateApiTokenModal } from "@/components/api-token"; +import { NotAuthorizedView } from "@/components/auth-screens"; import { PageHead } from "@/components/core"; import { EmptyState } from "@/components/empty-state"; import { APITokenSettingsLoader } from "@/components/ui"; // constants import { EmptyStateType } from "@/constants/empty-state"; import { API_TOKENS_LIST } from "@/constants/fetch-keys"; -import { EUserWorkspaceRoles } from "@/constants/workspace"; // store hooks import { useUser, useWorkspace } from "@/hooks/store"; // services @@ -28,28 +28,20 @@ const ApiTokensPage = observer(() => { // router const { workspaceSlug } = useParams(); // store hooks - const { - membership: { currentWorkspaceRole }, - } = useUser(); + const { canPerformWorkspaceAdminActions } = useUser(); const { currentWorkspace } = useWorkspace(); - const isAdmin = currentWorkspaceRole === EUserWorkspaceRoles.ADMIN; - - const { data: tokens } = useSWR(workspaceSlug && isAdmin ? API_TOKENS_LIST(workspaceSlug.toString()) : null, () => - workspaceSlug && isAdmin ? apiTokenService.getApiTokens(workspaceSlug.toString()) : null + const { data: tokens } = useSWR( + workspaceSlug && canPerformWorkspaceAdminActions ? API_TOKENS_LIST(workspaceSlug.toString()) : null, + () => + workspaceSlug && canPerformWorkspaceAdminActions ? apiTokenService.getApiTokens(workspaceSlug.toString()) : null ); const pageTitle = currentWorkspace?.name ? `${currentWorkspace.name} - API Tokens` : undefined; - if (!isAdmin) - return ( - <> - -
-

You are not authorized to access this page.

-
- - ); + if (!canPerformWorkspaceAdminActions) { + return ; + } if (!tokens) { return ; diff --git a/web/app/[workspaceSlug]/(projects)/settings/billing/page.tsx b/web/app/[workspaceSlug]/(projects)/settings/billing/page.tsx index 3e3c586c473..e436e5edd70 100644 --- a/web/app/[workspaceSlug]/(projects)/settings/billing/page.tsx +++ b/web/app/[workspaceSlug]/(projects)/settings/billing/page.tsx @@ -2,9 +2,8 @@ import { observer } from "mobx-react"; // component +import { NotAuthorizedView } from "@/components/auth-screens"; import { PageHead } from "@/components/core"; -// constants -import { EUserWorkspaceRoles } from "@/constants/workspace"; // hooks import { useUser, useWorkspace } from "@/hooks/store"; // plane web components @@ -12,23 +11,14 @@ import { BillingRoot } from "@/plane-web/components/workspace"; const BillingSettingsPage = observer(() => { // store hooks - const { - membership: { currentWorkspaceRole }, - } = useUser(); + const { canPerformWorkspaceAdminActions } = useUser(); const { currentWorkspace } = useWorkspace(); // derived values - const isAdmin = currentWorkspaceRole === EUserWorkspaceRoles.ADMIN; const pageTitle = currentWorkspace?.name ? `${currentWorkspace.name} - Billing & Plans` : undefined; - if (!isAdmin) - return ( - <> - -
-

You are not authorized to access this page.

-
- - ); + if (!canPerformWorkspaceAdminActions) { + return ; + } return ( <> diff --git a/web/app/[workspaceSlug]/(projects)/settings/webhooks/page.tsx b/web/app/[workspaceSlug]/(projects)/settings/webhooks/page.tsx index 695f1f16b28..2d0629b705a 100644 --- a/web/app/[workspaceSlug]/(projects)/settings/webhooks/page.tsx +++ b/web/app/[workspaceSlug]/(projects)/settings/webhooks/page.tsx @@ -7,6 +7,7 @@ import useSWR from "swr"; // ui import { Button } from "@plane/ui"; // components +import { NotAuthorizedView } from "@/components/auth-screens"; import { PageHead } from "@/components/core"; import { EmptyState } from "@/components/empty-state"; import { WebhookSettingsLoader } from "@/components/ui"; @@ -22,17 +23,13 @@ const WebhooksListPage = observer(() => { // router const { workspaceSlug } = useParams(); // mobx store - const { - membership: { currentWorkspaceRole }, - } = useUser(); + const { canPerformWorkspaceAdminActions } = useUser(); const { fetchWebhooks, webhooks, clearSecretKey, webhookSecretKey, createWebhook } = useWebhook(); const { currentWorkspace } = useWorkspace(); - const isAdmin = currentWorkspaceRole === 20; - useSWR( - workspaceSlug && isAdmin ? `WEBHOOKS_LIST_${workspaceSlug}` : null, - workspaceSlug && isAdmin ? () => fetchWebhooks(workspaceSlug.toString()) : null + workspaceSlug && canPerformWorkspaceAdminActions ? `WEBHOOKS_LIST_${workspaceSlug}` : null, + workspaceSlug && canPerformWorkspaceAdminActions ? () => fetchWebhooks(workspaceSlug.toString()) : null ); const pageTitle = currentWorkspace?.name ? `${currentWorkspace.name} - Webhooks` : undefined; @@ -42,15 +39,9 @@ const WebhooksListPage = observer(() => { if (!showCreateWebhookModal && webhookSecretKey) clearSecretKey(); }, [showCreateWebhookModal, webhookSecretKey, clearSecretKey]); - if (!isAdmin) - return ( - <> - -
-

You are not authorized to access this page.

-
- - ); + if (!canPerformWorkspaceAdminActions) { + return ; + } if (!webhooks) return ; From fa518737316369c99537cc6451cd5ae7f886c450 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia Date: Mon, 12 Aug 2024 14:13:11 +0530 Subject: [PATCH 10/30] chore: workspace setting unauthorized access empty state --- web/app/[workspaceSlug]/(projects)/settings/exports/page.tsx | 2 +- web/app/[workspaceSlug]/(projects)/settings/members/page.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/web/app/[workspaceSlug]/(projects)/settings/exports/page.tsx b/web/app/[workspaceSlug]/(projects)/settings/exports/page.tsx index e298a4c3048..21e9beccefb 100644 --- a/web/app/[workspaceSlug]/(projects)/settings/exports/page.tsx +++ b/web/app/[workspaceSlug]/(projects)/settings/exports/page.tsx @@ -20,7 +20,7 @@ const ExportsPage = observer(() => { // if user is not authorized to view this page if (!canPerformWorkspaceViewerActions) { - return ; + return ; } return ( diff --git a/web/app/[workspaceSlug]/(projects)/settings/members/page.tsx b/web/app/[workspaceSlug]/(projects)/settings/members/page.tsx index 75334832a15..1d4c0f18cf5 100644 --- a/web/app/[workspaceSlug]/(projects)/settings/members/page.tsx +++ b/web/app/[workspaceSlug]/(projects)/settings/members/page.tsx @@ -83,7 +83,7 @@ const WorkspaceMembersSettingsPage = observer(() => { // if user is not authorized to view this page if (!canPerformWorkspaceViewerActions) { - return ; + return ; } return ( From f0906ae8b449d299284a962576c2c85c3b6d5ada Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia Date: Mon, 12 Aug 2024 14:38:30 +0530 Subject: [PATCH 11/30] chore: project settings sidebar permission updated --- .../projects/(detail)/[projectId]/settings/sidebar.tsx | 5 +++-- web/core/constants/project.ts | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/sidebar.tsx b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/sidebar.tsx index 0b9a94f82e0..3b60e152533 100644 --- a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/sidebar.tsx +++ b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/sidebar.tsx @@ -1,6 +1,7 @@ "use client"; import React from "react"; +import { observer } from "mobx-react"; import Link from "next/link"; import { useParams, usePathname } from "next/navigation"; // ui @@ -12,7 +13,7 @@ import { EUserProjectRoles, PROJECT_SETTINGS_LINKS } from "@/constants/project"; // hooks import { useUser } from "@/hooks/store"; -export const ProjectSettingsSidebar = () => { +export const ProjectSettingsSidebar = observer(() => { const { workspaceSlug, projectId } = useParams(); const pathname = usePathname(); // mobx store @@ -60,4 +61,4 @@ export const ProjectSettingsSidebar = () => {
); -}; +}); diff --git a/web/core/constants/project.ts b/web/core/constants/project.ts index 0737e77d0d3..2152a3407c0 100644 --- a/web/core/constants/project.ts +++ b/web/core/constants/project.ts @@ -79,7 +79,7 @@ export const PROJECT_SETTINGS_LINKS: { key: "general", label: "General", href: `/settings`, - access: EUserProjectRoles.MEMBER, + access: EUserProjectRoles.GUEST, highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/settings/`, Icon: SettingIcon, }, @@ -87,7 +87,7 @@ export const PROJECT_SETTINGS_LINKS: { key: "members", label: "Members", href: `/settings/members`, - access: EUserProjectRoles.MEMBER, + access: EUserProjectRoles.VIEWER, highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/settings/members/`, Icon: SettingIcon, }, From 8818beba67572580a581317be96767021b05ef84 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia Date: Mon, 12 Aug 2024 15:02:38 +0530 Subject: [PATCH 12/30] fix: project settings user role permission updated --- .../[projectId]/settings/automations/page.tsx | 14 +++++------ .../[projectId]/settings/estimates/page.tsx | 23 ++++++++++++------- .../[projectId]/settings/features/page.tsx | 22 +++++++----------- .../(detail)/[projectId]/settings/header.tsx | 21 +++++++++-------- .../[projectId]/settings/labels/page.tsx | 9 +++++++- .../[projectId]/settings/members/page.tsx | 8 ++++++- .../[projectId]/settings/states/page.tsx | 8 ++++++- 7 files changed, 64 insertions(+), 41 deletions(-) diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/automations/page.tsx b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/automations/page.tsx index 1676b25aa1c..380966b0044 100644 --- a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/automations/page.tsx +++ b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/automations/page.tsx @@ -7,10 +7,9 @@ import { IProject } from "@plane/types"; // ui import { TOAST_TYPE, setToast } from "@plane/ui"; // components +import { NotAuthorizedView } from "@/components/auth-screens"; import { AutoArchiveAutomation, AutoCloseAutomation } from "@/components/automation"; import { PageHead } from "@/components/core"; -// constants -import { EUserProjectRoles } from "@/constants/project"; // hooks import { useProject, useUser } from "@/hooks/store"; @@ -18,9 +17,7 @@ const AutomationSettingsPage = observer(() => { // router const { workspaceSlug, projectId } = useParams(); // store hooks - const { - membership: { currentProjectRole }, - } = useUser(); + const { canPerformProjectAdminActions } = useUser(); const { currentProjectDetails: projectDetails, updateProject } = useProject(); const handleChange = async (formData: Partial) => { @@ -36,13 +33,16 @@ const AutomationSettingsPage = observer(() => { }; // derived values - const isAdmin = currentProjectRole === EUserProjectRoles.ADMIN; const pageTitle = projectDetails?.name ? `${projectDetails?.name} - Automations` : undefined; + if (!canPerformProjectAdminActions) { + return ; + } + return ( <> -
+

Automations

diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/estimates/page.tsx b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/estimates/page.tsx index f292bd6d995..bbcf3f966f8 100644 --- a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/estimates/page.tsx +++ b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/estimates/page.tsx @@ -3,30 +3,37 @@ import { observer } from "mobx-react"; import { useParams } from "next/navigation"; // components +import { NotAuthorizedView } from "@/components/auth-screens"; import { PageHead } from "@/components/core"; import { EstimateRoot } from "@/components/estimates"; -// constants -import { EUserProjectRoles } from "@/constants/project"; // hooks import { useUser, useProject } from "@/hooks/store"; const EstimatesSettingsPage = observer(() => { const { workspaceSlug, projectId } = useParams(); - const { - membership: { currentProjectRole }, - } = useUser(); + const { canPerformProjectAdminActions } = useUser(); const { currentProjectDetails } = useProject(); // derived values - const isAdmin = currentProjectRole === EUserProjectRoles.ADMIN; const pageTitle = currentProjectDetails?.name ? `${currentProjectDetails?.name} - Estimates` : undefined; if (!workspaceSlug || !projectId) return <>; + + if (!canPerformProjectAdminActions) { + return ; + } + return ( <> -
- +
+
); diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/features/page.tsx b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/features/page.tsx index f09ae985b11..24c05b3f25c 100644 --- a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/features/page.tsx +++ b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/features/page.tsx @@ -2,41 +2,35 @@ import { observer } from "mobx-react"; import { useParams } from "next/navigation"; -import useSWR from "swr"; // components +import { NotAuthorizedView } from "@/components/auth-screens"; import { PageHead } from "@/components/core"; import { ProjectFeaturesList } from "@/components/project"; -// constants -import { EUserProjectRoles } from "@/constants/project"; // hooks import { useProject, useUser } from "@/hooks/store"; const FeaturesSettingsPage = observer(() => { const { workspaceSlug, projectId } = useParams(); // store - const { - membership: { fetchUserProjectInfo }, - } = useUser(); + const { canPerformProjectAdminActions } = useUser(); const { currentProjectDetails } = useProject(); - // fetch the project details - const { data: memberDetails } = useSWR( - workspaceSlug && projectId ? `PROJECT_MEMBERS_ME_${workspaceSlug}_${projectId}` : null, - workspaceSlug && projectId ? () => fetchUserProjectInfo(workspaceSlug.toString(), projectId.toString()) : null - ); // derived values - const isAdmin = memberDetails?.role === EUserProjectRoles.ADMIN; const pageTitle = currentProjectDetails?.name ? `${currentProjectDetails?.name} - Features` : undefined; if (!workspaceSlug || !projectId) return null; + if (!canPerformProjectAdminActions) { + return ; + } + return ( <> -
+
diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/header.tsx b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/header.tsx index be9e3781a3d..8cb142b751f 100644 --- a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/header.tsx +++ b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/header.tsx @@ -24,7 +24,7 @@ export const ProjectSettingHeader: FC = observer(() => { } = useUser(); const { currentProjectDetails, loader } = useProject(); - if (currentProjectRole && currentProjectRole <= EUserProjectRoles.VIEWER) return null; + const projectMemberInfo = currentProjectRole || EUserProjectRoles.GUEST; return (
@@ -70,14 +70,17 @@ export const ProjectSettingHeader: FC = observer(() => { placement="bottom-start" closeOnSelect > - {PROJECT_SETTINGS_LINKS.map((item) => ( - router.push(`/${workspaceSlug}/projects/${projectId}${item.href}`)} - > - {item.label} - - ))} + {PROJECT_SETTINGS_LINKS.map( + (item) => + projectMemberInfo >= item.access && ( + router.push(`/${workspaceSlug}/projects/${projectId}${item.href}`)} + > + {item.label} + + ) + )}
diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/labels/page.tsx b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/labels/page.tsx index 192d7147f70..df5357f40d9 100644 --- a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/labels/page.tsx +++ b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/labels/page.tsx @@ -5,13 +5,16 @@ import { combine } from "@atlaskit/pragmatic-drag-and-drop/combine"; import { autoScrollForElements } from "@atlaskit/pragmatic-drag-and-drop-auto-scroll/element"; import { observer } from "mobx-react"; // components +import { NotAuthorizedView } from "@/components/auth-screens"; import { PageHead } from "@/components/core"; import { ProjectSettingsLabelList } from "@/components/labels"; // hooks -import { useProject } from "@/hooks/store"; +import { useProject, useUser } from "@/hooks/store"; const LabelsSettingsPage = observer(() => { + // store hooks const { currentProjectDetails } = useProject(); + const { canPerformProjectMemberActions } = useUser(); const pageTitle = currentProjectDetails?.name ? `${currentProjectDetails?.name} - Labels` : undefined; const scrollableContainerRef = useRef(null); @@ -29,6 +32,10 @@ const LabelsSettingsPage = observer(() => { ); }, [scrollableContainerRef?.current]); + if (!canPerformProjectMemberActions) { + return ; + } + return ( <> diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/members/page.tsx b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/members/page.tsx index af1c82e12aa..56e59caa81c 100644 --- a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/members/page.tsx +++ b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/members/page.tsx @@ -2,17 +2,23 @@ import { observer } from "mobx-react"; // components +import { NotAuthorizedView } from "@/components/auth-screens"; import { PageHead } from "@/components/core"; import { ProjectMemberList, ProjectSettingsMemberDefaults } from "@/components/project"; // hooks -import { useProject } from "@/hooks/store"; +import { useProject, useUser } from "@/hooks/store"; const MembersSettingsPage = observer(() => { // store const { currentProjectDetails } = useProject(); + const { canPerformProjectViewerActions } = useUser(); // derived values const pageTitle = currentProjectDetails?.name ? `${currentProjectDetails?.name} - Members` : undefined; + if (!canPerformProjectViewerActions) { + return ; + } + return ( <> diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/states/page.tsx b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/states/page.tsx index 4132662bf02..cd94594b9d7 100644 --- a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/states/page.tsx +++ b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/states/page.tsx @@ -3,18 +3,24 @@ import { observer } from "mobx-react"; import { useParams } from "next/navigation"; // components +import { NotAuthorizedView } from "@/components/auth-screens"; import { PageHead } from "@/components/core"; import { ProjectStateRoot } from "@/components/project-states"; // hook -import { useProject } from "@/hooks/store"; +import { useProject, useUser } from "@/hooks/store"; const StatesSettingsPage = observer(() => { const { workspaceSlug, projectId } = useParams(); // store const { currentProjectDetails } = useProject(); + const { canPerformProjectMemberActions } = useUser(); // derived values const pageTitle = currentProjectDetails?.name ? `${currentProjectDetails?.name} - States` : undefined; + if (!canPerformProjectMemberActions) { + return ; + } + return ( <> From af31302f46a674d6b132f545580ff45dadb2535a Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia Date: Mon, 12 Aug 2024 15:27:24 +0530 Subject: [PATCH 13/30] chore: app sidebar role permission validation updated --- web/core/constants/dashboard.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/core/constants/dashboard.ts b/web/core/constants/dashboard.ts index a723a40ba6f..9775570721c 100644 --- a/web/core/constants/dashboard.ts +++ b/web/core/constants/dashboard.ts @@ -279,7 +279,7 @@ export const SIDEBAR_WORKSPACE_MENU_ITEMS: { key: "active-cycles", label: "Cycles", href: `/active-cycles`, - access: EUserWorkspaceRoles.GUEST, + access: EUserWorkspaceRoles.MEMBER, highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/active-cycles/`, Icon: ContrastIcon, }, @@ -317,7 +317,7 @@ export const SIDEBAR_USER_MENU_ITEMS: { key: "your-work", label: "Your work", href: "/profile", - access: EUserWorkspaceRoles.GUEST, + access: EUserWorkspaceRoles.MEMBER, highlight: (pathname: string, baseUrl: string, options?: TLinkOptions) => options?.userId ? pathname.includes(`${baseUrl}/profile/${options?.userId}`) : false, Icon: UserActivityIcon, From 32e9eb52548f3653800dcb88a3006cda97e4c83d Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia Date: Mon, 12 Aug 2024 15:45:53 +0530 Subject: [PATCH 14/30] chore: app sidebar role permission validation --- .../[workspaceSlug]/(projects)/sidebar.tsx | 5 +- .../workspace/sidebar/projects-list-item.tsx | 59 +++++++++++-------- 2 files changed, 39 insertions(+), 25 deletions(-) diff --git a/web/app/[workspaceSlug]/(projects)/sidebar.tsx b/web/app/[workspaceSlug]/(projects)/sidebar.tsx index be09caa9b40..338a8488b10 100644 --- a/web/app/[workspaceSlug]/(projects)/sidebar.tsx +++ b/web/app/[workspaceSlug]/(projects)/sidebar.tsx @@ -13,7 +13,7 @@ import { import { SidebarFavoritesMenu } from "@/components/workspace/sidebar/favorites/favorites-menu"; import { cn } from "@/helpers/common.helper"; // hooks -import { useAppTheme } from "@/hooks/store"; +import { useAppTheme, useUser } from "@/hooks/store"; import useOutsideClickDetector from "@/hooks/use-outside-click-detector"; // plane web components import useSize from "@/hooks/use-window-size"; @@ -23,6 +23,7 @@ export interface IAppSidebar {} export const AppSidebar: FC = observer(() => { // store hooks + const { canPerformWorkspaceMemberActions } = useUser(); const { toggleSidebar, sidebarCollapsed } = useAppTheme(); const windowSize = useSize(); // refs @@ -85,7 +86,7 @@ export const AppSidebar: FC = observer(() => { "opacity-0": !sidebarCollapsed, })} /> - + {canPerformWorkspaceMemberActions && }
diff --git a/web/core/components/workspace/sidebar/projects-list-item.tsx b/web/core/components/workspace/sidebar/projects-list-item.tsx index ea52fc29b6f..6a68f1b4c27 100644 --- a/web/core/components/workspace/sidebar/projects-list-item.tsx +++ b/web/core/components/workspace/sidebar/projects-list-item.tsx @@ -45,7 +45,7 @@ import { EUserProjectRoles } from "@/constants/project"; // helpers import { cn } from "@/helpers/common.helper"; // hooks -import { useAppTheme, useEventTracker, useProject } from "@/hooks/store"; +import { useAppTheme, useEventTracker, useProject, useUser } from "@/hooks/store"; import useOutsideClickDetector from "@/hooks/use-outside-click-detector"; import { usePlatformOS } from "@/hooks/use-platform-os"; // constants @@ -70,31 +70,37 @@ const navigation = (workspaceSlug: string, projectId: string) => [ name: "Issues", href: `/${workspaceSlug}/projects/${projectId}/issues`, Icon: LayersIcon, + access: EUserProjectRoles.GUEST, }, { name: "Cycles", href: `/${workspaceSlug}/projects/${projectId}/cycles`, Icon: ContrastIcon, + access: EUserProjectRoles.VIEWER, }, { name: "Modules", href: `/${workspaceSlug}/projects/${projectId}/modules`, Icon: DiceIcon, + access: EUserProjectRoles.VIEWER, }, { name: "Views", href: `/${workspaceSlug}/projects/${projectId}/views`, Icon: Layers, + access: EUserProjectRoles.GUEST, }, { name: "Pages", href: `/${workspaceSlug}/projects/${projectId}/pages`, Icon: FileText, + access: EUserProjectRoles.VIEWER, }, { name: "Intake", href: `/${workspaceSlug}/projects/${projectId}/inbox`, Icon: Intake, + access: EUserProjectRoles.GUEST, }, ]; @@ -106,6 +112,9 @@ export const SidebarProjectsListItem: React.FC = observer((props) => { const { setTrackElement } = useEventTracker(); const { addProjectToFavorites, removeProjectFromFavorites, getProjectById } = useProject(); const { isMobile } = usePlatformOS(); + const { + membership: { currentProjectRole }, + } = useUser(); // states const [leaveProjectModalOpen, setLeaveProjectModal] = useState(false); const [publishModalOpen, setPublishModal] = useState(false); @@ -480,31 +489,35 @@ export const SidebarProjectsListItem: React.FC = observer((props) => { (item.name === "Intake" && !project.inbox_view) ) return; - + const currentRole = currentProjectRole ?? 5; return ( - - - + {currentRole >= item.access && ( + -
- - {!isSidebarCollapsed && {item.name}} -
-
- -
+ + +
+ + {!isSidebarCollapsed && {item.name}} +
+
+ + + )} + ); })} From f0242c2919dade18d6f7909a32494403af4860c7 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia Date: Mon, 12 Aug 2024 16:35:15 +0530 Subject: [PATCH 15/30] chore: disabled page empty state validation --- .../(detail)/[projectId]/pages/(list)/page.tsx | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(list)/page.tsx b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(list)/page.tsx index b3ac8980f85..4171e1f332d 100644 --- a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(list)/page.tsx +++ b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(list)/page.tsx @@ -6,7 +6,10 @@ import { useParams, useSearchParams } from "next/navigation"; import { TPageNavigationTabs } from "@plane/types"; // components import { PageHead } from "@/components/core"; +import { EmptyState } from "@/components/empty-state"; import { PagesListRoot, PagesListView } from "@/components/pages"; +// constants +import { EmptyStateType } from "@/constants/empty-state"; // hooks import { useProject } from "@/hooks/store"; @@ -16,7 +19,7 @@ const ProjectPagesPage = observer(() => { const type = searchParams.get("type"); const { workspaceSlug, projectId } = useParams(); // store hooks - const { getProjectById } = useProject(); + const { getProjectById, currentProjectDetails } = useProject(); // derived values const project = projectId ? getProjectById(projectId.toString()) : undefined; const pageTitle = project?.name ? `${project?.name} - Pages` : undefined; @@ -29,6 +32,17 @@ const ProjectPagesPage = observer(() => { }; if (!workspaceSlug || !projectId) return <>; + + // No access to cycle + if (currentProjectDetails?.page_view === false) + return ( +
+ +
+ ); return ( <> From ca473779bf06a93ad495191aac2d999278837a14 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia Date: Mon, 12 Aug 2024 16:40:07 +0530 Subject: [PATCH 16/30] chore: app sidebar add project improvement --- web/core/components/workspace/sidebar/projects-list.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/web/core/components/workspace/sidebar/projects-list.tsx b/web/core/components/workspace/sidebar/projects-list.tsx index 568f9844396..c9943a54205 100644 --- a/web/core/components/workspace/sidebar/projects-list.tsx +++ b/web/core/components/workspace/sidebar/projects-list.tsx @@ -263,7 +263,6 @@ export const SidebarProjectsList: FC = observer(() => { toggleCreateProjectModal(true); }} > - {!isCollapsed && "Add project"} )} From bcf936462a78af251f04b48876b0d6c0195a6757 Mon Sep 17 00:00:00 2001 From: NarayanBavisetti Date: Mon, 12 Aug 2024 18:16:51 +0530 Subject: [PATCH 17/30] chore: guest role changes --- apiserver/plane/app/permissions/__init__.py | 1 + apiserver/plane/app/permissions/base.py | 59 ++++++++++++++++++ apiserver/plane/app/urls/inbox.py | 2 +- apiserver/plane/app/views/analytic/base.py | 26 +++----- apiserver/plane/app/views/cycle/archive.py | 9 ++- apiserver/plane/app/views/cycle/base.py | 32 ++++------ apiserver/plane/app/views/cycle/issue.py | 19 ++---- apiserver/plane/app/views/estimate/base.py | 12 ++-- apiserver/plane/app/views/exporter/base.py | 7 +-- apiserver/plane/app/views/external/base.py | 12 ++-- apiserver/plane/app/views/inbox/base.py | 57 +++++++++-------- apiserver/plane/app/views/issue/activity.py | 3 +- apiserver/plane/app/views/issue/archive.py | 9 ++- apiserver/plane/app/views/issue/attachment.py | 24 ++------ apiserver/plane/app/views/issue/base.py | 61 ++++++------------- apiserver/plane/app/views/issue/comment.py | 8 +-- apiserver/plane/app/views/issue/label.py | 10 +-- apiserver/plane/app/views/module/base.py | 21 ++++--- apiserver/plane/app/views/module/issue.py | 10 +-- .../plane/app/views/notification/base.py | 13 ++++ apiserver/plane/app/views/project/base.py | 39 ++++++++---- apiserver/plane/app/views/project/member.py | 13 ++-- apiserver/plane/app/views/search/issue.py | 14 ++++- apiserver/plane/app/views/view/base.py | 37 +++++++++++ apiserver/plane/app/views/webhook/base.py | 17 +++--- .../plane/app/views/workspace/favorite.py | 13 ++-- apiserver/plane/app/views/workspace/label.py | 2 +- 27 files changed, 302 insertions(+), 228 deletions(-) create mode 100644 apiserver/plane/app/permissions/base.py diff --git a/apiserver/plane/app/permissions/__init__.py b/apiserver/plane/app/permissions/__init__.py index 8e879350476..09e18f9055c 100644 --- a/apiserver/plane/app/permissions/__init__.py +++ b/apiserver/plane/app/permissions/__init__.py @@ -12,3 +12,4 @@ ProjectMemberPermission, ProjectLitePermission, ) +from .base import allow_permission \ No newline at end of file diff --git a/apiserver/plane/app/permissions/base.py b/apiserver/plane/app/permissions/base.py new file mode 100644 index 00000000000..09d4e6f8b44 --- /dev/null +++ b/apiserver/plane/app/permissions/base.py @@ -0,0 +1,59 @@ +from plane.db.models import WorkspaceMember, ProjectMember +from functools import wraps +from rest_framework.response import Response +from rest_framework import status + + +ROLE_VALUES = { + "ADMIN": 20, + "MEMBER": 15, + "VIEWER": 10, + "GUEST": 5, +} + + +def get_role_values(roles): + return [ROLE_VALUES.get(role.upper(), 0) for role in roles] + + +def allow_permission(roles, level="PROJECT", creator=False, model=None ): + def decorator(view_func): + @wraps(view_func) + def _wrapped_view(instance, request, *args, **kwargs): + + # Check for creator if required + if creator and model: + obj = model.objects.filter( + id=kwargs["pk"], created_by=request.user + ).exists() + if obj: + return view_func(instance, request, *args, **kwargs) + + # Check role permissions + if level == "WORKSPACE": + if WorkspaceMember.objects.filter( + member=request.user, + workspace__slug=kwargs["slug"], + role__in=get_role_values(roles), + is_active=True, + ).exists(): + return view_func(instance, request, *args, **kwargs) + else: + if ProjectMember.objects.filter( + member=request.user, + workspace__slug=kwargs["slug"], + project_id=kwargs["project_id"], + role__in=get_role_values(roles), + is_active=True, + ).exists(): + return view_func(instance, request, *args, **kwargs) + + # Return permission denied if no conditions are met + return Response( + {"error": "You don't have the required permissions."}, + status=status.HTTP_401_UNAUTHORIZED, + ) + + return _wrapped_view + + return decorator diff --git a/apiserver/plane/app/urls/inbox.py b/apiserver/plane/app/urls/inbox.py index b6848244ba6..6508c001d3a 100644 --- a/apiserver/plane/app/urls/inbox.py +++ b/apiserver/plane/app/urls/inbox.py @@ -40,7 +40,7 @@ name="inbox-issue", ), path( - "workspaces//projects//inbox-issues//", + "workspaces//projects//inbox-issues//", InboxIssueViewSet.as_view( { "get": "retrieve", diff --git a/apiserver/plane/app/views/analytic/base.py b/apiserver/plane/app/views/analytic/base.py index 3d27641e3c4..3fb8410c614 100644 --- a/apiserver/plane/app/views/analytic/base.py +++ b/apiserver/plane/app/views/analytic/base.py @@ -7,22 +7,20 @@ from rest_framework import status from rest_framework.response import Response +# Module imports from plane.app.permissions import WorkSpaceAdminPermission from plane.app.serializers import AnalyticViewSerializer - -# Module imports from plane.app.views.base import BaseAPIView, BaseViewSet from plane.bgtasks.analytic_plot_export import analytic_export_task from plane.db.models import AnalyticView, Issue, Workspace from plane.utils.analytics_plot import build_graph_plot from plane.utils.issue_filters import issue_filters +from plane.app.permissions import allow_permission class AnalyticsEndpoint(BaseAPIView): - permission_classes = [ - WorkSpaceAdminPermission, - ] + @allow_permission(["ADMIN", "MEMBER", "VIEWER"], level="WORKSPACE") def get(self, request, slug): x_axis = request.GET.get("x_axis", False) y_axis = request.GET.get("y_axis", False) @@ -201,10 +199,8 @@ def get_queryset(self): class SavedAnalyticEndpoint(BaseAPIView): - permission_classes = [ - WorkSpaceAdminPermission, - ] + @allow_permission(["ADMIN", "MEMBER", "VIEWER"], level="WORKSPACE") def get(self, request, slug, analytic_id): analytic_view = AnalyticView.objects.get( pk=analytic_id, workspace__slug=slug @@ -234,10 +230,8 @@ def get(self, request, slug, analytic_id): class ExportAnalyticsEndpoint(BaseAPIView): - permission_classes = [ - WorkSpaceAdminPermission, - ] + @allow_permission(["ADMIN", "MEMBER", "VIEWER"], level="WORKSPACE") def post(self, request, slug): x_axis = request.data.get("x_axis", False) y_axis = request.data.get("y_axis", False) @@ -301,10 +295,8 @@ def post(self, request, slug): class DefaultAnalyticsEndpoint(BaseAPIView): - permission_classes = [ - WorkSpaceAdminPermission, - ] + @allow_permission(["ADMIN", "MEMBER", "VIEWER"], level="WORKSPACE") def get(self, request, slug): filters = issue_filters(request.GET, "GET") base_issues = Issue.issue_objects.filter( @@ -380,12 +372,10 @@ def get(self, request, slug): .order_by("-count") ) - open_estimate_sum = open_issues_queryset.aggregate( - sum=Sum("point") - )["sum"] - total_estimate_sum = base_issues.aggregate(sum=Sum("point"))[ + open_estimate_sum = open_issues_queryset.aggregate(sum=Sum("point"))[ "sum" ] + total_estimate_sum = base_issues.aggregate(sum=Sum("point"))["sum"] return Response( { diff --git a/apiserver/plane/app/views/cycle/archive.py b/apiserver/plane/app/views/cycle/archive.py index 947f43a5f8d..789e7906b6c 100644 --- a/apiserver/plane/app/views/cycle/archive.py +++ b/apiserver/plane/app/views/cycle/archive.py @@ -24,7 +24,7 @@ # Third party imports from rest_framework import status from rest_framework.response import Response -from plane.app.permissions import ProjectEntityPermission +from plane.app.permissions import allow_permission from plane.db.models import Cycle, UserFavorite, Issue, Label, User, Project from plane.utils.analytics_plot import burndown_plot @@ -34,10 +34,6 @@ class CycleArchiveUnarchiveEndpoint(BaseAPIView): - permission_classes = [ - ProjectEntityPermission, - ] - def get_queryset(self): favorite_subquery = UserFavorite.objects.filter( user=self.request.user, @@ -292,6 +288,7 @@ def get_queryset(self): .distinct() ) + @allow_permission(["ADMIN", "MEMBER", "VIEWER"]) def get(self, request, slug, project_id, pk=None): if pk is None: queryset = ( @@ -596,6 +593,7 @@ def get(self, request, slug, project_id, pk=None): status=status.HTTP_200_OK, ) + @allow_permission(["ADMIN", "MEMBER"]) def post(self, request, slug, project_id, cycle_id): cycle = Cycle.objects.get( pk=cycle_id, project_id=project_id, workspace__slug=slug @@ -614,6 +612,7 @@ def post(self, request, slug, project_id, cycle_id): status=status.HTTP_200_OK, ) + @allow_permission(["ADMIN", "MEMBER"]) def delete(self, request, slug, project_id, cycle_id): cycle = Cycle.objects.get( pk=cycle_id, project_id=project_id, workspace__slug=slug diff --git a/apiserver/plane/app/views/cycle/base.py b/apiserver/plane/app/views/cycle/base.py index 6587b777521..a41a78a3744 100644 --- a/apiserver/plane/app/views/cycle/base.py +++ b/apiserver/plane/app/views/cycle/base.py @@ -29,8 +29,7 @@ from rest_framework import status from rest_framework.response import Response from plane.app.permissions import ( - ProjectEntityPermission, - ProjectLitePermission, + allow_permission, ) from plane.app.serializers import ( CycleSerializer, @@ -60,15 +59,6 @@ class CycleViewSet(BaseViewSet): serializer_class = CycleSerializer model = Cycle webhook_event = "cycle" - permission_classes = [ - ProjectEntityPermission, - ] - - def perform_create(self, serializer): - serializer.save( - project_id=self.kwargs.get("project_id"), - owned_by=self.request.user, - ) def get_queryset(self): favorite_subquery = UserFavorite.objects.filter( @@ -325,6 +315,7 @@ def get_queryset(self): .distinct() ) + @allow_permission(["ADMIN", "MEMBER", "VIEWER"]) def list(self, request, slug, project_id): queryset = self.get_queryset().filter(archived_at__isnull=True) cycle_view = request.GET.get("cycle_view", "all") @@ -611,6 +602,7 @@ def list(self, request, slug, project_id): ) return Response(data, status=status.HTTP_200_OK) + @allow_permission(["ADMIN", "MEMBER"]) def create(self, request, slug, project_id): if ( request.data.get("start_date", None) is None @@ -684,6 +676,7 @@ def create(self, request, slug, project_id): status=status.HTTP_400_BAD_REQUEST, ) + @allow_permission(["ADMIN", "MEMBER"]) def partial_update(self, request, slug, project_id, pk): queryset = self.get_queryset().filter( workspace__slug=slug, project_id=project_id, pk=pk @@ -771,6 +764,7 @@ def partial_update(self, request, slug, project_id, pk): return Response(cycle, status=status.HTTP_200_OK) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + @allow_permission(["ADMIN", "MEMBER", "VIEWER"]) def retrieve(self, request, slug, project_id, pk): queryset = ( self.get_queryset().filter(archived_at__isnull=True).filter(pk=pk) @@ -1039,6 +1033,7 @@ def retrieve(self, request, slug, project_id, pk): status=status.HTTP_200_OK, ) + @allow_permission(["ADMIN"], creator=True, model=Cycle) def destroy(self, request, slug, project_id, pk): cycle = Cycle.objects.get( workspace__slug=slug, project_id=project_id, pk=pk @@ -1097,10 +1092,8 @@ def destroy(self, request, slug, project_id, pk): class CycleDateCheckEndpoint(BaseAPIView): - permission_classes = [ - ProjectEntityPermission, - ] + @allow_permission(["ADMIN", "MEMBER"]) def post(self, request, slug, project_id): start_date = request.data.get("start_date", False) end_date = request.data.get("end_date", False) @@ -1144,6 +1137,7 @@ def get_queryset(self): .select_related("cycle", "cycle__owned_by") ) + @allow_permission(["ADMIN", "MEMBER"]) def create(self, request, slug, project_id): _ = UserFavorite.objects.create( project_id=project_id, @@ -1153,6 +1147,7 @@ def create(self, request, slug, project_id): ) return Response(status=status.HTTP_204_NO_CONTENT) + @allow_permission(["ADMIN", "MEMBER"]) def destroy(self, request, slug, project_id, cycle_id): cycle_favorite = UserFavorite.objects.get( project=project_id, @@ -1166,10 +1161,8 @@ def destroy(self, request, slug, project_id, cycle_id): class TransferCycleIssueEndpoint(BaseAPIView): - permission_classes = [ - ProjectEntityPermission, - ] + @allow_permission(["ADMIN", "MEMBER"]) def post(self, request, slug, project_id, cycle_id): new_cycle_id = request.data.get("new_cycle_id", False) @@ -1579,10 +1572,8 @@ def post(self, request, slug, project_id, cycle_id): class CycleUserPropertiesEndpoint(BaseAPIView): - permission_classes = [ - ProjectLitePermission, - ] + @allow_permission(["ADMIN", "MEMBER", "VIEWER", "GUEST"]) def patch(self, request, slug, project_id, cycle_id): cycle_properties = CycleUserProperties.objects.get( user=request.user, @@ -1605,6 +1596,7 @@ def patch(self, request, slug, project_id, cycle_id): serializer = CycleUserPropertiesSerializer(cycle_properties) return Response(serializer.data, status=status.HTTP_201_CREATED) + @allow_permission(["ADMIN", "MEMBER", "VIEWER", "GUEST"]) def get(self, request, slug, project_id, cycle_id): cycle_properties, _ = CycleUserProperties.objects.get_or_create( user=request.user, diff --git a/apiserver/plane/app/views/cycle/issue.py b/apiserver/plane/app/views/cycle/issue.py index 895289ec0d4..adb4dbcaa40 100644 --- a/apiserver/plane/app/views/cycle/issue.py +++ b/apiserver/plane/app/views/cycle/issue.py @@ -3,12 +3,7 @@ # Django imports from django.core import serializers -from django.db.models import ( - F, - Func, - OuterRef, - Q, -) +from django.db.models import F, Func, OuterRef, Q from django.utils import timezone from django.utils.decorators import method_decorator from django.views.decorators.gzip import gzip_page @@ -17,10 +12,6 @@ from rest_framework import status from rest_framework.response import Response -from plane.app.permissions import ( - ProjectEntityPermission, -) - # Module imports from .. import BaseViewSet from plane.app.serializers import ( @@ -45,6 +36,7 @@ GroupedOffsetPaginator, SubGroupedOffsetPaginator, ) +from plane.app.permissions import allow_permission class CycleIssueViewSet(BaseViewSet): @@ -54,10 +46,6 @@ class CycleIssueViewSet(BaseViewSet): webhook_event = "cycle_issue" bulk = True - permission_classes = [ - ProjectEntityPermission, - ] - filterset_fields = [ "issue__labels__id", "issue__assignees__id", @@ -92,6 +80,7 @@ def get_queryset(self): ) @method_decorator(gzip_page) + @allow_permission(["ADMIN", "MEMBER", "VIEWER"]) def list(self, request, slug, project_id, cycle_id): order_by_param = request.GET.get("order_by", "created_at") filters = issue_filters(request.query_params, "GET") @@ -238,6 +227,7 @@ def list(self, request, slug, project_id, cycle_id): ), ) + @allow_permission(["ADMIN", "MEMBER"]) def create(self, request, slug, project_id, cycle_id): issues = request.data.get("issues", []) @@ -333,6 +323,7 @@ def create(self, request, slug, project_id, cycle_id): ) return Response({"message": "success"}, status=status.HTTP_201_CREATED) + @allow_permission(["ADMIN", "MEMBER"]) def destroy(self, request, slug, project_id, cycle_id, issue_id): cycle_issue = CycleIssue.objects.filter( issue_id=issue_id, diff --git a/apiserver/plane/app/views/estimate/base.py b/apiserver/plane/app/views/estimate/base.py index d70d4b8693d..0918b3547c1 100644 --- a/apiserver/plane/app/views/estimate/base.py +++ b/apiserver/plane/app/views/estimate/base.py @@ -7,7 +7,7 @@ # Module imports from ..base import BaseViewSet, BaseAPIView -from plane.app.permissions import ProjectEntityPermission +from plane.app.permissions import ProjectEntityPermission, allow_permission from plane.db.models import Project, Estimate, EstimatePoint, Issue from plane.app.serializers import ( EstimateSerializer, @@ -23,10 +23,8 @@ def generate_random_name(length=10): class ProjectEstimatePointEndpoint(BaseAPIView): - permission_classes = [ - ProjectEntityPermission, - ] + @allow_permission(["ADMIN", "MEMBER", "VIEWER"]) def get(self, request, slug, project_id): project = Project.objects.get(workspace__slug=slug, pk=project_id) if project.estimate_id is not None: @@ -189,10 +187,8 @@ def destroy(self, request, slug, project_id, estimate_id): class EstimatePointEndpoint(BaseViewSet): - permission_classes = [ - ProjectEntityPermission, - ] + @allow_permission(["ADMIN", "MEMBER"]) def create(self, request, slug, project_id, estimate_id): # TODO: add a key validation if the same key already exists if not request.data.get("key") or not request.data.get("value"): @@ -211,6 +207,7 @@ def create(self, request, slug, project_id, estimate_id): serializer = EstimatePointSerializer(estimate_point).data return Response(serializer, status=status.HTTP_200_OK) + @allow_permission(["ADMIN", "MEMBER"]) def partial_update( self, request, slug, project_id, estimate_id, estimate_point_id ): @@ -231,6 +228,7 @@ def partial_update( serializer.save() return Response(serializer.data, status=status.HTTP_200_OK) + @allow_permission(["ADMIN", "MEMBER"]) def destroy( self, request, slug, project_id, estimate_id, estimate_point_id ): diff --git a/apiserver/plane/app/views/exporter/base.py b/apiserver/plane/app/views/exporter/base.py index 5649581dc8c..9bb6f13f8ff 100644 --- a/apiserver/plane/app/views/exporter/base.py +++ b/apiserver/plane/app/views/exporter/base.py @@ -2,7 +2,7 @@ from rest_framework import status from rest_framework.response import Response -from plane.app.permissions import WorkSpaceAdminPermission +from plane.app.permissions import allow_permission from plane.app.serializers import ExporterHistorySerializer from plane.bgtasks.export_task import issue_export_task from plane.db.models import ExporterHistory, Project, Workspace @@ -12,12 +12,10 @@ class ExportIssuesEndpoint(BaseAPIView): - permission_classes = [ - WorkSpaceAdminPermission, - ] model = ExporterHistory serializer_class = ExporterHistorySerializer + @allow_permission(roles=["ADMIN"], level="WORKSPACE") def post(self, request, slug): # Get the workspace workspace = Workspace.objects.get(slug=slug) @@ -64,6 +62,7 @@ def post(self, request, slug): status=status.HTTP_400_BAD_REQUEST, ) + @allow_permission(roles=["ADMIN"], level="WORKSPACE") def get(self, request, slug): exporter_history = ExporterHistory.objects.filter( workspace__slug=slug, diff --git a/apiserver/plane/app/views/external/base.py b/apiserver/plane/app/views/external/base.py index d9a66b85068..bfc10352a8a 100644 --- a/apiserver/plane/app/views/external/base.py +++ b/apiserver/plane/app/views/external/base.py @@ -11,7 +11,9 @@ # Module imports from ..base import BaseAPIView -from plane.app.permissions import ProjectEntityPermission, WorkspaceEntityPermission +from plane.app.permissions import ( + allow_permission, +) from plane.db.models import Workspace, Project from plane.app.serializers import ( ProjectLiteSerializer, @@ -21,10 +23,8 @@ class GPTIntegrationEndpoint(BaseAPIView): - permission_classes = [ - ProjectEntityPermission, - ] + @allow_permission(["ADMIN", "MEMBER"]) def post(self, request, slug, project_id): OPENAI_API_KEY, GPT_ENGINE = get_configuration_value( [ @@ -84,10 +84,8 @@ def post(self, request, slug, project_id): class WorkspaceGPTIntegrationEndpoint(BaseAPIView): - permission_classes = [ - WorkspaceEntityPermission, - ] + @allow_permission(roles=["ADMIN", "MEMBER"], level="WORKSPACE") def post(self, request, slug): OPENAI_API_KEY, GPT_ENGINE = get_configuration_value( [ diff --git a/apiserver/plane/app/views/inbox/base.py b/apiserver/plane/app/views/inbox/base.py index 7a1d77d0aea..d9d3fd00150 100644 --- a/apiserver/plane/app/views/inbox/base.py +++ b/apiserver/plane/app/views/inbox/base.py @@ -16,7 +16,9 @@ # Module imports from ..base import BaseViewSet -from plane.app.permissions import ProjectBasePermission, ProjectLitePermission +from plane.app.permissions import ( + allow_permission, +) from plane.db.models import ( Inbox, InboxIssue, @@ -39,9 +41,6 @@ class InboxViewSet(BaseViewSet): - permission_classes = [ - ProjectBasePermission, - ] serializer_class = InboxSerializer model = Inbox @@ -63,6 +62,7 @@ def get_queryset(self): .select_related("workspace", "project") ) + @allow_permission(["ADMIN", "MEMBER"]) def list(self, request, slug, project_id): inbox = self.get_queryset().first() return Response( @@ -70,9 +70,11 @@ def list(self, request, slug, project_id): status=status.HTTP_200_OK, ) + @allow_permission(["ADMIN", "MEMBER"]) def perform_create(self, serializer): serializer.save(project_id=self.kwargs.get("project_id")) + @allow_permission(["ADMIN", "MEMBER"]) def destroy(self, request, slug, project_id, pk): inbox = Inbox.objects.filter( workspace__slug=slug, project_id=project_id, pk=pk @@ -88,9 +90,6 @@ def destroy(self, request, slug, project_id, pk): class InboxIssueViewSet(BaseViewSet): - permission_classes = [ - ProjectLitePermission, - ] serializer_class = InboxIssueSerializer model = InboxIssue @@ -168,6 +167,7 @@ def get_queryset(self): ) ).distinct() + @allow_permission(["ADMIN", "MEMBER", "VIEWER", "GUEST"]) def list(self, request, slug, project_id): inbox_id = Inbox.objects.filter( workspace__slug=slug, project_id=project_id @@ -201,6 +201,14 @@ def list(self, request, slug, project_id): if inbox_status: inbox_issue = inbox_issue.filter(status__in=inbox_status) + if ProjectMember.objects.filter( + workspace__slug=slug, + project_id=project_id, + member=request.user, + role=5, + is_active=True, + ).exists(): + inbox_issue = inbox_issue.filter(created_by=request.user) return self.paginate( request=request, queryset=(inbox_issue), @@ -210,6 +218,7 @@ def list(self, request, slug, project_id): ).data, ) + @allow_permission(["ADMIN", "MEMBER", "GUEST"]) def create(self, request, slug, project_id): if not request.data.get("issue", {}).get("name", False): return Response( @@ -312,12 +321,13 @@ def create(self, request, slug, project_id): serializer.errors, status=status.HTTP_400_BAD_REQUEST ) - def partial_update(self, request, slug, project_id, issue_id): + @allow_permission(["ADMIN", "MEMBER", "GUEST"]) + def partial_update(self, request, slug, project_id, pk): inbox_id = Inbox.objects.filter( workspace__slug=slug, project_id=project_id ).first() inbox_issue = InboxIssue.objects.get( - issue_id=issue_id, + issue_id=pk, workspace__slug=slug, project_id=project_id, inbox_id=inbox_id, @@ -458,7 +468,7 @@ def partial_update(self, request, slug, project_id, issue_id): request.data, cls=DjangoJSONEncoder ), actor_id=str(request.user.id), - issue_id=str(issue_id), + issue_id=str(pk), project_id=str(project_id), current_instance=current_instance, epoch=int(timezone.now().timestamp()), @@ -493,7 +503,7 @@ def partial_update(self, request, slug, project_id, issue_id): ) .get( inbox_id=inbox_id.id, - issue_id=issue_id, + issue_id=pk, project_id=project_id, ) ) @@ -506,7 +516,8 @@ def partial_update(self, request, slug, project_id, issue_id): serializer = InboxIssueDetailSerializer(inbox_issue).data return Response(serializer, status=status.HTTP_200_OK) - def retrieve(self, request, slug, project_id, issue_id): + @allow_permission(roles=["ADMIN", "MEMBER", "VIEWER"], creator=True, model=Issue) + def retrieve(self, request, slug, project_id, pk): inbox_id = Inbox.objects.filter( workspace__slug=slug, project_id=project_id ).first() @@ -535,7 +546,7 @@ def retrieve(self, request, slug, project_id, issue_id): ), ) .get( - inbox_id=inbox_id.id, issue_id=issue_id, project_id=project_id + inbox_id=inbox_id.id, issue_id=pk, project_id=project_id ) ) issue = InboxIssueDetailSerializer(inbox_issue).data @@ -544,12 +555,13 @@ def retrieve(self, request, slug, project_id, issue_id): status=status.HTTP_200_OK, ) - def destroy(self, request, slug, project_id, issue_id): + @allow_permission(roles=["ADMIN"], creator=True, model=Issue) + def destroy(self, request, slug, project_id, pk): inbox_id = Inbox.objects.filter( workspace__slug=slug, project_id=project_id ).first() inbox_issue = InboxIssue.objects.get( - issue_id=issue_id, + issue_id=pk, workspace__slug=slug, project_id=project_id, inbox_id=inbox_id, @@ -559,21 +571,8 @@ def destroy(self, request, slug, project_id, issue_id): if inbox_issue.status in [-2, -1, 0, 2]: # Delete the issue also issue = Issue.objects.filter( - workspace__slug=slug, project_id=project_id, pk=issue_id + workspace__slug=slug, project_id=project_id, pk=pk ).first() - if issue.created_by_id != request.user.id and ( - not ProjectMember.objects.filter( - workspace__slug=slug, - member=request.user, - role=20, - project_id=project_id, - is_active=True, - ).exists() - ): - return Response( - {"error": "Only admin or creator can delete the issue"}, - status=status.HTTP_403_FORBIDDEN, - ) issue.delete() inbox_issue.delete() diff --git a/apiserver/plane/app/views/issue/activity.py b/apiserver/plane/app/views/issue/activity.py index 6815b254ed7..fad879636a5 100644 --- a/apiserver/plane/app/views/issue/activity.py +++ b/apiserver/plane/app/views/issue/activity.py @@ -19,7 +19,7 @@ IssueActivitySerializer, IssueCommentSerializer, ) -from plane.app.permissions import ProjectEntityPermission +from plane.app.permissions import ProjectEntityPermission, allow_permission from plane.db.models import ( IssueActivity, IssueComment, @@ -33,6 +33,7 @@ class IssueActivityEndpoint(BaseAPIView): ] @method_decorator(gzip_page) + @allow_permission(["ADMIN", "MEMBER", "GUEST", "VIEWER"]) def get(self, request, slug, project_id, issue_id): filters = {} if request.GET.get("created_at__gt", None) is not None: diff --git a/apiserver/plane/app/views/issue/archive.py b/apiserver/plane/app/views/issue/archive.py index 264e9cc3712..aefafbc13d0 100644 --- a/apiserver/plane/app/views/issue/archive.py +++ b/apiserver/plane/app/views/issue/archive.py @@ -46,15 +46,13 @@ GroupedOffsetPaginator, SubGroupedOffsetPaginator, ) +from plane.app.permissions import allow_permission # Module imports from .. import BaseViewSet, BaseAPIView class IssueArchiveViewSet(BaseViewSet): - permission_classes = [ - ProjectEntityPermission, - ] serializer_class = IssueFlatSerializer model = Issue @@ -98,6 +96,7 @@ def get_queryset(self): ) @method_decorator(gzip_page) + @allow_permission(["ADMIN", "MEMBER", "VIEWER"]) def list(self, request, slug, project_id): filters = issue_filters(request.query_params, "GET") show_sub_issues = request.GET.get("show_sub_issues", "true") @@ -213,6 +212,7 @@ def list(self, request, slug, project_id): ), ) + @allow_permission(["ADMIN", "MEMBER", "VIEWER"]) def retrieve(self, request, slug, project_id, pk=None): issue = ( self.get_queryset() @@ -256,6 +256,7 @@ def retrieve(self, request, slug, project_id, pk=None): serializer = IssueDetailSerializer(issue, expand=self.expand) return Response(serializer.data, status=status.HTTP_200_OK) + @allow_permission(["ADMIN", "MEMBER"]) def archive(self, request, slug, project_id, pk=None): issue = Issue.issue_objects.get( workspace__slug=slug, @@ -294,6 +295,7 @@ def archive(self, request, slug, project_id, pk=None): {"archived_at": str(issue.archived_at)}, status=status.HTTP_200_OK ) + @allow_permission(["ADMIN", "MEMBER"]) def unarchive(self, request, slug, project_id, pk=None): issue = Issue.objects.get( workspace__slug=slug, @@ -325,6 +327,7 @@ class BulkArchiveIssuesEndpoint(BaseAPIView): ProjectEntityPermission, ] + @allow_permission(["ADMIN", "MEMBER"]) def post(self, request, slug, project_id): issue_ids = request.data.get("issue_ids", []) diff --git a/apiserver/plane/app/views/issue/attachment.py b/apiserver/plane/app/views/issue/attachment.py index c084d58ffea..382ef8ee00a 100644 --- a/apiserver/plane/app/views/issue/attachment.py +++ b/apiserver/plane/app/views/issue/attachment.py @@ -13,19 +13,16 @@ # Module imports from .. import BaseAPIView from plane.app.serializers import IssueAttachmentSerializer -from plane.app.permissions import ProjectEntityPermission -from plane.db.models import IssueAttachment, ProjectMember +from plane.db.models import IssueAttachment from plane.bgtasks.issue_activites_task import issue_activity - +from plane.app.permissions import allow_permission class IssueAttachmentEndpoint(BaseAPIView): serializer_class = IssueAttachmentSerializer - permission_classes = [ - ProjectEntityPermission, - ] model = IssueAttachment parser_classes = (MultiPartParser, FormParser) + @allow_permission(["ADMIN", "MEMBER", "GUEST"]) def post(self, request, slug, project_id, issue_id): serializer = IssueAttachmentSerializer(data=request.data) if serializer.is_valid(): @@ -47,21 +44,9 @@ def post(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(["ADMIN"], creator=True, model=IssueAttachment) def delete(self, request, slug, project_id, issue_id, pk): issue_attachment = IssueAttachment.objects.get(pk=pk) - if issue_attachment.created_by_id != request.user.id and ( - not ProjectMember.objects.filter( - workspace__slug=slug, - member=request.user, - role=20, - project_id=project_id, - is_active=True, - ).exists() - ): - return Response( - {"error": "Only admin or creator can delete the attachment"}, - status=status.HTTP_403_FORBIDDEN, - ) issue_attachment.asset.delete(save=False) issue_attachment.delete() issue_activity.delay( @@ -78,6 +63,7 @@ def delete(self, request, slug, project_id, issue_id, pk): return Response(status=status.HTTP_204_NO_CONTENT) + @allow_permission(["ADMIN", "MEMBER", "GUEST", "VIEWER"]) def get(self, request, slug, project_id, issue_id): issue_attachments = IssueAttachment.objects.filter( issue_id=issue_id, workspace__slug=slug, project_id=project_id diff --git a/apiserver/plane/app/views/issue/base.py b/apiserver/plane/app/views/issue/base.py index 49c7b5b1e0d..e62ad6e39d1 100644 --- a/apiserver/plane/app/views/issue/base.py +++ b/apiserver/plane/app/views/issue/base.py @@ -25,10 +25,7 @@ from rest_framework.response import Response # Module imports -from plane.app.permissions import ( - ProjectEntityPermission, - ProjectLitePermission, -) +from plane.app.permissions import allow_permission from plane.app.serializers import ( IssueCreateSerializer, IssueDetailSerializer, @@ -60,14 +57,10 @@ from .. import BaseAPIView, BaseViewSet from plane.utils.user_timezone_converter import user_timezone_converter -# Module imports - class IssueListEndpoint(BaseAPIView): - permission_classes = [ - ProjectEntityPermission, - ] + @allow_permission(["ADMIN", "MEMBER", "VIEWER", "GUEST"]) def get(self, request, slug, project_id): issue_ids = request.GET.get("issues", False) @@ -184,9 +177,6 @@ def get_serializer_class(self): model = Issue webhook_event = "issue" - permission_classes = [ - ProjectEntityPermission, - ] search_fields = [ "name", @@ -232,6 +222,7 @@ def get_queryset(self): ).distinct() @method_decorator(gzip_page) + @allow_permission(["ADMIN", "MEMBER", "VIEWER", "GUEST"]) def list(self, request, slug, project_id): filters = issue_filters(request.query_params, "GET") order_by_param = request.GET.get("order_by", "-created_at") @@ -256,6 +247,15 @@ def list(self, request, slug, project_id): sub_group_by=sub_group_by, ) + if ProjectMember.objects.filter( + workspace__slug=slug, + project_id=project_id, + member=request.user, + role=5, + is_active=True, + ).exists(): + issue_queryset = issue_queryset.filter(created_by=request.user) + if group_by: if sub_group_by: if group_by == sub_group_by: @@ -337,6 +337,7 @@ def list(self, request, slug, project_id): ), ) + @allow_permission(["ADMIN", "MEMBER"]) def create(self, request, slug, project_id): project = Project.objects.get(pk=project_id) @@ -411,6 +412,7 @@ def create(self, request, slug, project_id): return Response(issue, status=status.HTTP_201_CREATED) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + @allow_permission(["ADMIN", "MEMBER", "VIEWER"], creator=True, model=Issue) def retrieve(self, request, slug, project_id, pk=None): issue = ( self.get_queryset() @@ -483,6 +485,7 @@ def retrieve(self, request, slug, project_id, pk=None): serializer = IssueDetailSerializer(issue, expand=self.expand) return Response(serializer.data, status=status.HTTP_200_OK) + @allow_permission(["ADMIN", "MEMBER", "GUEST"]) def partial_update(self, request, slug, project_id, pk=None): issue = ( self.get_queryset() @@ -548,23 +551,11 @@ def partial_update(self, request, slug, project_id, pk=None): return Response(status=status.HTTP_204_NO_CONTENT) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + @allow_permission(["ADMIN"], creator=True, model=Issue) def destroy(self, request, slug, project_id, pk=None): issue = Issue.objects.get( workspace__slug=slug, project_id=project_id, pk=pk ) - if issue.created_by_id != request.user.id and ( - not ProjectMember.objects.filter( - workspace__slug=slug, - member=request.user, - role=20, - project_id=project_id, - is_active=True, - ).exists() - ): - return Response( - {"error": "Only admin or creator can delete the issue"}, - status=status.HTTP_403_FORBIDDEN, - ) issue.delete() issue_activity.delay( @@ -582,10 +573,8 @@ def destroy(self, request, slug, project_id, pk=None): class IssueUserDisplayPropertyEndpoint(BaseAPIView): - permission_classes = [ - ProjectLitePermission, - ] + @allow_permission(["ADMIN", "MEMBER", "GUEST", "VIEWER"]) def patch(self, request, slug, project_id): issue_property = IssueUserProperty.objects.get( user=request.user, @@ -605,6 +594,7 @@ def patch(self, request, slug, project_id): serializer = IssueUserPropertySerializer(issue_property) return Response(serializer.data, status=status.HTTP_201_CREATED) + @allow_permission(["ADMIN", "MEMBER", "GUEST", "VIEWER"]) def get(self, request, slug, project_id): issue_property, _ = IssueUserProperty.objects.get_or_create( user=request.user, project_id=project_id @@ -614,22 +604,9 @@ def get(self, request, slug, project_id): class BulkDeleteIssuesEndpoint(BaseAPIView): - permission_classes = [ - ProjectEntityPermission, - ] + @allow_permission(["ADMIN"]) def delete(self, request, slug, project_id): - if ProjectMember.objects.filter( - workspace__slug=slug, - member=request.user, - role__in=[15, 10, 5], - project_id=project_id, - is_active=True, - ).exists(): - return Response( - {"error": "Only admin can perform this action"}, - status=status.HTTP_403_FORBIDDEN, - ) issue_ids = request.data.get("issue_ids", []) diff --git a/apiserver/plane/app/views/issue/comment.py b/apiserver/plane/app/views/issue/comment.py index 1698efef83f..2fa7519785f 100644 --- a/apiserver/plane/app/views/issue/comment.py +++ b/apiserver/plane/app/views/issue/comment.py @@ -16,7 +16,7 @@ IssueCommentSerializer, CommentReactionSerializer, ) -from plane.app.permissions import ProjectLitePermission +from plane.app.permissions import ProjectLitePermission, allow_permission from plane.db.models import ( IssueComment, ProjectMember, @@ -29,9 +29,6 @@ class IssueCommentViewSet(BaseViewSet): serializer_class = IssueCommentSerializer model = IssueComment webhook_event = "issue_comment" - permission_classes = [ - ProjectLitePermission, - ] filterset_fields = [ "issue__id", @@ -66,6 +63,7 @@ def get_queryset(self): .distinct() ) + @allow_permission(["ADMIN", "MEMBER", "GUEST", "VIEWER"]) def create(self, request, slug, project_id, issue_id): serializer = IssueCommentSerializer(data=request.data) if serializer.is_valid(): @@ -90,6 +88,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(roles=["ADMIN", "MEMBER"], creator=True, model=IssueComment) def partial_update(self, request, slug, project_id, issue_id, pk): issue_comment = IssueComment.objects.get( workspace__slug=slug, @@ -121,6 +120,7 @@ def partial_update(self, request, slug, project_id, issue_id, pk): return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + @allow_permission(roles=["ADMIN"], creator=True, model=IssueComment) def destroy(self, request, slug, project_id, issue_id, pk): issue_comment = IssueComment.objects.get( workspace__slug=slug, diff --git a/apiserver/plane/app/views/issue/label.py b/apiserver/plane/app/views/issue/label.py index c5dc35809e9..6fbf71cc67f 100644 --- a/apiserver/plane/app/views/issue/label.py +++ b/apiserver/plane/app/views/issue/label.py @@ -11,9 +11,7 @@ # Module imports from .. import BaseViewSet, BaseAPIView from plane.app.serializers import LabelSerializer -from plane.app.permissions import ( - ProjectMemberPermission, -) +from plane.app.permissions import allow_permission, ProjectBasePermission from plane.db.models import ( Project, Label, @@ -25,7 +23,7 @@ class LabelViewSet(BaseViewSet): serializer_class = LabelSerializer model = Label permission_classes = [ - ProjectMemberPermission, + ProjectBasePermission, ] def get_queryset(self): @@ -45,6 +43,7 @@ def get_queryset(self): @invalidate_cache( path="/api/workspaces/:slug/labels/", url_params=True, user=False ) + @allow_permission(["ADMIN", "MEMBER"]) def create(self, request, slug, project_id): try: serializer = LabelSerializer(data=request.data) @@ -67,17 +66,20 @@ def create(self, request, slug, project_id): @invalidate_cache( path="/api/workspaces/:slug/labels/", url_params=True, user=False ) + @allow_permission(["ADMIN", "MEMBER"]) def partial_update(self, request, *args, **kwargs): return super().partial_update(request, *args, **kwargs) @invalidate_cache( path="/api/workspaces/:slug/labels/", url_params=True, user=False ) + @allow_permission(["ADMIN", "MEMBER"]) def destroy(self, request, *args, **kwargs): return super().destroy(request, *args, **kwargs) class BulkCreateIssueLabelsEndpoint(BaseAPIView): + @allow_permission(["ADMIN"]) def post(self, request, slug, project_id): label_data = request.data.get("label_data", []) project = Project.objects.get(pk=project_id) diff --git a/apiserver/plane/app/views/module/base.py b/apiserver/plane/app/views/module/base.py index 1cd67d18c18..e830c05a4e5 100644 --- a/apiserver/plane/app/views/module/base.py +++ b/apiserver/plane/app/views/module/base.py @@ -30,8 +30,9 @@ # Module imports from plane.app.permissions import ( ProjectEntityPermission, - ProjectLitePermission, + allow_permission, ) + from plane.app.serializers import ( ModuleDetailSerializer, ModuleLinkSerializer, @@ -58,9 +59,9 @@ class ModuleViewSet(BaseViewSet): model = Module - permission_classes = [ - ProjectEntityPermission, - ] + # permission_classes = [ + # ProjectEntityPermission, + # ] webhook_event = "module" def get_serializer_class(self): @@ -318,6 +319,7 @@ def get_queryset(self): .order_by("-is_favorite", "-created_at") ) + allow_permission(["ADMIN", "MEMBER", "VIEWER"]) def create(self, request, slug, project_id): project = Project.objects.get(workspace__slug=slug, pk=project_id) serializer = ModuleWriteSerializer( @@ -380,6 +382,8 @@ def create(self, request, slug, project_id): return Response(module, status=status.HTTP_201_CREATED) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + allow_permission(["ADMIN", "MEMBER", "VIEWER", "GUEST"]) + def list(self, request, slug, project_id): queryset = self.get_queryset().filter(archived_at__isnull=True) if self.fields: @@ -427,6 +431,8 @@ def list(self, request, slug, project_id): ) return Response(modules, status=status.HTTP_200_OK) + allow_permission(["ADMIN", "MEMBER", "VIEWER"]) + def retrieve(self, request, slug, project_id, pk): queryset = ( self.get_queryset() @@ -671,6 +677,7 @@ def retrieve(self, request, slug, project_id, pk): status=status.HTTP_200_OK, ) + @allow_permission(["ADMIN", "MEMBER"]) def partial_update(self, request, slug, project_id, pk): module = self.get_queryset().filter(pk=pk) @@ -740,6 +747,7 @@ def partial_update(self, request, slug, project_id, pk): return Response(module, status=status.HTTP_200_OK) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + @allow_permission(["ADMIN", "MEMBER", "VIEWER", "GUEST"]) def destroy(self, request, slug, project_id, pk): module = Module.objects.get( workspace__slug=slug, project_id=project_id, pk=pk @@ -859,10 +867,8 @@ def destroy(self, request, slug, project_id, module_id): class ModuleUserPropertiesEndpoint(BaseAPIView): - permission_classes = [ - ProjectLitePermission, - ] + @allow_permission(["ADMIN", "MEMBER", "VIEWER", "GUEST"]) def patch(self, request, slug, project_id, module_id): module_properties = ModuleUserProperties.objects.get( user=request.user, @@ -885,6 +891,7 @@ def patch(self, request, slug, project_id, module_id): serializer = ModuleUserPropertiesSerializer(module_properties) return Response(serializer.data, status=status.HTTP_201_CREATED) + @allow_permission(["ADMIN", "MEMBER", "VIEWER", "GUEST"]) def get(self, request, slug, project_id, module_id): module_properties, _ = ModuleUserProperties.objects.get_or_create( user=request.user, diff --git a/apiserver/plane/app/views/module/issue.py b/apiserver/plane/app/views/module/issue.py index 689d394927f..d5617854a6b 100644 --- a/apiserver/plane/app/views/module/issue.py +++ b/apiserver/plane/app/views/module/issue.py @@ -18,7 +18,7 @@ from rest_framework.response import Response from plane.app.permissions import ( - ProjectEntityPermission, + allow_permission, ) from plane.app.serializers import ( ModuleIssueSerializer, @@ -57,10 +57,6 @@ class ModuleIssueViewSet(BaseViewSet): "issue__assignees__id", ] - permission_classes = [ - ProjectEntityPermission, - ] - def get_queryset(self): return ( Issue.issue_objects.filter( @@ -96,6 +92,7 @@ def get_queryset(self): ).distinct() @method_decorator(gzip_page) + @allow_permission(["ADMIN", "MEMBER", "VIEWER"]) def list(self, request, slug, project_id, module_id): filters = issue_filters(request.query_params, "GET") issue_queryset = self.get_queryset().filter(**filters) @@ -203,6 +200,7 @@ def list(self, request, slug, project_id, module_id): ), ) + @allow_permission(["ADMIN", "MEMBER"]) # create multiple issues inside a module def create_module_issues(self, request, slug, project_id, module_id): issues = request.data.get("issues", []) @@ -244,6 +242,7 @@ def create_module_issues(self, request, slug, project_id, module_id): ] return Response({"message": "success"}, status=status.HTTP_201_CREATED) + @allow_permission(["ADMIN", "MEMBER"]) # add multiple module inside an issue and remove multiple modules from an issue def create_issue_modules(self, request, slug, project_id, issue_id): modules = request.data.get("modules", []) @@ -306,6 +305,7 @@ def create_issue_modules(self, request, slug, project_id, issue_id): return Response({"message": "success"}, status=status.HTTP_201_CREATED) + @allow_permission(["ADMIN", "MEMBER"]) def destroy(self, request, slug, project_id, module_id, issue_id): module_issue = ModuleIssue.objects.filter( workspace__slug=slug, diff --git a/apiserver/plane/app/views/notification/base.py b/apiserver/plane/app/views/notification/base.py index 6cae9d02a72..543d830eddd 100644 --- a/apiserver/plane/app/views/notification/base.py +++ b/apiserver/plane/app/views/notification/base.py @@ -19,6 +19,7 @@ WorkspaceMember, ) from plane.utils.paginator import BasePaginator +from plane.app.permissions import allow_permission # Module imports from ..base import BaseAPIView, BaseViewSet @@ -39,6 +40,7 @@ def get_queryset(self): .select_related("workspace", "project," "triggered_by", "receiver") ) + @allow_permission(roles=["ADMIN", "MEMBER", "GUEST"], level="WORKSPACE") def list(self, request, slug): # Get query parameters snoozed = request.GET.get("snoozed", "false") @@ -168,6 +170,7 @@ def list(self, request, slug): serializer = NotificationSerializer(notifications, many=True) return Response(serializer.data, status=status.HTTP_200_OK) + @allow_permission(roles=["ADMIN", "MEMBER", "GUEST"], level="WORKSPACE") def partial_update(self, request, slug, pk): notification = Notification.objects.get( workspace__slug=slug, pk=pk, receiver=request.user @@ -185,6 +188,7 @@ def partial_update(self, request, slug, pk): return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + @allow_permission(roles=["ADMIN", "MEMBER"], level="WORKSPACE") def mark_read(self, request, slug, pk): notification = Notification.objects.get( receiver=request.user, workspace__slug=slug, pk=pk @@ -194,6 +198,7 @@ def mark_read(self, request, slug, pk): serializer = NotificationSerializer(notification) return Response(serializer.data, status=status.HTTP_200_OK) + @allow_permission(roles=["ADMIN", "MEMBER"], level="WORKSPACE") def mark_unread(self, request, slug, pk): notification = Notification.objects.get( receiver=request.user, workspace__slug=slug, pk=pk @@ -203,6 +208,7 @@ def mark_unread(self, request, slug, pk): serializer = NotificationSerializer(notification) return Response(serializer.data, status=status.HTTP_200_OK) + @allow_permission(roles=["ADMIN", "MEMBER"], level="WORKSPACE") def archive(self, request, slug, pk): notification = Notification.objects.get( receiver=request.user, workspace__slug=slug, pk=pk @@ -212,6 +218,7 @@ def archive(self, request, slug, pk): serializer = NotificationSerializer(notification) return Response(serializer.data, status=status.HTTP_200_OK) + @allow_permission(roles=["ADMIN", "MEMBER"], level="WORKSPACE") def unarchive(self, request, slug, pk): notification = Notification.objects.get( receiver=request.user, workspace__slug=slug, pk=pk @@ -223,6 +230,8 @@ def unarchive(self, request, slug, pk): class UnreadNotificationEndpoint(BaseAPIView): + + @allow_permission(roles=["ADMIN", "MEMBER", "GUEST"], level="WORKSPACE") def get(self, request, slug): # Watching Issues Count unread_notifications_count = ( @@ -260,6 +269,8 @@ def get(self, request, slug): class MarkAllReadNotificationViewSet(BaseViewSet): + + @allow_permission(roles=["ADMIN", "MEMBER"], level="WORKSPACE") def create(self, request, slug): snoozed = request.data.get("snoozed", False) archived = request.data.get("archived", False) @@ -343,6 +354,7 @@ class UserNotificationPreferenceEndpoint(BaseAPIView): serializer_class = UserNotificationPreferenceSerializer # request the object + @allow_permission(roles=["ADMIN", "MEMBER", "GUEST"], level="WORKSPACE") def get(self, request): user_notification_preference = UserNotificationPreference.objects.get( user=request.user @@ -353,6 +365,7 @@ def get(self, request): return Response(serializer.data, status=status.HTTP_200_OK) # update the object + @allow_permission(roles=["ADMIN", "MEMBER", "GUEST"], level="WORKSPACE") def patch(self, request): user_notification_preference = UserNotificationPreference.objects.get( user=request.user diff --git a/apiserver/plane/app/views/project/base.py b/apiserver/plane/app/views/project/base.py index 22181dacff9..cccea8108a8 100644 --- a/apiserver/plane/app/views/project/base.py +++ b/apiserver/plane/app/views/project/base.py @@ -31,8 +31,8 @@ ) from plane.app.permissions import ( - ProjectBasePermission, ProjectMemberPermission, + allow_permission, ) from plane.db.models import ( UserFavorite, @@ -47,6 +47,7 @@ ProjectMember, State, Workspace, + WorkspaceMember, ) from plane.utils.cache import cache_response from plane.bgtasks.webhook_task import model_activity @@ -57,10 +58,6 @@ class ProjectViewSet(BaseViewSet): model = Project webhook_event = "project" - permission_classes = [ - ProjectBasePermission, - ] - def get_queryset(self): sort_order = ProjectMember.objects.filter( member=self.request.user, @@ -155,6 +152,9 @@ def get_queryset(self): .distinct() ) + @allow_permission( + roles=["ADMIN", "MEMBER", "VIEWER", "GUEST"], level="WORKSPACE" + ) def list(self, request, slug): fields = [ field @@ -173,11 +173,26 @@ def list(self, request, slug): projects, many=True ).data, ) + + if WorkspaceMember.objects.filter( + member=request.user, + workspace__slug=slug, + is_active=True, + role__in=[5, 10], + ).exists(): + projects = projects.filter( + project_projectmember__member=self.request.user, + project_projectmember__is_active=True, + ) + projects = ProjectListSerializer( projects, many=True, fields=fields if fields else None ).data return Response(projects, status=status.HTTP_200_OK) + @allow_permission( + roles=["ADMIN", "MEMBER", "VIEWER", "GUEST"], level="WORKSPACE" + ) def retrieve(self, request, slug, pk): project = ( self.get_queryset() @@ -249,6 +264,7 @@ def retrieve(self, request, slug, pk): serializer = ProjectListSerializer(project) return Response(serializer.data, status=status.HTTP_200_OK) + @allow_permission(["ADMIN", "MEMBER"], level="WORKSPACE") def create(self, request, slug): try: workspace = Workspace.objects.get(slug=slug) @@ -378,6 +394,7 @@ def create(self, request, slug): status=status.HTTP_410_GONE, ) + @allow_permission(["ADMIN", "MEMBER"], level="WORKSPACE") def partial_update(self, request, slug, pk=None): try: workspace = Workspace.objects.get(slug=slug) @@ -459,10 +476,7 @@ def partial_update(self, request, slug, pk=None): class ProjectArchiveUnarchiveEndpoint(BaseAPIView): - permission_classes = [ - ProjectBasePermission, - ] - + @allow_permission(["ADMIN", "MEMBER"]) def post(self, request, slug, project_id): project = Project.objects.get(pk=project_id, workspace__slug=slug) project.archived_at = timezone.now() @@ -472,6 +486,7 @@ def post(self, request, slug, project_id): status=status.HTTP_200_OK, ) + @allow_permission(["ADMIN", "MEMBER"]) def delete(self, request, slug, project_id): project = Project.objects.get(pk=project_id, workspace__slug=slug) project.archived_at = None @@ -480,10 +495,7 @@ def delete(self, request, slug, project_id): class ProjectIdentifierEndpoint(BaseAPIView): - permission_classes = [ - ProjectBasePermission, - ] - + @allow_permission(["ADMIN", "MEMBER"], level="WORKSPACE") def get(self, request, slug): name = request.GET.get("name", "").strip().upper() @@ -502,6 +514,7 @@ def get(self, request, slug): status=status.HTTP_200_OK, ) + @allow_permission(["ADMIN", "MEMBER"], level="WORKSPACE") def delete(self, request, slug): name = request.data.get("name", "").strip().upper() diff --git a/apiserver/plane/app/views/project/member.py b/apiserver/plane/app/views/project/member.py index 8c6852480b7..4af7d268919 100644 --- a/apiserver/plane/app/views/project/member.py +++ b/apiserver/plane/app/views/project/member.py @@ -26,14 +26,14 @@ ) from plane.bgtasks.project_add_user_email_task import project_add_user_email from plane.utils.host import base_host - +from plane.app.permissions.base import allow_permission class ProjectMemberViewSet(BaseViewSet): serializer_class = ProjectMemberAdminSerializer model = ProjectMember - permission_classes = [ - ProjectMemberPermission, - ] + # permission_classes = [ + # ProjectMemberPermission, + # ] def get_permissions(self): if self.action == "leave": @@ -65,6 +65,7 @@ def get_queryset(self): .select_related("workspace", "workspace__owner") ) + @allow_permission(["ADMIN", "MEMBER"]) def create(self, request, slug, project_id): # Get the list of members to be added to the project and their roles i.e. the user_id and the role members = request.data.get("members", []) @@ -172,6 +173,7 @@ def create(self, request, slug, project_id): # Return the serialized data return Response(serializer.data, status=status.HTTP_201_CREATED) + @allow_permission(["ADMIN", "MEMBER"]) def list(self, request, slug, project_id): # Get the list of project members for the project project_members = ProjectMember.objects.filter( @@ -186,6 +188,7 @@ def list(self, request, slug, project_id): ) return Response(serializer.data, status=status.HTTP_200_OK) + @allow_permission(["ADMIN", "MEMBER"]) def partial_update(self, request, slug, project_id, pk): project_member = ProjectMember.objects.get( pk=pk, @@ -226,6 +229,7 @@ def partial_update(self, request, slug, project_id, pk): return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + @allow_permission(["ADMIN", "MEMBER"]) def destroy(self, request, slug, project_id, pk): project_member = ProjectMember.objects.get( workspace__slug=slug, @@ -262,6 +266,7 @@ def destroy(self, request, slug, project_id, pk): project_member.save() return Response(status=status.HTTP_204_NO_CONTENT) + @allow_permission(["ADMIN", "MEMBER", "VIEWER", "GUEST"]) def leave(self, request, slug, project_id): project_member = ProjectMember.objects.get( workspace__slug=slug, diff --git a/apiserver/plane/app/views/search/issue.py b/apiserver/plane/app/views/search/issue.py index 6c0535af23a..3bb65418217 100644 --- a/apiserver/plane/app/views/search/issue.py +++ b/apiserver/plane/app/views/search/issue.py @@ -9,9 +9,7 @@ # Module imports from .base import BaseAPIView -from plane.db.models import ( - Issue, -) +from plane.db.models import Issue, ProjectMember from plane.utils.issue_search import search_issues @@ -75,6 +73,16 @@ def get(self, request, slug, project_id): if target_date == "none": issues = issues.filter(target_date__isnull=True) + + if ProjectMember.objects.filter( + project_id=project_id, + member=self.request.user, + is_active=True, + role=5 + ).exists(): + issues = issues.filter( + created_by=self.request.user + ) return Response( issues.values( diff --git a/apiserver/plane/app/views/view/base.py b/apiserver/plane/app/views/view/base.py index 7a913095131..dbcf69f28cc 100644 --- a/apiserver/plane/app/views/view/base.py +++ b/apiserver/plane/app/views/view/base.py @@ -77,6 +77,25 @@ def get_queryset(self): .order_by(self.request.GET.get("order_by", "-created_at")) .distinct() ) + + def list(self, request, slug): + queryset = self.get_queryset() + fields = [ + field + for field in request.GET.get("fields", "").split(",") + if field + ] + if WorkspaceMember.objects.filter( + workspace__slug=slug, + member=request.user, + role=5, + is_active=True, + ).exists(): + queryset = queryset.filter(owned_by=request.user) + views = IssueViewSerializer( + queryset, many=True, fields=fields if fields else None + ).data + return Response(views, status=status.HTTP_200_OK) def partial_update(self, request, slug, pk): with transaction.atomic(): @@ -242,6 +261,16 @@ def list(self, request, slug): .annotate(cycle_id=F("issue_cycle__cycle_id")) ) + if WorkspaceMember.objects.filter( + workspace__slug=slug, + member=request.user, + role=5, + is_active=True, + ).exists(): + issue_queryset = issue_queryset.filter( + created_by=request.user, + ) + # Issue queryset issue_queryset, order_by_param = order_issue_queryset( issue_queryset=issue_queryset, @@ -386,6 +415,14 @@ def get_queryset(self): def list(self, request, slug, project_id): queryset = self.get_queryset() + if ProjectMember.objects.filter( + workspace__slug=slug, + project_id=project_id, + member=request.user, + role=5, + is_active=True, + ).exists(): + queryset = queryset.filter(owned_by=request.user) fields = [ field for field in request.GET.get("fields", "").split(",") diff --git a/apiserver/plane/app/views/webhook/base.py b/apiserver/plane/app/views/webhook/base.py index 9586722a0cc..1d66cab9e52 100644 --- a/apiserver/plane/app/views/webhook/base.py +++ b/apiserver/plane/app/views/webhook/base.py @@ -9,15 +9,13 @@ from plane.db.models import Webhook, WebhookLog, Workspace from plane.db.models.webhook import generate_token from ..base import BaseAPIView -from plane.app.permissions import WorkspaceOwnerPermission +from plane.app.permissions import allow_permission from plane.app.serializers import WebhookSerializer, WebhookLogSerializer class WebhookEndpoint(BaseAPIView): - permission_classes = [ - WorkspaceOwnerPermission, - ] + @allow_permission(roles=["ADMIN"], level="WORKSPACE") def post(self, request, slug): workspace = Workspace.objects.get(slug=slug) try: @@ -40,6 +38,7 @@ def post(self, request, slug): ) raise IntegrityError + @allow_permission(roles=["ADMIN"], level="WORKSPACE") def get(self, request, slug, pk=None): if pk is None: webhooks = Webhook.objects.filter(workspace__slug=slug) @@ -79,6 +78,7 @@ def get(self, request, slug, pk=None): ) return Response(serializer.data, status=status.HTTP_200_OK) + @allow_permission(roles=["ADMIN"], level="WORKSPACE") def patch(self, request, slug, pk): webhook = Webhook.objects.get(workspace__slug=slug, pk=pk) serializer = WebhookSerializer( @@ -104,6 +104,7 @@ def patch(self, request, slug, pk): return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + @allow_permission(roles=["ADMIN"], level="WORKSPACE") def delete(self, request, slug, pk): webhook = Webhook.objects.get(pk=pk, workspace__slug=slug) webhook.delete() @@ -111,10 +112,8 @@ def delete(self, request, slug, pk): class WebhookSecretRegenerateEndpoint(BaseAPIView): - permission_classes = [ - WorkspaceOwnerPermission, - ] + @allow_permission(roles=["ADMIN"], level="WORKSPACE") def post(self, request, slug, pk): webhook = Webhook.objects.get(workspace__slug=slug, pk=pk) webhook.secret_key = generate_token() @@ -124,10 +123,8 @@ def post(self, request, slug, pk): class WebhookLogsEndpoint(BaseAPIView): - permission_classes = [ - WorkspaceOwnerPermission, - ] + @allow_permission(roles=["ADMIN"], level="WORKSPACE") def get(self, request, slug, webhook_id): webhook_logs = WebhookLog.objects.filter( workspace__slug=slug, webhook_id=webhook_id diff --git a/apiserver/plane/app/views/workspace/favorite.py b/apiserver/plane/app/views/workspace/favorite.py index d4fe6a622fd..5900b37a756 100644 --- a/apiserver/plane/app/views/workspace/favorite.py +++ b/apiserver/plane/app/views/workspace/favorite.py @@ -9,14 +9,12 @@ from plane.app.views.base import BaseAPIView from plane.db.models import UserFavorite, Workspace from plane.app.serializers import UserFavoriteSerializer -from plane.app.permissions import WorkspaceEntityPermission +from plane.app.permissions import allow_permission class WorkspaceFavoriteEndpoint(BaseAPIView): - permission_classes = [ - WorkspaceEntityPermission, - ] + @allow_permission(roles=["ADMIN", "MEMBER"], level="WORKSPACE") def get(self, request, slug): # the second filter is to check if the user is a member of the project favorites = UserFavorite.objects.filter( @@ -34,6 +32,7 @@ def get(self, request, slug): serializer = UserFavoriteSerializer(favorites, many=True) return Response(serializer.data, status=status.HTTP_200_OK) + @allow_permission(roles=["ADMIN", "MEMBER"], level="WORKSPACE") def post(self, request, slug): workspace = Workspace.objects.get(slug=slug) serializer = UserFavoriteSerializer(data=request.data) @@ -46,6 +45,7 @@ def post(self, request, slug): return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + @allow_permission(roles=["ADMIN", "MEMBER"], level="WORKSPACE") def patch(self, request, slug, favorite_id): favorite = UserFavorite.objects.get( user=request.user, workspace__slug=slug, pk=favorite_id @@ -58,6 +58,7 @@ def patch(self, request, slug, favorite_id): return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + @allow_permission(roles=["ADMIN", "MEMBER"], level="WORKSPACE") def delete(self, request, slug, favorite_id): favorite = UserFavorite.objects.get( user=request.user, workspace__slug=slug, pk=favorite_id @@ -67,10 +68,8 @@ def delete(self, request, slug, favorite_id): class WorkspaceFavoriteGroupEndpoint(BaseAPIView): - permission_classes = [ - WorkspaceEntityPermission, - ] + @allow_permission(roles=["ADMIN", "MEMBER"], level="WORKSPACE") def get(self, request, slug, favorite_id): favorites = UserFavorite.objects.filter( user=request.user, diff --git a/apiserver/plane/app/views/workspace/label.py b/apiserver/plane/app/views/workspace/label.py index 328f3f8c132..0ea9ea24ab6 100644 --- a/apiserver/plane/app/views/workspace/label.py +++ b/apiserver/plane/app/views/workspace/label.py @@ -13,7 +13,7 @@ class WorkspaceLabelsEndpoint(BaseAPIView): permission_classes = [ WorkspaceViewerPermission, ] - + @cache_response(60 * 60 * 2) def get(self, request, slug): labels = Label.objects.filter( From 2eeeba39783c693f73aeda94cc527b62c63309f8 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia Date: Mon, 12 Aug 2024 18:49:12 +0530 Subject: [PATCH 18/30] fix: user favorite --- web/core/hooks/use-favorite-item-details.tsx | 2 +- web/core/layouts/auth-layout/workspace-wrapper.tsx | 10 +++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/web/core/hooks/use-favorite-item-details.tsx b/web/core/hooks/use-favorite-item-details.tsx index 6e5c3b98def..4fda5ea3e48 100644 --- a/web/core/hooks/use-favorite-item-details.tsx +++ b/web/core/hooks/use-favorite-item-details.tsx @@ -6,7 +6,7 @@ import { import { useProject, usePage, useProjectView, useCycle, useModule } from "@/hooks/store"; export const useFavoriteItemDetails = (workspaceSlug: string, favorite: IFavorite) => { - const favoriteItemId = favorite.entity_data.id; + const favoriteItemId = favorite?.entity_data?.id; const favoriteItemLogoProps = favorite?.entity_data?.logo_props; const favoriteItemName = favorite?.entity_data.name || favorite?.name; const favoriteItemEntityType = favorite?.entity_type; diff --git a/web/core/layouts/auth-layout/workspace-wrapper.tsx b/web/core/layouts/auth-layout/workspace-wrapper.tsx index 37be07fcecc..361c983a7dd 100644 --- a/web/core/layouts/auth-layout/workspace-wrapper.tsx +++ b/web/core/layouts/auth-layout/workspace-wrapper.tsx @@ -30,7 +30,7 @@ export const WorkspaceAuthWrapper: FC = observer((props) // next themes const { resolvedTheme } = useTheme(); // store hooks - const { membership, signOut, data: currentUser } = useUser(); + const { membership, signOut, data: currentUser, canPerformWorkspaceMemberActions } = useUser(); const { fetchProjects } = useProject(); const { fetchFavorite } = useFavorite(); const { @@ -72,8 +72,12 @@ export const WorkspaceAuthWrapper: FC = observer((props) ); // fetch workspace favorite useSWR( - workspaceSlug && currentWorkspace ? `WORKSPACE_FAVORITE_${workspaceSlug}` : null, - workspaceSlug && currentWorkspace ? () => fetchFavorite(workspaceSlug.toString()) : null, + workspaceSlug && currentWorkspace && canPerformWorkspaceMemberActions + ? `WORKSPACE_FAVORITE_${workspaceSlug}` + : null, + workspaceSlug && currentWorkspace && canPerformWorkspaceMemberActions + ? () => fetchFavorite(workspaceSlug.toString()) + : null, { revalidateIfStale: false, revalidateOnFocus: false } ); From c03c6f01799d9234820d79d7ebc7bef6bf44197b Mon Sep 17 00:00:00 2001 From: NarayanBavisetti Date: Mon, 12 Aug 2024 20:23:34 +0530 Subject: [PATCH 19/30] chore: changed pages permission --- apiserver/plane/app/views/analytic/base.py | 2 +- apiserver/plane/app/views/cycle/base.py | 2 +- apiserver/plane/app/views/dashboard/base.py | 109 +++++++++++++++----- apiserver/plane/app/views/module/base.py | 16 +-- apiserver/plane/app/views/page/base.py | 31 +++--- apiserver/plane/app/views/project/member.py | 5 +- 6 files changed, 100 insertions(+), 65 deletions(-) diff --git a/apiserver/plane/app/views/analytic/base.py b/apiserver/plane/app/views/analytic/base.py index 3fb8410c614..d620e77110a 100644 --- a/apiserver/plane/app/views/analytic/base.py +++ b/apiserver/plane/app/views/analytic/base.py @@ -296,7 +296,7 @@ def post(self, request, slug): class DefaultAnalyticsEndpoint(BaseAPIView): - @allow_permission(["ADMIN", "MEMBER", "VIEWER"], level="WORKSPACE") + @allow_permission(["ADMIN", "MEMBER", "VIEWER", "GUEST"], level="WORKSPACE") def get(self, request, slug): filters = issue_filters(request.GET, "GET") base_issues = Issue.issue_objects.filter( diff --git a/apiserver/plane/app/views/cycle/base.py b/apiserver/plane/app/views/cycle/base.py index a41a78a3744..2b3e5bdf491 100644 --- a/apiserver/plane/app/views/cycle/base.py +++ b/apiserver/plane/app/views/cycle/base.py @@ -315,7 +315,7 @@ def get_queryset(self): .distinct() ) - @allow_permission(["ADMIN", "MEMBER", "VIEWER"]) + @allow_permission(["ADMIN", "MEMBER", "VIEWER", "GUEST"]) def list(self, request, slug, project_id): queryset = self.get_queryset().filter(archived_at__isnull=True) cycle_view = request.GET.get("cycle_view", "all") diff --git a/apiserver/plane/app/views/dashboard/base.py b/apiserver/plane/app/views/dashboard/base.py index fe9eb50ecb8..5dc1cf1b6e7 100644 --- a/apiserver/plane/app/views/dashboard/base.py +++ b/apiserver/plane/app/views/dashboard/base.py @@ -43,6 +43,7 @@ ProjectMember, User, Widget, + WorkspaceMember, ) from plane.utils.issue_filters import issue_filters @@ -51,36 +52,61 @@ def dashboard_overview_stats(self, request, slug): - assigned_issues = Issue.issue_objects.filter( - project__project_projectmember__is_active=True, - project__project_projectmember__member=request.user, + extra_filters = {} + if WorkspaceMember.objects.filter( workspace__slug=slug, - assignees__in=[request.user], - ).count() + member=request.user, + role=5, + is_active=True, + ).exists(): + extra_filters = {"created_by": request.user} - pending_issues_count = Issue.issue_objects.filter( - ~Q(state__group__in=["completed", "cancelled"]), - target_date__lt=timezone.now().date(), - project__project_projectmember__is_active=True, - project__project_projectmember__member=request.user, - workspace__slug=slug, - assignees__in=[request.user], - ).count() + assigned_issues = ( + Issue.issue_objects.filter( + project__project_projectmember__is_active=True, + project__project_projectmember__member=request.user, + workspace__slug=slug, + assignees__in=[request.user], + ) + .filter(**extra_filters) + .count() + ) - created_issues_count = Issue.issue_objects.filter( - workspace__slug=slug, - project__project_projectmember__is_active=True, - project__project_projectmember__member=request.user, - created_by_id=request.user.id, - ).count() + pending_issues_count = ( + Issue.issue_objects.filter( + ~Q(state__group__in=["completed", "cancelled"]), + target_date__lt=timezone.now().date(), + project__project_projectmember__is_active=True, + project__project_projectmember__member=request.user, + workspace__slug=slug, + assignees__in=[request.user], + ) + .filter(**extra_filters) + .count() + ) - completed_issues_count = Issue.issue_objects.filter( - workspace__slug=slug, - project__project_projectmember__is_active=True, - project__project_projectmember__member=request.user, - assignees__in=[request.user], - state__group="completed", - ).count() + created_issues_count = ( + Issue.issue_objects.filter( + workspace__slug=slug, + project__project_projectmember__is_active=True, + project__project_projectmember__member=request.user, + created_by_id=request.user.id, + ) + .filter(**extra_filters) + .count() + ) + + completed_issues_count = ( + Issue.issue_objects.filter( + workspace__slug=slug, + project__project_projectmember__is_active=True, + project__project_projectmember__member=request.user, + assignees__in=[request.user], + state__group="completed", + ) + .filter(**extra_filters) + .count() + ) return Response( { @@ -166,6 +192,14 @@ def dashboard_assigned_issues(self, request, slug): ) ) + if WorkspaceMember.objects.filter( + workspace__slug=slug, + member=request.user, + role=5, + is_active=True, + ).exists(): + assigned_issues = assigned_issues.filter(created_by=request.user) + # Priority Ordering priority_order = ["urgent", "high", "medium", "low", "none"] assigned_issues = assigned_issues.annotate( @@ -409,6 +443,16 @@ def dashboard_created_issues(self, request, slug): def dashboard_issues_by_state_groups(self, request, slug): filters = issue_filters(request.query_params, "GET") state_order = ["backlog", "unstarted", "started", "completed", "cancelled"] + extra_filters = {} + + if WorkspaceMember.objects.filter( + workspace__slug=slug, + member=request.user, + role=5, + is_active=True, + ).exists(): + extra_filters = {"created_by": request.user} + issues_by_state_groups = ( Issue.issue_objects.filter( workspace__slug=slug, @@ -416,7 +460,7 @@ def dashboard_issues_by_state_groups(self, request, slug): project__project_projectmember__member=request.user, assignees__in=[request.user], ) - .filter(**filters) + .filter(**filters, **extra_filters) .values("state__group") .annotate(count=Count("id")) ) @@ -439,6 +483,15 @@ def dashboard_issues_by_state_groups(self, request, slug): def dashboard_issues_by_priority(self, request, slug): filters = issue_filters(request.query_params, "GET") priority_order = ["urgent", "high", "medium", "low", "none"] + extra_filters = {} + + if WorkspaceMember.objects.filter( + workspace__slug=slug, + member=request.user, + role=5, + is_active=True, + ).exists(): + extra_filters = {"created_by": request.user} issues_by_priority = ( Issue.issue_objects.filter( @@ -447,7 +500,7 @@ def dashboard_issues_by_priority(self, request, slug): project__project_projectmember__member=request.user, assignees__in=[request.user], ) - .filter(**filters) + .filter(**filters, **extra_filters) .values("priority") .annotate(count=Count("id")) ) diff --git a/apiserver/plane/app/views/module/base.py b/apiserver/plane/app/views/module/base.py index e830c05a4e5..952774b584f 100644 --- a/apiserver/plane/app/views/module/base.py +++ b/apiserver/plane/app/views/module/base.py @@ -747,26 +747,12 @@ def partial_update(self, request, slug, project_id, pk): return Response(module, status=status.HTTP_200_OK) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - @allow_permission(["ADMIN", "MEMBER", "VIEWER", "GUEST"]) + @allow_permission(["ADMIN"], creator=True, model=Module) def destroy(self, request, slug, project_id, pk): module = Module.objects.get( workspace__slug=slug, project_id=project_id, pk=pk ) - if module.created_by_id != request.user.id and ( - not ProjectMember.objects.filter( - workspace__slug=slug, - member=request.user, - role=20, - project_id=project_id, - is_active=True, - ).exists() - ): - return Response( - {"error": "Only admin or creator can delete the module"}, - status=status.HTTP_403_FORBIDDEN, - ) - module_issues = list( ModuleIssue.objects.filter(module_id=pk).values_list( "issue", flat=True diff --git a/apiserver/plane/app/views/page/base.py b/apiserver/plane/app/views/page/base.py index 44c2c21408f..8b7a4d714fd 100644 --- a/apiserver/plane/app/views/page/base.py +++ b/apiserver/plane/app/views/page/base.py @@ -19,7 +19,7 @@ from rest_framework.response import Response -from plane.app.permissions import ProjectEntityPermission +from plane.app.permissions import allow_permission from plane.app.serializers import ( PageLogSerializer, PageSerializer, @@ -60,9 +60,6 @@ def unarchive_archive_page_and_descendants(page_id, archived_at): class PageViewSet(BaseViewSet): serializer_class = PageSerializer model = Page - permission_classes = [ - ProjectEntityPermission, - ] search_fields = [ "name", ] @@ -122,6 +119,7 @@ def get_queryset(self): .distinct() ) + @allow_permission(["ADMIN", "MEMBER"]) def create(self, request, slug, project_id): serializer = PageSerializer( data=request.data, @@ -143,6 +141,7 @@ def create(self, request, slug, project_id): return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + @allow_permission(["ADMIN", "MEMBER"]) def partial_update(self, request, slug, project_id, pk): try: page = Page.objects.get( @@ -208,6 +207,7 @@ def partial_update(self, request, slug, project_id, pk): status=status.HTTP_400_BAD_REQUEST, ) + @allow_permission(["ADMIN", "MEMBER", "VIEWER"]) def retrieve(self, request, slug, project_id, pk=None): page = self.get_queryset().filter(pk=pk).first() if page is None: @@ -226,6 +226,7 @@ def retrieve(self, request, slug, project_id, pk=None): status=status.HTTP_200_OK, ) + @allow_permission(["ADMIN", "MEMBER"]) def lock(self, request, slug, project_id, pk): page = Page.objects.filter( pk=pk, workspace__slug=slug, projects__id=project_id @@ -235,6 +236,7 @@ def lock(self, request, slug, project_id, pk): page.save() return Response(status=status.HTTP_204_NO_CONTENT) + @allow_permission(["ADMIN", "MEMBER"]) def unlock(self, request, slug, project_id, pk): page = Page.objects.filter( pk=pk, workspace__slug=slug, projects__id=project_id @@ -245,6 +247,7 @@ def unlock(self, request, slug, project_id, pk): return Response(status=status.HTTP_204_NO_CONTENT) + @allow_permission(["ADMIN", "MEMBER"]) def access(self, request, slug, project_id, pk): access = request.data.get("access", 0) page = Page.objects.filter( @@ -267,11 +270,13 @@ def access(self, request, slug, project_id, pk): page.save() return Response(status=status.HTTP_204_NO_CONTENT) + @allow_permission(["ADMIN", "MEMBER", "VIEWER"]) def list(self, request, slug, project_id): queryset = self.get_queryset() pages = PageSerializer(queryset, many=True).data return Response(pages, status=status.HTTP_200_OK) + @allow_permission(["ADMIN", "MEMBER"]) def archive(self, request, slug, project_id, pk): page = Page.objects.get( pk=pk, workspace__slug=slug, projects__id=project_id @@ -299,6 +304,7 @@ def archive(self, request, slug, project_id, pk): status=status.HTTP_200_OK, ) + @allow_permission(["ADMIN", "MEMBER"]) def unarchive(self, request, slug, project_id, pk): page = Page.objects.get( pk=pk, workspace__slug=slug, projects__id=project_id @@ -328,6 +334,7 @@ def unarchive(self, request, slug, project_id, pk): return Response(status=status.HTTP_204_NO_CONTENT) + @allow_permission(["ADMIN"], creator=True, model=Page) def destroy(self, request, slug, project_id, pk): page = Page.objects.get( pk=pk, workspace__slug=slug, projects__id=project_id @@ -370,12 +377,10 @@ def destroy(self, request, slug, project_id, pk): class PageFavoriteViewSet(BaseViewSet): - permission_classes = [ - ProjectEntityPermission, - ] model = UserFavorite + @allow_permission(["ADMIN", "MEMBER"]) def create(self, request, slug, project_id, pk): _ = UserFavorite.objects.create( project_id=project_id, @@ -385,6 +390,7 @@ def create(self, request, slug, project_id, pk): ) return Response(status=status.HTTP_204_NO_CONTENT) + @allow_permission(["ADMIN", "MEMBER"]) def destroy(self, request, slug, project_id, pk): page_favorite = UserFavorite.objects.get( project=project_id, @@ -398,9 +404,6 @@ def destroy(self, request, slug, project_id, pk): class PageLogEndpoint(BaseAPIView): - permission_classes = [ - ProjectEntityPermission, - ] serializer_class = PageLogSerializer model = PageLog @@ -440,9 +443,6 @@ def delete(self, request, slug, project_id, page_id, transaction): class SubPagesEndpoint(BaseAPIView): - permission_classes = [ - ProjectEntityPermission, - ] @method_decorator(gzip_page) def get(self, request, slug, project_id, page_id): @@ -461,10 +461,8 @@ def get(self, request, slug, project_id, page_id): class PagesDescriptionViewSet(BaseViewSet): - permission_classes = [ - ProjectEntityPermission, - ] + @allow_permission(["ADMIN", "MEMBER", "VIEWER"]) def retrieve(self, request, slug, project_id, pk): page = ( Page.objects.filter( @@ -489,6 +487,7 @@ def stream_data(): ) return response + @allow_permission(["ADMIN", "MEMBER", "VIEWER", "GUEST"]) def partial_update(self, request, slug, project_id, pk): page = ( Page.objects.filter( diff --git a/apiserver/plane/app/views/project/member.py b/apiserver/plane/app/views/project/member.py index 4af7d268919..8fadd1b15e2 100644 --- a/apiserver/plane/app/views/project/member.py +++ b/apiserver/plane/app/views/project/member.py @@ -31,9 +31,6 @@ class ProjectMemberViewSet(BaseViewSet): serializer_class = ProjectMemberAdminSerializer model = ProjectMember - # permission_classes = [ - # ProjectMemberPermission, - # ] def get_permissions(self): if self.action == "leave": @@ -173,7 +170,7 @@ def create(self, request, slug, project_id): # Return the serialized data return Response(serializer.data, status=status.HTTP_201_CREATED) - @allow_permission(["ADMIN", "MEMBER"]) + @allow_permission(["ADMIN", "MEMBER", "VIEWER", "GUEST"]) def list(self, request, slug, project_id): # Get the list of project members for the project project_members = ProjectMember.objects.filter( From 4260e0e2eb08aa064dd68c0ade10078fc536264a Mon Sep 17 00:00:00 2001 From: NarayanBavisetti Date: Tue, 13 Aug 2024 15:20:52 +0530 Subject: [PATCH 20/30] chore: guest role changes --- apiserver/plane/app/views/project/member.py | 39 +++++++++++++++++++-- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git a/apiserver/plane/app/views/project/member.py b/apiserver/plane/app/views/project/member.py index 8fadd1b15e2..afb3931c1b5 100644 --- a/apiserver/plane/app/views/project/member.py +++ b/apiserver/plane/app/views/project/member.py @@ -23,11 +23,13 @@ Workspace, TeamMember, IssueUserProperty, + WorkspaceMember, ) from plane.bgtasks.project_add_user_email_task import project_add_user_email from plane.utils.host import base_host from plane.app.permissions.base import allow_permission + class ProjectMemberViewSet(BaseViewSet): serializer_class = ProjectMemberAdminSerializer model = ProjectMember @@ -62,7 +64,7 @@ def get_queryset(self): .select_related("workspace", "workspace__owner") ) - @allow_permission(["ADMIN", "MEMBER"]) + @allow_permission(["ADMIN"]) def create(self, request, slug, project_id): # Get the list of members to be added to the project and their roles i.e. the user_id and the role members = request.data.get("members", []) @@ -86,6 +88,23 @@ def create(self, request, slug, project_id): member.get("member_id"): member.get("role") for member in members } + # check the workspace role of the new user + for member in member_roles: + workspace_member_role = WorkspaceMember.objects.get( + workspace__slug=slug, + member=member, + is_active=True, + ).role + if workspace_member_role in [5, 10] and member_roles.get( + member + ) in [15, 20]: + return Response( + { + "error": "You cannot add a user with role higher than the workspace role" + }, + status=status.HTTP_400_BAD_REQUEST, + ) + # Update roles in the members array based on the member_roles dictionary and set is_active to True for project_member in ProjectMember.objects.filter( project_id=project_id, @@ -185,7 +204,7 @@ def list(self, request, slug, project_id): ) return Response(serializer.data, status=status.HTTP_200_OK) - @allow_permission(["ADMIN", "MEMBER"]) + @allow_permission(["ADMIN"]) def partial_update(self, request, slug, project_id, pk): project_member = ProjectMember.objects.get( pk=pk, @@ -205,6 +224,22 @@ def partial_update(self, request, slug, project_id, pk): member=request.user, is_active=True, ) + + workspace_role = WorkspaceMember.objects.get( + workspace__slug=slug, + member=project_member.member, + is_active=True, + ).role + if workspace_role in [5, 10] and int( + request.data.get("role", project_member.role) + ) in [15, 20]: + return Response( + { + "error": "You cannot add a user with role higher than the workspace role" + }, + status=status.HTTP_400_BAD_REQUEST, + ) + if ( "role" in request.data and int(request.data.get("role", project_member.role)) From c4a1cd482a09a8e5964f9b2a42f1fe4a84efd137 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia Date: Tue, 13 Aug 2024 16:04:49 +0530 Subject: [PATCH 21/30] fix: app sidebar project item permission --- .../components/workspace/sidebar/projects-list-item.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/web/core/components/workspace/sidebar/projects-list-item.tsx b/web/core/components/workspace/sidebar/projects-list-item.tsx index 6a68f1b4c27..31ed92dfd3c 100644 --- a/web/core/components/workspace/sidebar/projects-list-item.tsx +++ b/web/core/components/workspace/sidebar/projects-list-item.tsx @@ -113,7 +113,7 @@ export const SidebarProjectsListItem: React.FC = observer((props) => { const { addProjectToFavorites, removeProjectFromFavorites, getProjectById } = useProject(); const { isMobile } = usePlatformOS(); const { - membership: { currentProjectRole }, + membership: { currentWorkspaceAllProjectsRole }, } = useUser(); // states const [leaveProjectModalOpen, setLeaveProjectModal] = useState(false); @@ -489,7 +489,9 @@ export const SidebarProjectsListItem: React.FC = observer((props) => { (item.name === "Intake" && !project.inbox_view) ) return; - const currentRole = currentProjectRole ?? 5; + const currentRole = currentWorkspaceAllProjectsRole + ? currentWorkspaceAllProjectsRole[projectId] + : undefined; return ( <> {currentRole >= item.access && ( From caef0d8f5516f21de0ac760fa464f09df94adc61 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia Date: Tue, 13 Aug 2024 16:06:11 +0530 Subject: [PATCH 22/30] fix: project setting empty state flicker --- .../(detail)/[projectId]/settings/automations/page.tsx | 7 +++++-- .../(detail)/[projectId]/settings/estimates/page.tsx | 7 +++++-- .../(detail)/[projectId]/settings/features/page.tsx | 7 +++++-- .../projects/(detail)/[projectId]/settings/labels/page.tsx | 7 +++++-- .../(detail)/[projectId]/settings/members/page.tsx | 7 +++++-- .../projects/(detail)/[projectId]/settings/states/page.tsx | 7 +++++-- 6 files changed, 30 insertions(+), 12 deletions(-) diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/automations/page.tsx b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/automations/page.tsx index 380966b0044..bcf6258dd56 100644 --- a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/automations/page.tsx +++ b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/automations/page.tsx @@ -17,7 +17,10 @@ const AutomationSettingsPage = observer(() => { // router const { workspaceSlug, projectId } = useParams(); // store hooks - const { canPerformProjectAdminActions } = useUser(); + const { + canPerformProjectAdminActions, + membership: { currentProjectRole }, + } = useUser(); const { currentProjectDetails: projectDetails, updateProject } = useProject(); const handleChange = async (formData: Partial) => { @@ -35,7 +38,7 @@ const AutomationSettingsPage = observer(() => { // derived values const pageTitle = projectDetails?.name ? `${projectDetails?.name} - Automations` : undefined; - if (!canPerformProjectAdminActions) { + if (currentProjectRole && !canPerformProjectAdminActions) { return ; } diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/estimates/page.tsx b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/estimates/page.tsx index bbcf3f966f8..84b01b86ab9 100644 --- a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/estimates/page.tsx +++ b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/estimates/page.tsx @@ -11,7 +11,10 @@ import { useUser, useProject } from "@/hooks/store"; const EstimatesSettingsPage = observer(() => { const { workspaceSlug, projectId } = useParams(); - const { canPerformProjectAdminActions } = useUser(); + const { + canPerformProjectAdminActions, + membership: { currentProjectRole }, + } = useUser(); const { currentProjectDetails } = useProject(); // derived values @@ -19,7 +22,7 @@ const EstimatesSettingsPage = observer(() => { if (!workspaceSlug || !projectId) return <>; - if (!canPerformProjectAdminActions) { + if (currentProjectRole && !canPerformProjectAdminActions) { return ; } diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/features/page.tsx b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/features/page.tsx index 24c05b3f25c..8aba7c88aea 100644 --- a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/features/page.tsx +++ b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/features/page.tsx @@ -12,14 +12,17 @@ import { useProject, useUser } from "@/hooks/store"; const FeaturesSettingsPage = observer(() => { const { workspaceSlug, projectId } = useParams(); // store - const { canPerformProjectAdminActions } = useUser(); + const { + canPerformProjectAdminActions, + membership: { currentProjectRole }, + } = useUser(); const { currentProjectDetails } = useProject(); // derived values const pageTitle = currentProjectDetails?.name ? `${currentProjectDetails?.name} - Features` : undefined; if (!workspaceSlug || !projectId) return null; - if (!canPerformProjectAdminActions) { + if (currentProjectRole && !canPerformProjectAdminActions) { return ; } diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/labels/page.tsx b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/labels/page.tsx index df5357f40d9..0e88b9c5d5e 100644 --- a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/labels/page.tsx +++ b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/labels/page.tsx @@ -14,7 +14,10 @@ import { useProject, useUser } from "@/hooks/store"; const LabelsSettingsPage = observer(() => { // store hooks const { currentProjectDetails } = useProject(); - const { canPerformProjectMemberActions } = useUser(); + const { + canPerformProjectMemberActions, + membership: { currentProjectRole }, + } = useUser(); const pageTitle = currentProjectDetails?.name ? `${currentProjectDetails?.name} - Labels` : undefined; const scrollableContainerRef = useRef(null); @@ -32,7 +35,7 @@ const LabelsSettingsPage = observer(() => { ); }, [scrollableContainerRef?.current]); - if (!canPerformProjectMemberActions) { + if (currentProjectRole && !canPerformProjectMemberActions) { return ; } diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/members/page.tsx b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/members/page.tsx index 56e59caa81c..28c65f680fd 100644 --- a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/members/page.tsx +++ b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/members/page.tsx @@ -11,11 +11,14 @@ import { useProject, useUser } from "@/hooks/store"; const MembersSettingsPage = observer(() => { // store const { currentProjectDetails } = useProject(); - const { canPerformProjectViewerActions } = useUser(); + const { + canPerformProjectViewerActions, + membership: { currentProjectRole }, + } = useUser(); // derived values const pageTitle = currentProjectDetails?.name ? `${currentProjectDetails?.name} - Members` : undefined; - if (!canPerformProjectViewerActions) { + if (currentProjectRole && !canPerformProjectViewerActions) { return ; } diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/states/page.tsx b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/states/page.tsx index cd94594b9d7..b03310a0917 100644 --- a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/states/page.tsx +++ b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/states/page.tsx @@ -13,11 +13,14 @@ const StatesSettingsPage = observer(() => { const { workspaceSlug, projectId } = useParams(); // store const { currentProjectDetails } = useProject(); - const { canPerformProjectMemberActions } = useUser(); + const { + canPerformProjectMemberActions, + membership: { currentProjectRole }, + } = useUser(); // derived values const pageTitle = currentProjectDetails?.name ? `${currentProjectDetails?.name} - States` : undefined; - if (!canPerformProjectMemberActions) { + if (currentProjectRole && !canPerformProjectMemberActions) { return ; } From 8ad025e1034eec6eb8467fa306754c1791c728b8 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia Date: Tue, 13 Aug 2024 16:09:34 +0530 Subject: [PATCH 23/30] fix: workspace setting empty state flicker --- .../(projects)/settings/api-tokens/page.tsx | 9 ++++++--- .../(projects)/settings/billing/page.tsx | 7 +++++-- .../(projects)/settings/exports/page.tsx | 8 ++++++-- .../(projects)/settings/members/page.tsx | 10 +++++++--- .../(projects)/settings/webhooks/page.tsx | 9 ++++++--- 5 files changed, 30 insertions(+), 13 deletions(-) diff --git a/web/app/[workspaceSlug]/(projects)/settings/api-tokens/page.tsx b/web/app/[workspaceSlug]/(projects)/settings/api-tokens/page.tsx index 7c9241b3937..bfc583b7859 100644 --- a/web/app/[workspaceSlug]/(projects)/settings/api-tokens/page.tsx +++ b/web/app/[workspaceSlug]/(projects)/settings/api-tokens/page.tsx @@ -28,7 +28,10 @@ const ApiTokensPage = observer(() => { // router const { workspaceSlug } = useParams(); // store hooks - const { canPerformWorkspaceAdminActions } = useUser(); + const { + canPerformWorkspaceAdminActions, + membership: { currentWorkspaceRole }, + } = useUser(); const { currentWorkspace } = useWorkspace(); const { data: tokens } = useSWR( @@ -39,7 +42,7 @@ const ApiTokensPage = observer(() => { const pageTitle = currentWorkspace?.name ? `${currentWorkspace.name} - API Tokens` : undefined; - if (!canPerformWorkspaceAdminActions) { + if (currentWorkspaceRole && !canPerformWorkspaceAdminActions) { return ; } @@ -84,4 +87,4 @@ const ApiTokensPage = observer(() => { ); }); -export default ApiTokensPage; \ No newline at end of file +export default ApiTokensPage; diff --git a/web/app/[workspaceSlug]/(projects)/settings/billing/page.tsx b/web/app/[workspaceSlug]/(projects)/settings/billing/page.tsx index e436e5edd70..0158c3c98e1 100644 --- a/web/app/[workspaceSlug]/(projects)/settings/billing/page.tsx +++ b/web/app/[workspaceSlug]/(projects)/settings/billing/page.tsx @@ -11,12 +11,15 @@ import { BillingRoot } from "@/plane-web/components/workspace"; const BillingSettingsPage = observer(() => { // store hooks - const { canPerformWorkspaceAdminActions } = useUser(); + const { + canPerformWorkspaceAdminActions, + membership: { currentWorkspaceRole }, + } = useUser(); const { currentWorkspace } = useWorkspace(); // derived values const pageTitle = currentWorkspace?.name ? `${currentWorkspace.name} - Billing & Plans` : undefined; - if (!canPerformWorkspaceAdminActions) { + if (currentWorkspaceRole && !canPerformWorkspaceAdminActions) { return ; } diff --git a/web/app/[workspaceSlug]/(projects)/settings/exports/page.tsx b/web/app/[workspaceSlug]/(projects)/settings/exports/page.tsx index 21e9beccefb..85b8cd64424 100644 --- a/web/app/[workspaceSlug]/(projects)/settings/exports/page.tsx +++ b/web/app/[workspaceSlug]/(projects)/settings/exports/page.tsx @@ -12,14 +12,18 @@ import { useUser, useWorkspace } from "@/hooks/store"; const ExportsPage = observer(() => { // store hooks - const { canPerformWorkspaceViewerActions, canPerformWorkspaceMemberActions } = useUser(); + const { + canPerformWorkspaceViewerActions, + canPerformWorkspaceMemberActions, + membership: { currentWorkspaceRole }, + } = useUser(); const { currentWorkspace } = useWorkspace(); // derived values const pageTitle = currentWorkspace?.name ? `${currentWorkspace.name} - Exports` : undefined; // if user is not authorized to view this page - if (!canPerformWorkspaceViewerActions) { + if (currentWorkspaceRole && !canPerformWorkspaceViewerActions) { return ; } diff --git a/web/app/[workspaceSlug]/(projects)/settings/members/page.tsx b/web/app/[workspaceSlug]/(projects)/settings/members/page.tsx index 1d4c0f18cf5..0d48bef73f3 100644 --- a/web/app/[workspaceSlug]/(projects)/settings/members/page.tsx +++ b/web/app/[workspaceSlug]/(projects)/settings/members/page.tsx @@ -28,8 +28,12 @@ const WorkspaceMembersSettingsPage = observer(() => { const { workspaceSlug } = useParams(); // store hooks const { captureEvent } = useEventTracker(); - const { canPerformWorkspaceAdminActions, canPerformWorkspaceViewerActions, canPerformWorkspaceMemberActions } = - useUser(); + const { + canPerformWorkspaceAdminActions, + canPerformWorkspaceViewerActions, + canPerformWorkspaceMemberActions, + membership: { currentWorkspaceRole }, + } = useUser(); const { workspace: { inviteMembersToWorkspace }, } = useMember(); @@ -82,7 +86,7 @@ const WorkspaceMembersSettingsPage = observer(() => { const pageTitle = currentWorkspace?.name ? `${currentWorkspace.name} - Members` : undefined; // if user is not authorized to view this page - if (!canPerformWorkspaceViewerActions) { + if (currentWorkspaceRole && !canPerformWorkspaceViewerActions) { return ; } diff --git a/web/app/[workspaceSlug]/(projects)/settings/webhooks/page.tsx b/web/app/[workspaceSlug]/(projects)/settings/webhooks/page.tsx index 2d0629b705a..a887c414406 100644 --- a/web/app/[workspaceSlug]/(projects)/settings/webhooks/page.tsx +++ b/web/app/[workspaceSlug]/(projects)/settings/webhooks/page.tsx @@ -23,7 +23,10 @@ const WebhooksListPage = observer(() => { // router const { workspaceSlug } = useParams(); // mobx store - const { canPerformWorkspaceAdminActions } = useUser(); + const { + canPerformWorkspaceAdminActions, + membership: { currentWorkspaceRole }, + } = useUser(); const { fetchWebhooks, webhooks, clearSecretKey, webhookSecretKey, createWebhook } = useWebhook(); const { currentWorkspace } = useWorkspace(); @@ -39,7 +42,7 @@ const WebhooksListPage = observer(() => { if (!showCreateWebhookModal && webhookSecretKey) clearSecretKey(); }, [showCreateWebhookModal, webhookSecretKey, clearSecretKey]); - if (!canPerformWorkspaceAdminActions) { + if (currentWorkspaceRole && !canPerformWorkspaceAdminActions) { return ; } @@ -86,4 +89,4 @@ const WebhooksListPage = observer(() => { ); }); -export default WebhooksListPage; \ No newline at end of file +export default WebhooksListPage; From bc359493040dc5cedf5c40910d7bd83f687245e6 Mon Sep 17 00:00:00 2001 From: NarayanBavisetti Date: Wed, 14 Aug 2024 13:16:09 +0530 Subject: [PATCH 24/30] chore: granted notification permission to viewer --- apiserver/plane/app/views/notification/base.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/apiserver/plane/app/views/notification/base.py b/apiserver/plane/app/views/notification/base.py index 543d830eddd..f6e1e3a6615 100644 --- a/apiserver/plane/app/views/notification/base.py +++ b/apiserver/plane/app/views/notification/base.py @@ -40,7 +40,9 @@ def get_queryset(self): .select_related("workspace", "project," "triggered_by", "receiver") ) - @allow_permission(roles=["ADMIN", "MEMBER", "GUEST"], level="WORKSPACE") + @allow_permission( + roles=["ADMIN", "MEMBER", "VIEWER", "GUEST"], level="WORKSPACE" + ) def list(self, request, slug): # Get query parameters snoozed = request.GET.get("snoozed", "false") @@ -170,7 +172,9 @@ def list(self, request, slug): serializer = NotificationSerializer(notifications, many=True) return Response(serializer.data, status=status.HTTP_200_OK) - @allow_permission(roles=["ADMIN", "MEMBER", "GUEST"], level="WORKSPACE") + @allow_permission( + roles=["ADMIN", "MEMBER", "VIEWER", "GUEST"], level="WORKSPACE" + ) def partial_update(self, request, slug, pk): notification = Notification.objects.get( workspace__slug=slug, pk=pk, receiver=request.user @@ -231,7 +235,9 @@ def unarchive(self, request, slug, pk): class UnreadNotificationEndpoint(BaseAPIView): - @allow_permission(roles=["ADMIN", "MEMBER", "GUEST"], level="WORKSPACE") + @allow_permission( + roles=["ADMIN", "MEMBER", "VIEWER", "GUEST"], level="WORKSPACE" + ) def get(self, request, slug): # Watching Issues Count unread_notifications_count = ( From 50ce413987e19ae117e1314665157f78e439d2a7 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia Date: Wed, 14 Aug 2024 13:42:44 +0530 Subject: [PATCH 25/30] chore: project invite and edit validation updated --- .../project/send-project-invitation-modal.tsx | 16 ++++------- .../project/settings/member-columns.tsx | 27 +++++++++++++++---- 2 files changed, 27 insertions(+), 16 deletions(-) diff --git a/web/core/components/project/send-project-invitation-modal.tsx b/web/core/components/project/send-project-invitation-modal.tsx index 8a2297335fa..759137c8d84 100644 --- a/web/core/components/project/send-project-invitation-modal.tsx +++ b/web/core/components/project/send-project-invitation-modal.tsx @@ -170,22 +170,16 @@ export const SendProjectInvitationModal: React.FC = observer((props) => { | undefined; const checkCurrentOptionWorkspaceRole = (value: string) => { - const selectedMemberWorkspaceRole = getWorkspaceMemberDetails(value)?.role; - if (!value || !selectedMemberWorkspaceRole) return ROLE; + const currentMemberWorkspaceRole = getWorkspaceMemberDetails(value)?.role; + if (!value || !currentMemberWorkspaceRole) return ROLE; - // Filter roles based on the selected member's workspace role const isGuestOrViewer = [EUserWorkspaceRoles.GUEST, EUserWorkspaceRoles.VIEWER].includes( - selectedMemberWorkspaceRole + currentMemberWorkspaceRole ); - const filteredRoles = Object.fromEntries( - Object.entries(ROLE).filter(([key]) => { - const roleKey = parseInt(key); - return isGuestOrViewer ? roleKey === selectedMemberWorkspaceRole : roleKey <= selectedMemberWorkspaceRole; - }) + return Object.fromEntries( + Object.entries(ROLE).filter(([key]) => !isGuestOrViewer || [5, 10].includes(parseInt(key))) ); - - return filteredRoles; }; return ( diff --git a/web/core/components/project/settings/member-columns.tsx b/web/core/components/project/settings/member-columns.tsx index 0c4184e0eae..51f510d306c 100644 --- a/web/core/components/project/settings/member-columns.tsx +++ b/web/core/components/project/settings/member-columns.tsx @@ -91,6 +91,7 @@ export const AccountTypeColumn: React.FC = observer((props) => // store hooks const { project: { updateMember }, + workspace: { getWorkspaceMemberDetails }, } = useMember(); const { data: currentUser } = useUser(); @@ -99,6 +100,19 @@ export const AccountTypeColumn: React.FC = observer((props) => const isAdminRole = currentProjectRole === EUserProjectRoles.ADMIN; const isRoleNonEditable = isCurrentUser || !isAdminRole; + const checkCurrentOptionWorkspaceRole = (value: string) => { + const currentMemberWorkspaceRole = getWorkspaceMemberDetails(value)?.role; + if (!value || !currentMemberWorkspaceRole) return ROLE; + + const isGuestOrViewer = [EUserWorkspaceRoles.GUEST, EUserWorkspaceRoles.VIEWER].includes( + currentMemberWorkspaceRole + ); + + return Object.fromEntries( + Object.entries(ROLE).filter(([key]) => !isGuestOrViewer || [5, 10].includes(parseInt(key))) + ); + }; + return ( <> {isRoleNonEditable ? ( @@ -140,11 +154,14 @@ export const AccountTypeColumn: React.FC = observer((props) => optionsClassName="w-full" input > - {Object.keys(ROLE).map((item) => ( - - {ROLE[item as unknown as keyof typeof ROLE]} - - ))} + {Object.entries(checkCurrentOptionWorkspaceRole(rowData.member.id)).map(([key, label]) => { + if (parseInt(key) > (currentProjectRole ?? EUserProjectRoles.GUEST)) return null; + return ( + + {label} + + ); + })} )} /> From 4349b5a8995cd9996a3438d19d7d6d02adce6237 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia Date: Wed, 14 Aug 2024 13:54:45 +0530 Subject: [PATCH 26/30] chore: favorite validation added for guest and viewer role --- .../pages/list/block-item-action.tsx | 27 ++++++++---- .../workspace/sidebar/projects-list-item.tsx | 42 +++++++++++-------- 2 files changed, 42 insertions(+), 27 deletions(-) diff --git a/web/core/components/pages/list/block-item-action.tsx b/web/core/components/pages/list/block-item-action.tsx index 83f5e3941cc..a5b3b5866cd 100644 --- a/web/core/components/pages/list/block-item-action.tsx +++ b/web/core/components/pages/list/block-item-action.tsx @@ -7,10 +7,12 @@ import { Earth, Info, Lock, Minus } from "lucide-react"; import { Avatar, FavoriteStar, TOAST_TYPE, Tooltip, setToast } from "@plane/ui"; // components import { PageQuickActions } from "@/components/pages/dropdowns"; +// constants +import { EUserProjectRoles } from "@/constants/project"; // helpers import { renderFormattedDate } from "@/helpers/date-time.helper"; // hooks -import { useMember, usePage } from "@/hooks/store"; +import { useMember, usePage, useProject } from "@/hooks/store"; type Props = { workspaceSlug: string; @@ -25,10 +27,15 @@ export const BlockItemAction: FC = observer((props) => { // store hooks const page = usePage(pageId); const { getUserDetails } = useMember(); + const { getProjectById } = useProject(); + // derived values const { access, created_at, is_favorite, owned_by, addToFavorites, removePageFromFavorites } = page; // derived values + const project = getProjectById(projectId); + const isViewerOrGuest = + project?.member_role && [EUserProjectRoles.VIEWER, EUserProjectRoles.GUEST].includes(project.member_role); const ownerDetails = owned_by ? getUserDetails(owned_by) : undefined; // handlers @@ -74,14 +81,16 @@ export const BlockItemAction: FC = observer((props) => { {/* favorite/unfavorite */} - { - e.preventDefault(); - e.stopPropagation(); - handleFavorites(); - }} - selected={is_favorite} - /> + {!isViewerOrGuest && ( + { + e.preventDefault(); + e.stopPropagation(); + handleFavorites(); + }} + selected={is_favorite} + /> + )} {/* quick actions dropdown */} = observer((props) => { customButtonClassName="grid place-items-center" placement="bottom-start" > - - - - {project.is_favorite ? "Remove from favorites" : "Add to favorites"} - - + {!isViewerOrGuest && ( + + + + {project.is_favorite ? "Remove from favorites" : "Add to favorites"} + + + )} {/* publish project settings */} {isAdmin && ( @@ -407,14 +411,16 @@ export const SidebarProjectsListItem: React.FC = observer((props) => {
)} - - -
- - Draft issues -
- -
+ {!isViewerOrGuest && ( + + +
+ + Draft issues +
+ +
+ )} From 6c808c4f4d642de69ea685483d7034d80ac01a3c Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia Date: Wed, 14 Aug 2024 14:04:42 +0530 Subject: [PATCH 27/30] chore: create view validation updated --- .../[projectId]/views/(list)/header.tsx | 21 ++++++------------- .../(projects)/workspace-views/header.tsx | 17 +++++---------- 2 files changed, 11 insertions(+), 27 deletions(-) diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/views/(list)/header.tsx b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/views/(list)/header.tsx index 26d5fd873c1..27f84d94efd 100644 --- a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/views/(list)/header.tsx +++ b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/views/(list)/header.tsx @@ -12,21 +12,17 @@ import { BreadcrumbLink, Logo } from "@/components/common"; import { ViewListHeader } from "@/components/views"; import { ViewAppliedFiltersList } from "@/components/views/applied-filters"; // constants -import { EUserProjectRoles } from "@/constants/project"; import { EViewAccess } from "@/constants/views"; // helpers import { calculateTotalFilters } from "@/helpers/filter.helper"; // hooks -import { useCommandPalette, useProject, useProjectView, useUser } from "@/hooks/store"; +import { useCommandPalette, useProject, useProjectView } from "@/hooks/store"; export const ProjectViewsHeader = observer(() => { // router const { workspaceSlug } = useParams(); // store hooks const { toggleCreateViewModal } = useCommandPalette(); - const { - membership: { currentProjectRole }, - } = useUser(); const { currentProjectDetails, loader } = useProject(); const { filters, updateFilters, clearAllFilters } = useProjectView(); @@ -49,9 +45,6 @@ export const ProjectViewsHeader = observer(() => { const isFiltersApplied = calculateTotalFilters(filters?.filters ?? {}) !== 0; - const canUserCreateView = - currentProjectRole && [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER].includes(currentProjectRole); - return ( <>
@@ -83,13 +76,11 @@ export const ProjectViewsHeader = observer(() => {
- {canUserCreateView && ( -
- -
- )} +
+ +
{isFiltersApplied && ( diff --git a/web/app/[workspaceSlug]/(projects)/workspace-views/header.tsx b/web/app/[workspaceSlug]/(projects)/workspace-views/header.tsx index 686e4f7b607..82e11b20836 100644 --- a/web/app/[workspaceSlug]/(projects)/workspace-views/header.tsx +++ b/web/app/[workspaceSlug]/(projects)/workspace-views/header.tsx @@ -14,11 +14,10 @@ import { DisplayFiltersSelection, FiltersDropdown, FilterSelection } from "@/com import { CreateUpdateWorkspaceViewModal } from "@/components/workspace"; // constants import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "@/constants/issue"; -import { EUserWorkspaceRoles } from "@/constants/workspace"; // helpers import { isIssueFilterActive } from "@/helpers/filter.helper"; // hooks -import { useLabel, useMember, useUser, useIssues, useGlobalView } from "@/hooks/store"; +import { useLabel, useMember, useIssues, useGlobalView } from "@/hooks/store"; export const GlobalIssuesHeader = observer(() => { // states @@ -30,9 +29,6 @@ export const GlobalIssuesHeader = observer(() => { issuesFilter: { filters, updateFilters }, } = useIssues(EIssuesStoreType.GLOBAL); const { getViewDetailsById } = useGlobalView(); - const { - membership: { currentWorkspaceRole }, - } = useUser(); const { workspaceLabels } = useLabel(); const { workspace: { workspaceMemberIds }, @@ -97,8 +93,6 @@ export const GlobalIssuesHeader = observer(() => { [workspaceSlug, updateFilters, globalViewId] ); - const isAuthorizedUser = !!currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER; - const isLocked = viewDetails?.is_locked; return ( @@ -142,11 +136,10 @@ export const GlobalIssuesHeader = observer(() => { )} - {isAuthorizedUser && ( - - )} + + From 1e8698c6d225a92358eb33170b7210cd221f5efd Mon Sep 17 00:00:00 2001 From: NarayanBavisetti Date: Wed, 14 Aug 2024 14:06:17 +0530 Subject: [PATCH 28/30] chore: views permission changes --- apiserver/plane/app/views/view/base.py | 34 +++++++++++++++++--------- 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/apiserver/plane/app/views/view/base.py b/apiserver/plane/app/views/view/base.py index dbcf69f28cc..866b6e20f7b 100644 --- a/apiserver/plane/app/views/view/base.py +++ b/apiserver/plane/app/views/view/base.py @@ -22,6 +22,7 @@ from plane.app.permissions import ( ProjectEntityPermission, WorkspaceEntityPermission, + allow_permission, ) from plane.app.serializers import ( IssueViewSerializer, @@ -58,9 +59,6 @@ class WorkspaceViewViewSet(BaseViewSet): serializer_class = IssueViewSerializer model = IssueView - permission_classes = [ - WorkspaceEntityPermission, - ] def perform_create(self, serializer): workspace = Workspace.objects.get(slug=self.kwargs.get("slug")) @@ -77,7 +75,10 @@ def get_queryset(self): .order_by(self.request.GET.get("order_by", "-created_at")) .distinct() ) - + + @allow_permission( + roles=["ADMIN", "MEMBER", "VIEWER", "GUEST"], level="WORKSPACE" + ) def list(self, request, slug): queryset = self.get_queryset() fields = [ @@ -97,6 +98,9 @@ def list(self, request, slug): ).data return Response(views, status=status.HTTP_200_OK) + @allow_permission( + roles=[], level="WORKSPACE", creator=True, model=IssueView + ) def partial_update(self, request, slug, pk): with transaction.atomic(): workspace_view = IssueView.objects.select_for_update().get( @@ -130,6 +134,9 @@ def partial_update(self, request, slug, pk): serializer.errors, status=status.HTTP_400_BAD_REQUEST ) + @allow_permission( + roles=["ADMIN"], level="WORKSPACE", creator=True, model=IssueView + ) def destroy(self, request, slug, pk): workspace_view = IssueView.objects.get( pk=pk, @@ -176,10 +183,6 @@ def destroy(self, request, slug, pk): class WorkspaceViewIssuesViewSet(BaseViewSet): - permission_classes = [ - WorkspaceEntityPermission, - ] - def get_queryset(self): return ( Issue.issue_objects.annotate( @@ -251,6 +254,9 @@ def get_queryset(self): ) @method_decorator(gzip_page) + @allow_permission( + roles=["ADMIN", "MEMBER", "VIEWER", "GUEST"], level="WORKSPACE" + ) def list(self, request, slug): filters = issue_filters(request.query_params, "GET") order_by_param = request.GET.get("order_by", "-created_at") @@ -377,9 +383,6 @@ def list(self, request, slug): class IssueViewViewSet(BaseViewSet): serializer_class = IssueViewSerializer model = IssueView - permission_classes = [ - ProjectEntityPermission, - ] def perform_create(self, serializer): serializer.save( @@ -413,6 +416,7 @@ def get_queryset(self): .distinct() ) + allow_permission(roles=["ADMIN", "MEMBER", "VIEWER", "GUEST"]) def list(self, request, slug, project_id): queryset = self.get_queryset() if ProjectMember.objects.filter( @@ -433,6 +437,8 @@ def list(self, request, slug, project_id): ).data return Response(views, status=status.HTTP_200_OK) + allow_permission(roles=[], creator=True, model=IssueView) + def partial_update(self, request, slug, project_id, pk): with transaction.atomic(): issue_view = IssueView.objects.select_for_update().get( @@ -465,6 +471,8 @@ def partial_update(self, request, slug, project_id, pk): serializer.errors, status=status.HTTP_400_BAD_REQUEST ) + allow_permission(roles=["ADMIN"], creator=True, model=IssueView) + def destroy(self, request, slug, project_id, pk): project_view = IssueView.objects.get( pk=pk, @@ -509,6 +517,8 @@ def get_queryset(self): .select_related("view") ) + allow_permission(["ADMIN", "MEMBER"]) + def create(self, request, slug, project_id): _ = UserFavorite.objects.create( user=request.user, @@ -518,6 +528,8 @@ def create(self, request, slug, project_id): ) return Response(status=status.HTTP_204_NO_CONTENT) + allow_permission(["ADMIN", "MEMBER"]) + def destroy(self, request, slug, project_id, view_id): view_favorite = UserFavorite.objects.get( project=project_id, From 3151bce3a1dcbcfe57cccafe8fd8b4d3ed6dbf6e Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia Date: Wed, 14 Aug 2024 14:11:29 +0530 Subject: [PATCH 29/30] chore: create view empty state validation updated --- web/core/constants/empty-state.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/core/constants/empty-state.ts b/web/core/constants/empty-state.ts index 87f74362d83..984f6f96d7f 100644 --- a/web/core/constants/empty-state.ts +++ b/web/core/constants/empty-state.ts @@ -490,7 +490,7 @@ const emptyStateDetails = { }, }, accessType: "project", - access: EUserProjectRoles.MEMBER, + access: EUserProjectRoles.GUEST, }, // project pages [EmptyStateType.PROJECT_PAGE]: { From 0cdb0921b1d0c86fcc756ca6177ec68020ba6dd4 Mon Sep 17 00:00:00 2001 From: NarayanBavisetti Date: Wed, 14 Aug 2024 17:53:17 +0530 Subject: [PATCH 30/30] chore: created ENUM for permissions --- apiserver/plane/app/permissions/__init__.py | 2 +- apiserver/plane/app/permissions/base.py | 28 ++++++------- apiserver/plane/app/views/analytic/base.py | 18 ++++++--- apiserver/plane/app/views/cycle/archive.py | 8 ++-- apiserver/plane/app/views/cycle/base.py | 24 ++++++------ apiserver/plane/app/views/cycle/issue.py | 8 ++-- apiserver/plane/app/views/estimate/base.py | 14 ++++--- apiserver/plane/app/views/exporter/base.py | 6 +-- apiserver/plane/app/views/external/base.py | 10 ++--- apiserver/plane/app/views/inbox/base.py | 26 +++++++------ apiserver/plane/app/views/issue/activity.py | 4 +- apiserver/plane/app/views/issue/archive.py | 16 ++++---- apiserver/plane/app/views/issue/attachment.py | 9 +++-- apiserver/plane/app/views/issue/base.py | 22 ++++++----- apiserver/plane/app/views/issue/comment.py | 14 +++++-- apiserver/plane/app/views/issue/label.py | 10 ++--- apiserver/plane/app/views/module/base.py | 20 +++++----- apiserver/plane/app/views/module/issue.py | 13 +++---- .../plane/app/views/notification/base.py | 39 +++++++++++++------ apiserver/plane/app/views/page/base.py | 30 +++++++------- apiserver/plane/app/views/project/base.py | 19 +++++---- apiserver/plane/app/views/project/member.py | 12 +++--- apiserver/plane/app/views/view/base.py | 29 ++++++++------ apiserver/plane/app/views/webhook/base.py | 14 +++---- .../plane/app/views/workspace/favorite.py | 22 ++++++++--- 25 files changed, 238 insertions(+), 179 deletions(-) diff --git a/apiserver/plane/app/permissions/__init__.py b/apiserver/plane/app/permissions/__init__.py index 09e18f9055c..e453881441b 100644 --- a/apiserver/plane/app/permissions/__init__.py +++ b/apiserver/plane/app/permissions/__init__.py @@ -12,4 +12,4 @@ ProjectMemberPermission, ProjectLitePermission, ) -from .base import allow_permission \ No newline at end of file +from .base import allow_permission, ROLE \ No newline at end of file diff --git a/apiserver/plane/app/permissions/base.py b/apiserver/plane/app/permissions/base.py index 09d4e6f8b44..bb4f867a793 100644 --- a/apiserver/plane/app/permissions/base.py +++ b/apiserver/plane/app/permissions/base.py @@ -3,20 +3,16 @@ from rest_framework.response import Response from rest_framework import status +from enum import Enum -ROLE_VALUES = { - "ADMIN": 20, - "MEMBER": 15, - "VIEWER": 10, - "GUEST": 5, -} +class ROLE(Enum): + ADMIN = 20 + MEMBER = 15 + VIEWER = 10 + GUEST = 5 -def get_role_values(roles): - return [ROLE_VALUES.get(role.upper(), 0) for role in roles] - - -def allow_permission(roles, level="PROJECT", creator=False, model=None ): +def allow_permission(allowed_roles, level="PROJECT", creator=False, model=None): def decorator(view_func): @wraps(view_func) def _wrapped_view(instance, request, *args, **kwargs): @@ -29,12 +25,18 @@ def _wrapped_view(instance, request, *args, **kwargs): if obj: return view_func(instance, request, *args, **kwargs) + # Convert allowed_roles to their values if they are enum members + allowed_role_values = [ + role.value if isinstance(role, ROLE) else role + for role in allowed_roles + ] + # Check role permissions if level == "WORKSPACE": if WorkspaceMember.objects.filter( member=request.user, workspace__slug=kwargs["slug"], - role__in=get_role_values(roles), + role__in=allowed_role_values, is_active=True, ).exists(): return view_func(instance, request, *args, **kwargs) @@ -43,7 +45,7 @@ def _wrapped_view(instance, request, *args, **kwargs): member=request.user, workspace__slug=kwargs["slug"], project_id=kwargs["project_id"], - role__in=get_role_values(roles), + role__in=allowed_role_values, is_active=True, ).exists(): return view_func(instance, request, *args, **kwargs) diff --git a/apiserver/plane/app/views/analytic/base.py b/apiserver/plane/app/views/analytic/base.py index d620e77110a..b72935fc25d 100644 --- a/apiserver/plane/app/views/analytic/base.py +++ b/apiserver/plane/app/views/analytic/base.py @@ -15,12 +15,14 @@ from plane.db.models import AnalyticView, Issue, Workspace from plane.utils.analytics_plot import build_graph_plot from plane.utils.issue_filters import issue_filters -from plane.app.permissions import allow_permission +from plane.app.permissions import allow_permission, ROLE class AnalyticsEndpoint(BaseAPIView): - @allow_permission(["ADMIN", "MEMBER", "VIEWER"], level="WORKSPACE") + @allow_permission( + [ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER], level="WORKSPACE" + ) def get(self, request, slug): x_axis = request.GET.get("x_axis", False) y_axis = request.GET.get("y_axis", False) @@ -200,7 +202,9 @@ def get_queryset(self): class SavedAnalyticEndpoint(BaseAPIView): - @allow_permission(["ADMIN", "MEMBER", "VIEWER"], level="WORKSPACE") + @allow_permission( + [ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER], level="WORKSPACE" + ) def get(self, request, slug, analytic_id): analytic_view = AnalyticView.objects.get( pk=analytic_id, workspace__slug=slug @@ -231,7 +235,9 @@ def get(self, request, slug, analytic_id): class ExportAnalyticsEndpoint(BaseAPIView): - @allow_permission(["ADMIN", "MEMBER", "VIEWER"], level="WORKSPACE") + @allow_permission( + [ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER], level="WORKSPACE" + ) def post(self, request, slug): x_axis = request.data.get("x_axis", False) y_axis = request.data.get("y_axis", False) @@ -296,7 +302,9 @@ def post(self, request, slug): class DefaultAnalyticsEndpoint(BaseAPIView): - @allow_permission(["ADMIN", "MEMBER", "VIEWER", "GUEST"], level="WORKSPACE") + @allow_permission( + [ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER, ROLE.GUEST], level="WORKSPACE" + ) def get(self, request, slug): filters = issue_filters(request.GET, "GET") base_issues = Issue.issue_objects.filter( diff --git a/apiserver/plane/app/views/cycle/archive.py b/apiserver/plane/app/views/cycle/archive.py index 789e7906b6c..5f7f1434738 100644 --- a/apiserver/plane/app/views/cycle/archive.py +++ b/apiserver/plane/app/views/cycle/archive.py @@ -24,7 +24,7 @@ # Third party imports from rest_framework import status from rest_framework.response import Response -from plane.app.permissions import allow_permission +from plane.app.permissions import allow_permission, ROLE from plane.db.models import Cycle, UserFavorite, Issue, Label, User, Project from plane.utils.analytics_plot import burndown_plot @@ -288,7 +288,7 @@ def get_queryset(self): .distinct() ) - @allow_permission(["ADMIN", "MEMBER", "VIEWER"]) + @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER]) def get(self, request, slug, project_id, pk=None): if pk is None: queryset = ( @@ -593,7 +593,7 @@ def get(self, request, slug, project_id, pk=None): status=status.HTTP_200_OK, ) - @allow_permission(["ADMIN", "MEMBER"]) + @allow_permission([ROLE.ADMIN, ROLE.MEMBER]) def post(self, request, slug, project_id, cycle_id): cycle = Cycle.objects.get( pk=cycle_id, project_id=project_id, workspace__slug=slug @@ -612,7 +612,7 @@ def post(self, request, slug, project_id, cycle_id): status=status.HTTP_200_OK, ) - @allow_permission(["ADMIN", "MEMBER"]) + @allow_permission([ROLE.ADMIN, ROLE.MEMBER]) def delete(self, request, slug, project_id, cycle_id): cycle = Cycle.objects.get( pk=cycle_id, project_id=project_id, workspace__slug=slug diff --git a/apiserver/plane/app/views/cycle/base.py b/apiserver/plane/app/views/cycle/base.py index 2b3e5bdf491..79d87c6a1c2 100644 --- a/apiserver/plane/app/views/cycle/base.py +++ b/apiserver/plane/app/views/cycle/base.py @@ -29,7 +29,7 @@ from rest_framework import status from rest_framework.response import Response from plane.app.permissions import ( - allow_permission, + allow_permission, ROLE ) from plane.app.serializers import ( CycleSerializer, @@ -315,7 +315,7 @@ def get_queryset(self): .distinct() ) - @allow_permission(["ADMIN", "MEMBER", "VIEWER", "GUEST"]) + @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER, ROLE.GUEST]) def list(self, request, slug, project_id): queryset = self.get_queryset().filter(archived_at__isnull=True) cycle_view = request.GET.get("cycle_view", "all") @@ -602,7 +602,7 @@ def list(self, request, slug, project_id): ) return Response(data, status=status.HTTP_200_OK) - @allow_permission(["ADMIN", "MEMBER"]) + @allow_permission([ROLE.ADMIN, ROLE.MEMBER]) def create(self, request, slug, project_id): if ( request.data.get("start_date", None) is None @@ -676,7 +676,7 @@ def create(self, request, slug, project_id): status=status.HTTP_400_BAD_REQUEST, ) - @allow_permission(["ADMIN", "MEMBER"]) + @allow_permission([ROLE.ADMIN, ROLE.MEMBER]) def partial_update(self, request, slug, project_id, pk): queryset = self.get_queryset().filter( workspace__slug=slug, project_id=project_id, pk=pk @@ -764,7 +764,7 @@ def partial_update(self, request, slug, project_id, pk): return Response(cycle, status=status.HTTP_200_OK) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - @allow_permission(["ADMIN", "MEMBER", "VIEWER"]) + @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER]) def retrieve(self, request, slug, project_id, pk): queryset = ( self.get_queryset().filter(archived_at__isnull=True).filter(pk=pk) @@ -1033,7 +1033,7 @@ def retrieve(self, request, slug, project_id, pk): status=status.HTTP_200_OK, ) - @allow_permission(["ADMIN"], creator=True, model=Cycle) + @allow_permission([ROLE.ADMIN], creator=True, model=Cycle) def destroy(self, request, slug, project_id, pk): cycle = Cycle.objects.get( workspace__slug=slug, project_id=project_id, pk=pk @@ -1093,7 +1093,7 @@ def destroy(self, request, slug, project_id, pk): class CycleDateCheckEndpoint(BaseAPIView): - @allow_permission(["ADMIN", "MEMBER"]) + @allow_permission([ROLE.ADMIN, ROLE.MEMBER]) def post(self, request, slug, project_id): start_date = request.data.get("start_date", False) end_date = request.data.get("end_date", False) @@ -1137,7 +1137,7 @@ def get_queryset(self): .select_related("cycle", "cycle__owned_by") ) - @allow_permission(["ADMIN", "MEMBER"]) + @allow_permission([ROLE.ADMIN, ROLE.MEMBER]) def create(self, request, slug, project_id): _ = UserFavorite.objects.create( project_id=project_id, @@ -1147,7 +1147,7 @@ def create(self, request, slug, project_id): ) return Response(status=status.HTTP_204_NO_CONTENT) - @allow_permission(["ADMIN", "MEMBER"]) + @allow_permission([ROLE.ADMIN, ROLE.MEMBER]) def destroy(self, request, slug, project_id, cycle_id): cycle_favorite = UserFavorite.objects.get( project=project_id, @@ -1162,7 +1162,7 @@ def destroy(self, request, slug, project_id, cycle_id): class TransferCycleIssueEndpoint(BaseAPIView): - @allow_permission(["ADMIN", "MEMBER"]) + @allow_permission([ROLE.ADMIN, ROLE.MEMBER]) def post(self, request, slug, project_id, cycle_id): new_cycle_id = request.data.get("new_cycle_id", False) @@ -1573,7 +1573,7 @@ def post(self, request, slug, project_id, cycle_id): class CycleUserPropertiesEndpoint(BaseAPIView): - @allow_permission(["ADMIN", "MEMBER", "VIEWER", "GUEST"]) + @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER, ROLE.GUEST]) def patch(self, request, slug, project_id, cycle_id): cycle_properties = CycleUserProperties.objects.get( user=request.user, @@ -1596,7 +1596,7 @@ def patch(self, request, slug, project_id, cycle_id): serializer = CycleUserPropertiesSerializer(cycle_properties) return Response(serializer.data, status=status.HTTP_201_CREATED) - @allow_permission(["ADMIN", "MEMBER", "VIEWER", "GUEST"]) + @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER, ROLE.GUEST]) def get(self, request, slug, project_id, cycle_id): cycle_properties, _ = CycleUserProperties.objects.get_or_create( user=request.user, diff --git a/apiserver/plane/app/views/cycle/issue.py b/apiserver/plane/app/views/cycle/issue.py index adb4dbcaa40..99179a92c2d 100644 --- a/apiserver/plane/app/views/cycle/issue.py +++ b/apiserver/plane/app/views/cycle/issue.py @@ -36,7 +36,7 @@ GroupedOffsetPaginator, SubGroupedOffsetPaginator, ) -from plane.app.permissions import allow_permission +from plane.app.permissions import allow_permission, ROLE class CycleIssueViewSet(BaseViewSet): @@ -80,7 +80,7 @@ def get_queryset(self): ) @method_decorator(gzip_page) - @allow_permission(["ADMIN", "MEMBER", "VIEWER"]) + @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER]) def list(self, request, slug, project_id, cycle_id): order_by_param = request.GET.get("order_by", "created_at") filters = issue_filters(request.query_params, "GET") @@ -227,7 +227,7 @@ def list(self, request, slug, project_id, cycle_id): ), ) - @allow_permission(["ADMIN", "MEMBER"]) + @allow_permission([ROLE.ADMIN, ROLE.MEMBER]) def create(self, request, slug, project_id, cycle_id): issues = request.data.get("issues", []) @@ -323,7 +323,7 @@ def create(self, request, slug, project_id, cycle_id): ) return Response({"message": "success"}, status=status.HTTP_201_CREATED) - @allow_permission(["ADMIN", "MEMBER"]) + @allow_permission([ROLE.ADMIN, ROLE.MEMBER]) def destroy(self, request, slug, project_id, cycle_id, issue_id): cycle_issue = CycleIssue.objects.filter( issue_id=issue_id, diff --git a/apiserver/plane/app/views/estimate/base.py b/apiserver/plane/app/views/estimate/base.py index 0918b3547c1..cd80c62999a 100644 --- a/apiserver/plane/app/views/estimate/base.py +++ b/apiserver/plane/app/views/estimate/base.py @@ -7,7 +7,11 @@ # Module imports from ..base import BaseViewSet, BaseAPIView -from plane.app.permissions import ProjectEntityPermission, allow_permission +from plane.app.permissions import ( + ProjectEntityPermission, + allow_permission, + ROLE, +) from plane.db.models import Project, Estimate, EstimatePoint, Issue from plane.app.serializers import ( EstimateSerializer, @@ -24,7 +28,7 @@ def generate_random_name(length=10): class ProjectEstimatePointEndpoint(BaseAPIView): - @allow_permission(["ADMIN", "MEMBER", "VIEWER"]) + @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER]) def get(self, request, slug, project_id): project = Project.objects.get(workspace__slug=slug, pk=project_id) if project.estimate_id is not None: @@ -188,7 +192,7 @@ def destroy(self, request, slug, project_id, estimate_id): class EstimatePointEndpoint(BaseViewSet): - @allow_permission(["ADMIN", "MEMBER"]) + @allow_permission([ROLE.ADMIN, ROLE.MEMBER]) def create(self, request, slug, project_id, estimate_id): # TODO: add a key validation if the same key already exists if not request.data.get("key") or not request.data.get("value"): @@ -207,7 +211,7 @@ def create(self, request, slug, project_id, estimate_id): serializer = EstimatePointSerializer(estimate_point).data return Response(serializer, status=status.HTTP_200_OK) - @allow_permission(["ADMIN", "MEMBER"]) + @allow_permission([ROLE.ADMIN, ROLE.MEMBER]) def partial_update( self, request, slug, project_id, estimate_id, estimate_point_id ): @@ -228,7 +232,7 @@ def partial_update( serializer.save() return Response(serializer.data, status=status.HTTP_200_OK) - @allow_permission(["ADMIN", "MEMBER"]) + @allow_permission([ROLE.ADMIN, ROLE.MEMBER]) def destroy( self, request, slug, project_id, estimate_id, estimate_point_id ): diff --git a/apiserver/plane/app/views/exporter/base.py b/apiserver/plane/app/views/exporter/base.py index 9bb6f13f8ff..50f9870d097 100644 --- a/apiserver/plane/app/views/exporter/base.py +++ b/apiserver/plane/app/views/exporter/base.py @@ -2,7 +2,7 @@ from rest_framework import status from rest_framework.response import Response -from plane.app.permissions import allow_permission +from plane.app.permissions import allow_permission, ROLE from plane.app.serializers import ExporterHistorySerializer from plane.bgtasks.export_task import issue_export_task from plane.db.models import ExporterHistory, Project, Workspace @@ -15,7 +15,7 @@ class ExportIssuesEndpoint(BaseAPIView): model = ExporterHistory serializer_class = ExporterHistorySerializer - @allow_permission(roles=["ADMIN"], level="WORKSPACE") + @allow_permission(allowed_roles=[ROLE.ADMIN], level="WORKSPACE") def post(self, request, slug): # Get the workspace workspace = Workspace.objects.get(slug=slug) @@ -62,7 +62,7 @@ def post(self, request, slug): status=status.HTTP_400_BAD_REQUEST, ) - @allow_permission(roles=["ADMIN"], level="WORKSPACE") + @allow_permission(allowed_roles=[ROLE.ADMIN], level="WORKSPACE") def get(self, request, slug): exporter_history = ExporterHistory.objects.filter( workspace__slug=slug, diff --git a/apiserver/plane/app/views/external/base.py b/apiserver/plane/app/views/external/base.py index bfc10352a8a..6ae3f37ba8e 100644 --- a/apiserver/plane/app/views/external/base.py +++ b/apiserver/plane/app/views/external/base.py @@ -11,9 +11,7 @@ # Module imports from ..base import BaseAPIView -from plane.app.permissions import ( - allow_permission, -) +from plane.app.permissions import allow_permission, ROLE from plane.db.models import Workspace, Project from plane.app.serializers import ( ProjectLiteSerializer, @@ -24,7 +22,7 @@ class GPTIntegrationEndpoint(BaseAPIView): - @allow_permission(["ADMIN", "MEMBER"]) + @allow_permission([ROLE.ADMIN, ROLE.MEMBER]) def post(self, request, slug, project_id): OPENAI_API_KEY, GPT_ENGINE = get_configuration_value( [ @@ -85,7 +83,9 @@ def post(self, request, slug, project_id): class WorkspaceGPTIntegrationEndpoint(BaseAPIView): - @allow_permission(roles=["ADMIN", "MEMBER"], level="WORKSPACE") + @allow_permission( + allowed_roles=[ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE" + ) def post(self, request, slug): OPENAI_API_KEY, GPT_ENGINE = get_configuration_value( [ diff --git a/apiserver/plane/app/views/inbox/base.py b/apiserver/plane/app/views/inbox/base.py index d9d3fd00150..78043e5dc93 100644 --- a/apiserver/plane/app/views/inbox/base.py +++ b/apiserver/plane/app/views/inbox/base.py @@ -17,7 +17,7 @@ # Module imports from ..base import BaseViewSet from plane.app.permissions import ( - allow_permission, + allow_permission, ROLE ) from plane.db.models import ( Inbox, @@ -62,7 +62,7 @@ def get_queryset(self): .select_related("workspace", "project") ) - @allow_permission(["ADMIN", "MEMBER"]) + @allow_permission([ROLE.ADMIN, ROLE.MEMBER]) def list(self, request, slug, project_id): inbox = self.get_queryset().first() return Response( @@ -70,11 +70,11 @@ def list(self, request, slug, project_id): status=status.HTTP_200_OK, ) - @allow_permission(["ADMIN", "MEMBER"]) + @allow_permission([ROLE.ADMIN, ROLE.MEMBER]) def perform_create(self, serializer): serializer.save(project_id=self.kwargs.get("project_id")) - @allow_permission(["ADMIN", "MEMBER"]) + @allow_permission([ROLE.ADMIN, ROLE.MEMBER]) def destroy(self, request, slug, project_id, pk): inbox = Inbox.objects.filter( workspace__slug=slug, project_id=project_id, pk=pk @@ -167,7 +167,7 @@ def get_queryset(self): ) ).distinct() - @allow_permission(["ADMIN", "MEMBER", "VIEWER", "GUEST"]) + @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER, ROLE.GUEST]) def list(self, request, slug, project_id): inbox_id = Inbox.objects.filter( workspace__slug=slug, project_id=project_id @@ -218,7 +218,7 @@ def list(self, request, slug, project_id): ).data, ) - @allow_permission(["ADMIN", "MEMBER", "GUEST"]) + @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST]) def create(self, request, slug, project_id): if not request.data.get("issue", {}).get("name", False): return Response( @@ -321,7 +321,7 @@ def create(self, request, slug, project_id): serializer.errors, status=status.HTTP_400_BAD_REQUEST ) - @allow_permission(["ADMIN", "MEMBER", "GUEST"]) + @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST]) def partial_update(self, request, slug, project_id, pk): inbox_id = Inbox.objects.filter( workspace__slug=slug, project_id=project_id @@ -516,7 +516,11 @@ def partial_update(self, request, slug, project_id, pk): serializer = InboxIssueDetailSerializer(inbox_issue).data return Response(serializer, status=status.HTTP_200_OK) - @allow_permission(roles=["ADMIN", "MEMBER", "VIEWER"], creator=True, model=Issue) + @allow_permission( + allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER], + creator=True, + model=Issue, + ) def retrieve(self, request, slug, project_id, pk): inbox_id = Inbox.objects.filter( workspace__slug=slug, project_id=project_id @@ -545,9 +549,7 @@ def retrieve(self, request, slug, project_id, pk): Value([], output_field=ArrayField(UUIDField())), ), ) - .get( - inbox_id=inbox_id.id, issue_id=pk, project_id=project_id - ) + .get(inbox_id=inbox_id.id, issue_id=pk, project_id=project_id) ) issue = InboxIssueDetailSerializer(inbox_issue).data return Response( @@ -555,7 +557,7 @@ def retrieve(self, request, slug, project_id, pk): status=status.HTTP_200_OK, ) - @allow_permission(roles=["ADMIN"], creator=True, model=Issue) + @allow_permission(allowed_roles=[ROLE.ADMIN], creator=True, model=Issue) def destroy(self, request, slug, project_id, pk): inbox_id = Inbox.objects.filter( workspace__slug=slug, project_id=project_id diff --git a/apiserver/plane/app/views/issue/activity.py b/apiserver/plane/app/views/issue/activity.py index fad879636a5..0475cb9ec9b 100644 --- a/apiserver/plane/app/views/issue/activity.py +++ b/apiserver/plane/app/views/issue/activity.py @@ -19,7 +19,7 @@ IssueActivitySerializer, IssueCommentSerializer, ) -from plane.app.permissions import ProjectEntityPermission, allow_permission +from plane.app.permissions import ProjectEntityPermission, allow_permission, ROLE from plane.db.models import ( IssueActivity, IssueComment, @@ -33,7 +33,7 @@ class IssueActivityEndpoint(BaseAPIView): ] @method_decorator(gzip_page) - @allow_permission(["ADMIN", "MEMBER", "GUEST", "VIEWER"]) + @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST, ROLE.VIEWER]) def get(self, request, slug, project_id, issue_id): filters = {} if request.GET.get("created_at__gt", None) is not None: diff --git a/apiserver/plane/app/views/issue/archive.py b/apiserver/plane/app/views/issue/archive.py index aefafbc13d0..0457b388c60 100644 --- a/apiserver/plane/app/views/issue/archive.py +++ b/apiserver/plane/app/views/issue/archive.py @@ -25,7 +25,7 @@ from plane.app.serializers import ( IssueFlatSerializer, IssueSerializer, - IssueDetailSerializer + IssueDetailSerializer, ) from plane.bgtasks.issue_activites_task import issue_activity from plane.db.models import ( @@ -46,7 +46,7 @@ GroupedOffsetPaginator, SubGroupedOffsetPaginator, ) -from plane.app.permissions import allow_permission +from plane.app.permissions import allow_permission, ROLE # Module imports from .. import BaseViewSet, BaseAPIView @@ -96,7 +96,7 @@ def get_queryset(self): ) @method_decorator(gzip_page) - @allow_permission(["ADMIN", "MEMBER", "VIEWER"]) + @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER]) def list(self, request, slug, project_id): filters = issue_filters(request.query_params, "GET") show_sub_issues = request.GET.get("show_sub_issues", "true") @@ -212,7 +212,7 @@ def list(self, request, slug, project_id): ), ) - @allow_permission(["ADMIN", "MEMBER", "VIEWER"]) + @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER]) def retrieve(self, request, slug, project_id, pk=None): issue = ( self.get_queryset() @@ -256,7 +256,7 @@ def retrieve(self, request, slug, project_id, pk=None): serializer = IssueDetailSerializer(issue, expand=self.expand) return Response(serializer.data, status=status.HTTP_200_OK) - @allow_permission(["ADMIN", "MEMBER"]) + @allow_permission([ROLE.ADMIN, ROLE.MEMBER]) def archive(self, request, slug, project_id, pk=None): issue = Issue.issue_objects.get( workspace__slug=slug, @@ -295,7 +295,7 @@ def archive(self, request, slug, project_id, pk=None): {"archived_at": str(issue.archived_at)}, status=status.HTTP_200_OK ) - @allow_permission(["ADMIN", "MEMBER"]) + @allow_permission([ROLE.ADMIN, ROLE.MEMBER]) def unarchive(self, request, slug, project_id, pk=None): issue = Issue.objects.get( workspace__slug=slug, @@ -327,7 +327,7 @@ class BulkArchiveIssuesEndpoint(BaseAPIView): ProjectEntityPermission, ] - @allow_permission(["ADMIN", "MEMBER"]) + @allow_permission([ROLE.ADMIN, ROLE.MEMBER]) def post(self, request, slug, project_id): issue_ids = request.data.get("issue_ids", []) @@ -346,7 +346,7 @@ def post(self, request, slug, project_id): return Response( { "error_code": 4091, - "error_message": "INVALID_ARCHIVE_STATE_GROUP" + "error_message": "INVALID_ARCHIVE_STATE_GROUP", }, status=status.HTTP_400_BAD_REQUEST, ) diff --git a/apiserver/plane/app/views/issue/attachment.py b/apiserver/plane/app/views/issue/attachment.py index 382ef8ee00a..baccfa7b0c9 100644 --- a/apiserver/plane/app/views/issue/attachment.py +++ b/apiserver/plane/app/views/issue/attachment.py @@ -15,14 +15,15 @@ from plane.app.serializers import IssueAttachmentSerializer from plane.db.models import IssueAttachment from plane.bgtasks.issue_activites_task import issue_activity -from plane.app.permissions import allow_permission +from plane.app.permissions import allow_permission, ROLE + class IssueAttachmentEndpoint(BaseAPIView): serializer_class = IssueAttachmentSerializer model = IssueAttachment parser_classes = (MultiPartParser, FormParser) - @allow_permission(["ADMIN", "MEMBER", "GUEST"]) + @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST]) def post(self, request, slug, project_id, issue_id): serializer = IssueAttachmentSerializer(data=request.data) if serializer.is_valid(): @@ -44,7 +45,7 @@ def post(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(["ADMIN"], creator=True, model=IssueAttachment) + @allow_permission([ROLE.ADMIN], creator=True, model=IssueAttachment) def delete(self, request, slug, project_id, issue_id, pk): issue_attachment = IssueAttachment.objects.get(pk=pk) issue_attachment.asset.delete(save=False) @@ -63,7 +64,7 @@ def delete(self, request, slug, project_id, issue_id, pk): return Response(status=status.HTTP_204_NO_CONTENT) - @allow_permission(["ADMIN", "MEMBER", "GUEST", "VIEWER"]) + @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST, ROLE.VIEWER]) def get(self, request, slug, project_id, issue_id): issue_attachments = IssueAttachment.objects.filter( issue_id=issue_id, workspace__slug=slug, project_id=project_id diff --git a/apiserver/plane/app/views/issue/base.py b/apiserver/plane/app/views/issue/base.py index e62ad6e39d1..c7ef85b9252 100644 --- a/apiserver/plane/app/views/issue/base.py +++ b/apiserver/plane/app/views/issue/base.py @@ -25,7 +25,7 @@ from rest_framework.response import Response # Module imports -from plane.app.permissions import allow_permission +from plane.app.permissions import allow_permission, ROLE from plane.app.serializers import ( IssueCreateSerializer, IssueDetailSerializer, @@ -60,7 +60,7 @@ class IssueListEndpoint(BaseAPIView): - @allow_permission(["ADMIN", "MEMBER", "VIEWER", "GUEST"]) + @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER, ROLE.GUEST]) def get(self, request, slug, project_id): issue_ids = request.GET.get("issues", False) @@ -222,7 +222,7 @@ def get_queryset(self): ).distinct() @method_decorator(gzip_page) - @allow_permission(["ADMIN", "MEMBER", "VIEWER", "GUEST"]) + @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER, ROLE.GUEST]) def list(self, request, slug, project_id): filters = issue_filters(request.query_params, "GET") order_by_param = request.GET.get("order_by", "-created_at") @@ -337,7 +337,7 @@ def list(self, request, slug, project_id): ), ) - @allow_permission(["ADMIN", "MEMBER"]) + @allow_permission([ROLE.ADMIN, ROLE.MEMBER]) def create(self, request, slug, project_id): project = Project.objects.get(pk=project_id) @@ -412,7 +412,9 @@ def create(self, request, slug, project_id): return Response(issue, status=status.HTTP_201_CREATED) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - @allow_permission(["ADMIN", "MEMBER", "VIEWER"], creator=True, model=Issue) + @allow_permission( + [ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER], creator=True, model=Issue + ) def retrieve(self, request, slug, project_id, pk=None): issue = ( self.get_queryset() @@ -485,7 +487,7 @@ def retrieve(self, request, slug, project_id, pk=None): serializer = IssueDetailSerializer(issue, expand=self.expand) return Response(serializer.data, status=status.HTTP_200_OK) - @allow_permission(["ADMIN", "MEMBER", "GUEST"]) + @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST]) def partial_update(self, request, slug, project_id, pk=None): issue = ( self.get_queryset() @@ -551,7 +553,7 @@ def partial_update(self, request, slug, project_id, pk=None): return Response(status=status.HTTP_204_NO_CONTENT) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - @allow_permission(["ADMIN"], creator=True, model=Issue) + @allow_permission([ROLE.ADMIN], creator=True, model=Issue) def destroy(self, request, slug, project_id, pk=None): issue = Issue.objects.get( workspace__slug=slug, project_id=project_id, pk=pk @@ -574,7 +576,7 @@ def destroy(self, request, slug, project_id, pk=None): class IssueUserDisplayPropertyEndpoint(BaseAPIView): - @allow_permission(["ADMIN", "MEMBER", "GUEST", "VIEWER"]) + @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST, ROLE.VIEWER]) def patch(self, request, slug, project_id): issue_property = IssueUserProperty.objects.get( user=request.user, @@ -594,7 +596,7 @@ def patch(self, request, slug, project_id): serializer = IssueUserPropertySerializer(issue_property) return Response(serializer.data, status=status.HTTP_201_CREATED) - @allow_permission(["ADMIN", "MEMBER", "GUEST", "VIEWER"]) + @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST, ROLE.VIEWER]) def get(self, request, slug, project_id): issue_property, _ = IssueUserProperty.objects.get_or_create( user=request.user, project_id=project_id @@ -605,7 +607,7 @@ def get(self, request, slug, project_id): class BulkDeleteIssuesEndpoint(BaseAPIView): - @allow_permission(["ADMIN"]) + @allow_permission([ROLE.ADMIN]) def delete(self, request, slug, project_id): issue_ids = request.data.get("issue_ids", []) diff --git a/apiserver/plane/app/views/issue/comment.py b/apiserver/plane/app/views/issue/comment.py index 2fa7519785f..1e15bbb6d31 100644 --- a/apiserver/plane/app/views/issue/comment.py +++ b/apiserver/plane/app/views/issue/comment.py @@ -16,7 +16,7 @@ IssueCommentSerializer, CommentReactionSerializer, ) -from plane.app.permissions import ProjectLitePermission, allow_permission +from plane.app.permissions import ProjectLitePermission, allow_permission, ROLE from plane.db.models import ( IssueComment, ProjectMember, @@ -63,7 +63,7 @@ def get_queryset(self): .distinct() ) - @allow_permission(["ADMIN", "MEMBER", "GUEST", "VIEWER"]) + @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST, ROLE.VIEWER]) def create(self, request, slug, project_id, issue_id): serializer = IssueCommentSerializer(data=request.data) if serializer.is_valid(): @@ -88,7 +88,11 @@ 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(roles=["ADMIN", "MEMBER"], creator=True, model=IssueComment) + @allow_permission( + allowed_roles=[ROLE.ADMIN, ROLE.MEMBER], + creator=True, + model=IssueComment, + ) def partial_update(self, request, slug, project_id, issue_id, pk): issue_comment = IssueComment.objects.get( workspace__slug=slug, @@ -120,7 +124,9 @@ def partial_update(self, request, slug, project_id, issue_id, pk): return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - @allow_permission(roles=["ADMIN"], creator=True, model=IssueComment) + @allow_permission( + allowed_roles=[ROLE.ADMIN], creator=True, model=IssueComment + ) def destroy(self, request, slug, project_id, issue_id, pk): issue_comment = IssueComment.objects.get( workspace__slug=slug, diff --git a/apiserver/plane/app/views/issue/label.py b/apiserver/plane/app/views/issue/label.py index 6fbf71cc67f..95ee2a8471b 100644 --- a/apiserver/plane/app/views/issue/label.py +++ b/apiserver/plane/app/views/issue/label.py @@ -11,7 +11,7 @@ # Module imports from .. import BaseViewSet, BaseAPIView from plane.app.serializers import LabelSerializer -from plane.app.permissions import allow_permission, ProjectBasePermission +from plane.app.permissions import allow_permission, ProjectBasePermission, ROLE from plane.db.models import ( Project, Label, @@ -43,7 +43,7 @@ def get_queryset(self): @invalidate_cache( path="/api/workspaces/:slug/labels/", url_params=True, user=False ) - @allow_permission(["ADMIN", "MEMBER"]) + @allow_permission([ROLE.ADMIN, ROLE.MEMBER]) def create(self, request, slug, project_id): try: serializer = LabelSerializer(data=request.data) @@ -66,20 +66,20 @@ def create(self, request, slug, project_id): @invalidate_cache( path="/api/workspaces/:slug/labels/", url_params=True, user=False ) - @allow_permission(["ADMIN", "MEMBER"]) + @allow_permission([ROLE.ADMIN, ROLE.MEMBER]) def partial_update(self, request, *args, **kwargs): return super().partial_update(request, *args, **kwargs) @invalidate_cache( path="/api/workspaces/:slug/labels/", url_params=True, user=False ) - @allow_permission(["ADMIN", "MEMBER"]) + @allow_permission([ROLE.ADMIN, ROLE.MEMBER]) def destroy(self, request, *args, **kwargs): return super().destroy(request, *args, **kwargs) class BulkCreateIssueLabelsEndpoint(BaseAPIView): - @allow_permission(["ADMIN"]) + @allow_permission([ROLE.ADMIN]) def post(self, request, slug, project_id): label_data = request.data.get("label_data", []) project = Project.objects.get(pk=project_id) diff --git a/apiserver/plane/app/views/module/base.py b/apiserver/plane/app/views/module/base.py index 952774b584f..512ee6e9b18 100644 --- a/apiserver/plane/app/views/module/base.py +++ b/apiserver/plane/app/views/module/base.py @@ -31,6 +31,7 @@ from plane.app.permissions import ( ProjectEntityPermission, allow_permission, + ROLE, ) from plane.app.serializers import ( @@ -49,7 +50,6 @@ ModuleLink, ModuleUserProperties, Project, - ProjectMember, ) from plane.utils.analytics_plot import burndown_plot from plane.utils.user_timezone_converter import user_timezone_converter @@ -59,9 +59,6 @@ class ModuleViewSet(BaseViewSet): model = Module - # permission_classes = [ - # ProjectEntityPermission, - # ] webhook_event = "module" def get_serializer_class(self): @@ -319,7 +316,8 @@ def get_queryset(self): .order_by("-is_favorite", "-created_at") ) - allow_permission(["ADMIN", "MEMBER", "VIEWER"]) + allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER]) + def create(self, request, slug, project_id): project = Project.objects.get(workspace__slug=slug, pk=project_id) serializer = ModuleWriteSerializer( @@ -382,7 +380,7 @@ def create(self, request, slug, project_id): return Response(module, status=status.HTTP_201_CREATED) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - allow_permission(["ADMIN", "MEMBER", "VIEWER", "GUEST"]) + allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER, ROLE.GUEST]) def list(self, request, slug, project_id): queryset = self.get_queryset().filter(archived_at__isnull=True) @@ -431,7 +429,7 @@ def list(self, request, slug, project_id): ) return Response(modules, status=status.HTTP_200_OK) - allow_permission(["ADMIN", "MEMBER", "VIEWER"]) + allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER]) def retrieve(self, request, slug, project_id, pk): queryset = ( @@ -677,7 +675,7 @@ def retrieve(self, request, slug, project_id, pk): status=status.HTTP_200_OK, ) - @allow_permission(["ADMIN", "MEMBER"]) + @allow_permission([ROLE.ADMIN, ROLE.MEMBER]) def partial_update(self, request, slug, project_id, pk): module = self.get_queryset().filter(pk=pk) @@ -747,7 +745,7 @@ def partial_update(self, request, slug, project_id, pk): return Response(module, status=status.HTTP_200_OK) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - @allow_permission(["ADMIN"], creator=True, model=Module) + @allow_permission([ROLE.ADMIN], creator=True, model=Module) def destroy(self, request, slug, project_id, pk): module = Module.objects.get( workspace__slug=slug, project_id=project_id, pk=pk @@ -854,7 +852,7 @@ def destroy(self, request, slug, project_id, module_id): class ModuleUserPropertiesEndpoint(BaseAPIView): - @allow_permission(["ADMIN", "MEMBER", "VIEWER", "GUEST"]) + @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER, ROLE.GUEST]) def patch(self, request, slug, project_id, module_id): module_properties = ModuleUserProperties.objects.get( user=request.user, @@ -877,7 +875,7 @@ def patch(self, request, slug, project_id, module_id): serializer = ModuleUserPropertiesSerializer(module_properties) return Response(serializer.data, status=status.HTTP_201_CREATED) - @allow_permission(["ADMIN", "MEMBER", "VIEWER", "GUEST"]) + @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER, ROLE.GUEST]) def get(self, request, slug, project_id, module_id): module_properties, _ = ModuleUserProperties.objects.get_or_create( user=request.user, diff --git a/apiserver/plane/app/views/module/issue.py b/apiserver/plane/app/views/module/issue.py index d5617854a6b..2b750e0e336 100644 --- a/apiserver/plane/app/views/module/issue.py +++ b/apiserver/plane/app/views/module/issue.py @@ -17,9 +17,7 @@ from rest_framework import status from rest_framework.response import Response -from plane.app.permissions import ( - allow_permission, -) +from plane.app.permissions import allow_permission, ROLE from plane.app.serializers import ( ModuleIssueSerializer, ) @@ -46,6 +44,7 @@ # Module imports from .. import BaseViewSet + class ModuleIssueViewSet(BaseViewSet): serializer_class = ModuleIssueSerializer model = ModuleIssue @@ -92,7 +91,7 @@ def get_queryset(self): ).distinct() @method_decorator(gzip_page) - @allow_permission(["ADMIN", "MEMBER", "VIEWER"]) + @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER]) def list(self, request, slug, project_id, module_id): filters = issue_filters(request.query_params, "GET") issue_queryset = self.get_queryset().filter(**filters) @@ -200,7 +199,7 @@ def list(self, request, slug, project_id, module_id): ), ) - @allow_permission(["ADMIN", "MEMBER"]) + @allow_permission([ROLE.ADMIN, ROLE.MEMBER]) # create multiple issues inside a module def create_module_issues(self, request, slug, project_id, module_id): issues = request.data.get("issues", []) @@ -242,7 +241,7 @@ def create_module_issues(self, request, slug, project_id, module_id): ] return Response({"message": "success"}, status=status.HTTP_201_CREATED) - @allow_permission(["ADMIN", "MEMBER"]) + @allow_permission([ROLE.ADMIN, ROLE.MEMBER]) # add multiple module inside an issue and remove multiple modules from an issue def create_issue_modules(self, request, slug, project_id, issue_id): modules = request.data.get("modules", []) @@ -305,7 +304,7 @@ def create_issue_modules(self, request, slug, project_id, issue_id): return Response({"message": "success"}, status=status.HTTP_201_CREATED) - @allow_permission(["ADMIN", "MEMBER"]) + @allow_permission([ROLE.ADMIN, ROLE.MEMBER]) def destroy(self, request, slug, project_id, module_id, issue_id): module_issue = ModuleIssue.objects.filter( workspace__slug=slug, diff --git a/apiserver/plane/app/views/notification/base.py b/apiserver/plane/app/views/notification/base.py index f6e1e3a6615..9d664c6c42e 100644 --- a/apiserver/plane/app/views/notification/base.py +++ b/apiserver/plane/app/views/notification/base.py @@ -19,7 +19,7 @@ WorkspaceMember, ) from plane.utils.paginator import BasePaginator -from plane.app.permissions import allow_permission +from plane.app.permissions import allow_permission, ROLE # Module imports from ..base import BaseAPIView, BaseViewSet @@ -41,7 +41,8 @@ def get_queryset(self): ) @allow_permission( - roles=["ADMIN", "MEMBER", "VIEWER", "GUEST"], level="WORKSPACE" + allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER, ROLE.GUEST], + level="WORKSPACE", ) def list(self, request, slug): # Get query parameters @@ -173,7 +174,8 @@ def list(self, request, slug): return Response(serializer.data, status=status.HTTP_200_OK) @allow_permission( - roles=["ADMIN", "MEMBER", "VIEWER", "GUEST"], level="WORKSPACE" + allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER, ROLE.GUEST], + level="WORKSPACE", ) def partial_update(self, request, slug, pk): notification = Notification.objects.get( @@ -192,7 +194,9 @@ def partial_update(self, request, slug, pk): return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - @allow_permission(roles=["ADMIN", "MEMBER"], level="WORKSPACE") + @allow_permission( + allowed_roles=[ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE" + ) def mark_read(self, request, slug, pk): notification = Notification.objects.get( receiver=request.user, workspace__slug=slug, pk=pk @@ -202,7 +206,9 @@ def mark_read(self, request, slug, pk): serializer = NotificationSerializer(notification) return Response(serializer.data, status=status.HTTP_200_OK) - @allow_permission(roles=["ADMIN", "MEMBER"], level="WORKSPACE") + @allow_permission( + allowed_roles=[ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE" + ) def mark_unread(self, request, slug, pk): notification = Notification.objects.get( receiver=request.user, workspace__slug=slug, pk=pk @@ -212,7 +218,9 @@ def mark_unread(self, request, slug, pk): serializer = NotificationSerializer(notification) return Response(serializer.data, status=status.HTTP_200_OK) - @allow_permission(roles=["ADMIN", "MEMBER"], level="WORKSPACE") + @allow_permission( + allowed_roles=[ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE" + ) def archive(self, request, slug, pk): notification = Notification.objects.get( receiver=request.user, workspace__slug=slug, pk=pk @@ -222,7 +230,9 @@ def archive(self, request, slug, pk): serializer = NotificationSerializer(notification) return Response(serializer.data, status=status.HTTP_200_OK) - @allow_permission(roles=["ADMIN", "MEMBER"], level="WORKSPACE") + @allow_permission( + allowed_roles=[ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE" + ) def unarchive(self, request, slug, pk): notification = Notification.objects.get( receiver=request.user, workspace__slug=slug, pk=pk @@ -236,7 +246,8 @@ def unarchive(self, request, slug, pk): class UnreadNotificationEndpoint(BaseAPIView): @allow_permission( - roles=["ADMIN", "MEMBER", "VIEWER", "GUEST"], level="WORKSPACE" + allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER, ROLE.GUEST], + level="WORKSPACE", ) def get(self, request, slug): # Watching Issues Count @@ -276,7 +287,9 @@ def get(self, request, slug): class MarkAllReadNotificationViewSet(BaseViewSet): - @allow_permission(roles=["ADMIN", "MEMBER"], level="WORKSPACE") + @allow_permission( + allowed_roles=[ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE" + ) def create(self, request, slug): snoozed = request.data.get("snoozed", False) archived = request.data.get("archived", False) @@ -360,7 +373,9 @@ class UserNotificationPreferenceEndpoint(BaseAPIView): serializer_class = UserNotificationPreferenceSerializer # request the object - @allow_permission(roles=["ADMIN", "MEMBER", "GUEST"], level="WORKSPACE") + @allow_permission( + allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE" + ) def get(self, request): user_notification_preference = UserNotificationPreference.objects.get( user=request.user @@ -371,7 +386,9 @@ def get(self, request): return Response(serializer.data, status=status.HTTP_200_OK) # update the object - @allow_permission(roles=["ADMIN", "MEMBER", "GUEST"], level="WORKSPACE") + @allow_permission( + allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE" + ) def patch(self, request): user_notification_preference = UserNotificationPreference.objects.get( user=request.user diff --git a/apiserver/plane/app/views/page/base.py b/apiserver/plane/app/views/page/base.py index 8b7a4d714fd..cda44b227b9 100644 --- a/apiserver/plane/app/views/page/base.py +++ b/apiserver/plane/app/views/page/base.py @@ -19,7 +19,7 @@ from rest_framework.response import Response -from plane.app.permissions import allow_permission +from plane.app.permissions import allow_permission, ROLE from plane.app.serializers import ( PageLogSerializer, PageSerializer, @@ -119,7 +119,7 @@ def get_queryset(self): .distinct() ) - @allow_permission(["ADMIN", "MEMBER"]) + @allow_permission([ROLE.ADMIN, ROLE.MEMBER]) def create(self, request, slug, project_id): serializer = PageSerializer( data=request.data, @@ -141,7 +141,7 @@ def create(self, request, slug, project_id): return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - @allow_permission(["ADMIN", "MEMBER"]) + @allow_permission([ROLE.ADMIN, ROLE.MEMBER]) def partial_update(self, request, slug, project_id, pk): try: page = Page.objects.get( @@ -207,7 +207,7 @@ def partial_update(self, request, slug, project_id, pk): status=status.HTTP_400_BAD_REQUEST, ) - @allow_permission(["ADMIN", "MEMBER", "VIEWER"]) + @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER]) def retrieve(self, request, slug, project_id, pk=None): page = self.get_queryset().filter(pk=pk).first() if page is None: @@ -226,7 +226,7 @@ def retrieve(self, request, slug, project_id, pk=None): status=status.HTTP_200_OK, ) - @allow_permission(["ADMIN", "MEMBER"]) + @allow_permission([ROLE.ADMIN, ROLE.MEMBER]) def lock(self, request, slug, project_id, pk): page = Page.objects.filter( pk=pk, workspace__slug=slug, projects__id=project_id @@ -236,7 +236,7 @@ def lock(self, request, slug, project_id, pk): page.save() return Response(status=status.HTTP_204_NO_CONTENT) - @allow_permission(["ADMIN", "MEMBER"]) + @allow_permission([ROLE.ADMIN, ROLE.MEMBER]) def unlock(self, request, slug, project_id, pk): page = Page.objects.filter( pk=pk, workspace__slug=slug, projects__id=project_id @@ -247,7 +247,7 @@ def unlock(self, request, slug, project_id, pk): return Response(status=status.HTTP_204_NO_CONTENT) - @allow_permission(["ADMIN", "MEMBER"]) + @allow_permission([ROLE.ADMIN, ROLE.MEMBER]) def access(self, request, slug, project_id, pk): access = request.data.get("access", 0) page = Page.objects.filter( @@ -270,13 +270,13 @@ def access(self, request, slug, project_id, pk): page.save() return Response(status=status.HTTP_204_NO_CONTENT) - @allow_permission(["ADMIN", "MEMBER", "VIEWER"]) + @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER]) def list(self, request, slug, project_id): queryset = self.get_queryset() pages = PageSerializer(queryset, many=True).data return Response(pages, status=status.HTTP_200_OK) - @allow_permission(["ADMIN", "MEMBER"]) + @allow_permission([ROLE.ADMIN, ROLE.MEMBER]) def archive(self, request, slug, project_id, pk): page = Page.objects.get( pk=pk, workspace__slug=slug, projects__id=project_id @@ -304,7 +304,7 @@ def archive(self, request, slug, project_id, pk): status=status.HTTP_200_OK, ) - @allow_permission(["ADMIN", "MEMBER"]) + @allow_permission([ROLE.ADMIN, ROLE.MEMBER]) def unarchive(self, request, slug, project_id, pk): page = Page.objects.get( pk=pk, workspace__slug=slug, projects__id=project_id @@ -334,7 +334,7 @@ def unarchive(self, request, slug, project_id, pk): return Response(status=status.HTTP_204_NO_CONTENT) - @allow_permission(["ADMIN"], creator=True, model=Page) + @allow_permission([ROLE.ADMIN], creator=True, model=Page) def destroy(self, request, slug, project_id, pk): page = Page.objects.get( pk=pk, workspace__slug=slug, projects__id=project_id @@ -380,7 +380,7 @@ class PageFavoriteViewSet(BaseViewSet): model = UserFavorite - @allow_permission(["ADMIN", "MEMBER"]) + @allow_permission([ROLE.ADMIN, ROLE.MEMBER]) def create(self, request, slug, project_id, pk): _ = UserFavorite.objects.create( project_id=project_id, @@ -390,7 +390,7 @@ def create(self, request, slug, project_id, pk): ) return Response(status=status.HTTP_204_NO_CONTENT) - @allow_permission(["ADMIN", "MEMBER"]) + @allow_permission([ROLE.ADMIN, ROLE.MEMBER]) def destroy(self, request, slug, project_id, pk): page_favorite = UserFavorite.objects.get( project=project_id, @@ -462,7 +462,7 @@ def get(self, request, slug, project_id, page_id): class PagesDescriptionViewSet(BaseViewSet): - @allow_permission(["ADMIN", "MEMBER", "VIEWER"]) + @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER]) def retrieve(self, request, slug, project_id, pk): page = ( Page.objects.filter( @@ -487,7 +487,7 @@ def stream_data(): ) return response - @allow_permission(["ADMIN", "MEMBER", "VIEWER", "GUEST"]) + @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER, ROLE.GUEST]) def partial_update(self, request, slug, project_id, pk): page = ( Page.objects.filter( diff --git a/apiserver/plane/app/views/project/base.py b/apiserver/plane/app/views/project/base.py index cccea8108a8..2aec4b3bba2 100644 --- a/apiserver/plane/app/views/project/base.py +++ b/apiserver/plane/app/views/project/base.py @@ -33,6 +33,7 @@ from plane.app.permissions import ( ProjectMemberPermission, allow_permission, + ROLE, ) from plane.db.models import ( UserFavorite, @@ -153,7 +154,8 @@ def get_queryset(self): ) @allow_permission( - roles=["ADMIN", "MEMBER", "VIEWER", "GUEST"], level="WORKSPACE" + allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER, ROLE.GUEST], + level="WORKSPACE", ) def list(self, request, slug): fields = [ @@ -191,7 +193,8 @@ def list(self, request, slug): return Response(projects, status=status.HTTP_200_OK) @allow_permission( - roles=["ADMIN", "MEMBER", "VIEWER", "GUEST"], level="WORKSPACE" + allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER, ROLE.GUEST], + level="WORKSPACE", ) def retrieve(self, request, slug, pk): project = ( @@ -264,7 +267,7 @@ def retrieve(self, request, slug, pk): serializer = ProjectListSerializer(project) return Response(serializer.data, status=status.HTTP_200_OK) - @allow_permission(["ADMIN", "MEMBER"], level="WORKSPACE") + @allow_permission([ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE") def create(self, request, slug): try: workspace = Workspace.objects.get(slug=slug) @@ -394,7 +397,7 @@ def create(self, request, slug): status=status.HTTP_410_GONE, ) - @allow_permission(["ADMIN", "MEMBER"], level="WORKSPACE") + @allow_permission([ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE") def partial_update(self, request, slug, pk=None): try: workspace = Workspace.objects.get(slug=slug) @@ -476,7 +479,7 @@ def partial_update(self, request, slug, pk=None): class ProjectArchiveUnarchiveEndpoint(BaseAPIView): - @allow_permission(["ADMIN", "MEMBER"]) + @allow_permission([ROLE.ADMIN, ROLE.MEMBER]) def post(self, request, slug, project_id): project = Project.objects.get(pk=project_id, workspace__slug=slug) project.archived_at = timezone.now() @@ -486,7 +489,7 @@ def post(self, request, slug, project_id): status=status.HTTP_200_OK, ) - @allow_permission(["ADMIN", "MEMBER"]) + @allow_permission([ROLE.ADMIN, ROLE.MEMBER]) def delete(self, request, slug, project_id): project = Project.objects.get(pk=project_id, workspace__slug=slug) project.archived_at = None @@ -495,7 +498,7 @@ def delete(self, request, slug, project_id): class ProjectIdentifierEndpoint(BaseAPIView): - @allow_permission(["ADMIN", "MEMBER"], level="WORKSPACE") + @allow_permission([ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE") def get(self, request, slug): name = request.GET.get("name", "").strip().upper() @@ -514,7 +517,7 @@ def get(self, request, slug): status=status.HTTP_200_OK, ) - @allow_permission(["ADMIN", "MEMBER"], level="WORKSPACE") + @allow_permission([ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE") def delete(self, request, slug): name = request.data.get("name", "").strip().upper() diff --git a/apiserver/plane/app/views/project/member.py b/apiserver/plane/app/views/project/member.py index afb3931c1b5..460e35e2ec4 100644 --- a/apiserver/plane/app/views/project/member.py +++ b/apiserver/plane/app/views/project/member.py @@ -27,7 +27,7 @@ ) from plane.bgtasks.project_add_user_email_task import project_add_user_email from plane.utils.host import base_host -from plane.app.permissions.base import allow_permission +from plane.app.permissions.base import allow_permission, ROLE class ProjectMemberViewSet(BaseViewSet): @@ -64,7 +64,7 @@ def get_queryset(self): .select_related("workspace", "workspace__owner") ) - @allow_permission(["ADMIN"]) + @allow_permission([ROLE.ADMIN]) def create(self, request, slug, project_id): # Get the list of members to be added to the project and their roles i.e. the user_id and the role members = request.data.get("members", []) @@ -189,7 +189,7 @@ def create(self, request, slug, project_id): # Return the serialized data return Response(serializer.data, status=status.HTTP_201_CREATED) - @allow_permission(["ADMIN", "MEMBER", "VIEWER", "GUEST"]) + @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER, ROLE.GUEST]) def list(self, request, slug, project_id): # Get the list of project members for the project project_members = ProjectMember.objects.filter( @@ -204,7 +204,7 @@ def list(self, request, slug, project_id): ) return Response(serializer.data, status=status.HTTP_200_OK) - @allow_permission(["ADMIN"]) + @allow_permission([ROLE.ADMIN]) def partial_update(self, request, slug, project_id, pk): project_member = ProjectMember.objects.get( pk=pk, @@ -261,7 +261,7 @@ def partial_update(self, request, slug, project_id, pk): return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - @allow_permission(["ADMIN", "MEMBER"]) + @allow_permission([ROLE.ADMIN, ROLE.MEMBER]) def destroy(self, request, slug, project_id, pk): project_member = ProjectMember.objects.get( workspace__slug=slug, @@ -298,7 +298,7 @@ def destroy(self, request, slug, project_id, pk): project_member.save() return Response(status=status.HTTP_204_NO_CONTENT) - @allow_permission(["ADMIN", "MEMBER", "VIEWER", "GUEST"]) + @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER, ROLE.GUEST]) def leave(self, request, slug, project_id): project_member = ProjectMember.objects.get( workspace__slug=slug, diff --git a/apiserver/plane/app/views/view/base.py b/apiserver/plane/app/views/view/base.py index 866b6e20f7b..96b4242a9c5 100644 --- a/apiserver/plane/app/views/view/base.py +++ b/apiserver/plane/app/views/view/base.py @@ -20,9 +20,8 @@ from rest_framework.response import Response from plane.app.permissions import ( - ProjectEntityPermission, - WorkspaceEntityPermission, allow_permission, + ROLE, ) from plane.app.serializers import ( IssueViewSerializer, @@ -77,7 +76,8 @@ def get_queryset(self): ) @allow_permission( - roles=["ADMIN", "MEMBER", "VIEWER", "GUEST"], level="WORKSPACE" + allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER, ROLE.GUEST], + level="WORKSPACE", ) def list(self, request, slug): queryset = self.get_queryset() @@ -99,7 +99,7 @@ def list(self, request, slug): return Response(views, status=status.HTTP_200_OK) @allow_permission( - roles=[], level="WORKSPACE", creator=True, model=IssueView + allowed_roles=[], level="WORKSPACE", creator=True, model=IssueView ) def partial_update(self, request, slug, pk): with transaction.atomic(): @@ -135,7 +135,10 @@ def partial_update(self, request, slug, pk): ) @allow_permission( - roles=["ADMIN"], level="WORKSPACE", creator=True, model=IssueView + allowed_roles=[ROLE.ADMIN], + level="WORKSPACE", + creator=True, + model=IssueView, ) def destroy(self, request, slug, pk): workspace_view = IssueView.objects.get( @@ -255,7 +258,8 @@ def get_queryset(self): @method_decorator(gzip_page) @allow_permission( - roles=["ADMIN", "MEMBER", "VIEWER", "GUEST"], level="WORKSPACE" + allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER, ROLE.GUEST], + level="WORKSPACE", ) def list(self, request, slug): filters = issue_filters(request.query_params, "GET") @@ -416,7 +420,10 @@ def get_queryset(self): .distinct() ) - allow_permission(roles=["ADMIN", "MEMBER", "VIEWER", "GUEST"]) + allow_permission( + allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER, ROLE.GUEST] + ) + def list(self, request, slug, project_id): queryset = self.get_queryset() if ProjectMember.objects.filter( @@ -437,7 +444,7 @@ def list(self, request, slug, project_id): ).data return Response(views, status=status.HTTP_200_OK) - allow_permission(roles=[], creator=True, model=IssueView) + allow_permission(allowed_roles=[], creator=True, model=IssueView) def partial_update(self, request, slug, project_id, pk): with transaction.atomic(): @@ -471,7 +478,7 @@ def partial_update(self, request, slug, project_id, pk): serializer.errors, status=status.HTTP_400_BAD_REQUEST ) - allow_permission(roles=["ADMIN"], creator=True, model=IssueView) + allow_permission(allowed_roles=[ROLE.ADMIN], creator=True, model=IssueView) def destroy(self, request, slug, project_id, pk): project_view = IssueView.objects.get( @@ -517,7 +524,7 @@ def get_queryset(self): .select_related("view") ) - allow_permission(["ADMIN", "MEMBER"]) + allow_permission([ROLE.ADMIN, ROLE.MEMBER]) def create(self, request, slug, project_id): _ = UserFavorite.objects.create( @@ -528,7 +535,7 @@ def create(self, request, slug, project_id): ) return Response(status=status.HTTP_204_NO_CONTENT) - allow_permission(["ADMIN", "MEMBER"]) + allow_permission([ROLE.ADMIN, ROLE.MEMBER]) def destroy(self, request, slug, project_id, view_id): view_favorite = UserFavorite.objects.get( diff --git a/apiserver/plane/app/views/webhook/base.py b/apiserver/plane/app/views/webhook/base.py index 1d66cab9e52..5581b6aa306 100644 --- a/apiserver/plane/app/views/webhook/base.py +++ b/apiserver/plane/app/views/webhook/base.py @@ -9,13 +9,13 @@ from plane.db.models import Webhook, WebhookLog, Workspace from plane.db.models.webhook import generate_token from ..base import BaseAPIView -from plane.app.permissions import allow_permission +from plane.app.permissions import allow_permission, ROLE from plane.app.serializers import WebhookSerializer, WebhookLogSerializer class WebhookEndpoint(BaseAPIView): - @allow_permission(roles=["ADMIN"], level="WORKSPACE") + @allow_permission(allowed_roles=[ROLE.ADMIN], level="WORKSPACE") def post(self, request, slug): workspace = Workspace.objects.get(slug=slug) try: @@ -38,7 +38,7 @@ def post(self, request, slug): ) raise IntegrityError - @allow_permission(roles=["ADMIN"], level="WORKSPACE") + @allow_permission(allowed_roles=[ROLE.ADMIN], level="WORKSPACE") def get(self, request, slug, pk=None): if pk is None: webhooks = Webhook.objects.filter(workspace__slug=slug) @@ -78,7 +78,7 @@ def get(self, request, slug, pk=None): ) return Response(serializer.data, status=status.HTTP_200_OK) - @allow_permission(roles=["ADMIN"], level="WORKSPACE") + @allow_permission(allowed_roles=[ROLE.ADMIN], level="WORKSPACE") def patch(self, request, slug, pk): webhook = Webhook.objects.get(workspace__slug=slug, pk=pk) serializer = WebhookSerializer( @@ -104,7 +104,7 @@ def patch(self, request, slug, pk): return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - @allow_permission(roles=["ADMIN"], level="WORKSPACE") + @allow_permission(allowed_roles=[ROLE.ADMIN], level="WORKSPACE") def delete(self, request, slug, pk): webhook = Webhook.objects.get(pk=pk, workspace__slug=slug) webhook.delete() @@ -113,7 +113,7 @@ def delete(self, request, slug, pk): class WebhookSecretRegenerateEndpoint(BaseAPIView): - @allow_permission(roles=["ADMIN"], level="WORKSPACE") + @allow_permission(allowed_roles=[ROLE.ADMIN], level="WORKSPACE") def post(self, request, slug, pk): webhook = Webhook.objects.get(workspace__slug=slug, pk=pk) webhook.secret_key = generate_token() @@ -124,7 +124,7 @@ def post(self, request, slug, pk): class WebhookLogsEndpoint(BaseAPIView): - @allow_permission(roles=["ADMIN"], level="WORKSPACE") + @allow_permission(allowed_roles=[ROLE.ADMIN], level="WORKSPACE") def get(self, request, slug, webhook_id): webhook_logs = WebhookLog.objects.filter( workspace__slug=slug, webhook_id=webhook_id diff --git a/apiserver/plane/app/views/workspace/favorite.py b/apiserver/plane/app/views/workspace/favorite.py index 5900b37a756..204dbfc3c2c 100644 --- a/apiserver/plane/app/views/workspace/favorite.py +++ b/apiserver/plane/app/views/workspace/favorite.py @@ -9,12 +9,14 @@ from plane.app.views.base import BaseAPIView from plane.db.models import UserFavorite, Workspace from plane.app.serializers import UserFavoriteSerializer -from plane.app.permissions import allow_permission +from plane.app.permissions import allow_permission, ROLE class WorkspaceFavoriteEndpoint(BaseAPIView): - @allow_permission(roles=["ADMIN", "MEMBER"], level="WORKSPACE") + @allow_permission( + allowed_roles=[ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE" + ) def get(self, request, slug): # the second filter is to check if the user is a member of the project favorites = UserFavorite.objects.filter( @@ -32,7 +34,9 @@ def get(self, request, slug): serializer = UserFavoriteSerializer(favorites, many=True) return Response(serializer.data, status=status.HTTP_200_OK) - @allow_permission(roles=["ADMIN", "MEMBER"], level="WORKSPACE") + @allow_permission( + allowed_roles=[ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE" + ) def post(self, request, slug): workspace = Workspace.objects.get(slug=slug) serializer = UserFavoriteSerializer(data=request.data) @@ -45,7 +49,9 @@ def post(self, request, slug): return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - @allow_permission(roles=["ADMIN", "MEMBER"], level="WORKSPACE") + @allow_permission( + allowed_roles=[ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE" + ) def patch(self, request, slug, favorite_id): favorite = UserFavorite.objects.get( user=request.user, workspace__slug=slug, pk=favorite_id @@ -58,7 +64,9 @@ def patch(self, request, slug, favorite_id): return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - @allow_permission(roles=["ADMIN", "MEMBER"], level="WORKSPACE") + @allow_permission( + allowed_roles=[ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE" + ) def delete(self, request, slug, favorite_id): favorite = UserFavorite.objects.get( user=request.user, workspace__slug=slug, pk=favorite_id @@ -69,7 +77,9 @@ def delete(self, request, slug, favorite_id): class WorkspaceFavoriteGroupEndpoint(BaseAPIView): - @allow_permission(roles=["ADMIN", "MEMBER"], level="WORKSPACE") + @allow_permission( + allowed_roles=[ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE" + ) def get(self, request, slug, favorite_id): favorites = UserFavorite.objects.filter( user=request.user,