feat(rbac): added frontend permission validation for missing pages and merged new features

This commit is contained in:
Akhil Mohan
2023-09-08 21:22:36 +05:30
parent 4b0bc238fc
commit 7c698e755a
9 changed files with 333 additions and 262 deletions
+16 -10
View File
@@ -138,6 +138,17 @@ export const getRoles = async (req: Request, res: Response) => {
const customRoles = await Role.find({ organization: orgId, isOrgRole, workspace: workspaceId }); const customRoles = await Role.find({ organization: orgId, isOrgRole, workspace: workspaceId });
const roles = [ const roles = [
...(isOrgRole
? [
{
_id: "owner",
name: "Owner",
slug: "owner",
description: "Complete administration access over the organization.",
permissions: adminPermissions.rules
}
]
: []),
{ {
_id: "admin", _id: "admin",
name: "Admin", name: "Admin",
@@ -152,24 +163,19 @@ export const getRoles = async (req: Request, res: Response) => {
description: "Non-administrative role in an organization", description: "Non-administrative role in an organization",
permissions: isOrgRole ? memberPermissions.rules : adminProjectPermissions.rules permissions: isOrgRole ? memberPermissions.rules : adminProjectPermissions.rules
}, },
...(isOrgRole
? []
: [
{ {
_id: "viewer", _id: "viewer",
name: "Viewer", name: "Viewer",
slug: "viewer", slug: "viewer",
description: "Non-administrative role in an organization", description: "Non-administrative role in an organization",
permissions: isOrgRole ? viewerProjectPermission.rules : viewerProjectPermission.rules permissions: isOrgRole ? viewerProjectPermission.rules : viewerProjectPermission.rules
}, }
]),
...customRoles ...customRoles
]; ];
if (isOrgRole) {
roles.unshift({
_id: "owner",
name: "Owner",
slug: "owner",
description: "Complete administration access over the organization.",
permissions: adminPermissions.rules
});
}
res.status(200).json({ res.status(200).json({
message: "Successfully fetched role list", message: "Successfully fetched role list",
@@ -10,7 +10,7 @@ type Props = {
} & BoundCanProps<TProjectPermission>; } & BoundCanProps<TProjectPermission>;
export const ProjectPermissionCan: FunctionComponent<Props> = ({ export const ProjectPermissionCan: FunctionComponent<Props> = ({
label = "Permission Denied. Kindly contact your org admin", label = "Permission Denied. Kindly contact your project admin",
children, children,
passThrough = true, passThrough = true,
...props ...props
@@ -4,7 +4,8 @@ import { useRouter } from "next/router";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import { Button, Modal, ModalContent } from "@app/components/v2"; import { Button, Modal, ModalContent } from "@app/components/v2";
import { useWorkspace } from "@app/context"; import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
import { withProjectPermission } from "@app/hoc";
import { usePopUp } from "@app/hooks"; import { usePopUp } from "@app/hooks";
import { import {
useDeleteIntegration, useDeleteIntegration,
@@ -31,7 +32,8 @@ type Props = {
frameworkIntegrations: Array<{ name: string; slug: string; image: string; docsLink: string }>; frameworkIntegrations: Array<{ name: string; slug: string; image: string; docsLink: string }>;
}; };
export const IntegrationsPage = ({ frameworkIntegrations }: Props) => { export const IntegrationsPage = withProjectPermission(
({ frameworkIntegrations }: Props) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { createNotification } = useNotificationContext(); const { createNotification } = useNotificationContext();
const router = useRouter(); const router = useRouter();
@@ -218,4 +220,6 @@ export const IntegrationsPage = ({ frameworkIntegrations }: Props) => {
<FrameworkIntegrationSection frameworks={frameworkIntegrations} /> <FrameworkIntegrationSection frameworks={frameworkIntegrations} />
</div> </div>
); );
}; },
{ action: ProjectPermissionActions.Read, subject: ProjectPermissionSub.Integrations }
);
@@ -2,7 +2,9 @@ import { useTranslation } from "react-i18next";
import { faCheck, faXmark } from "@fortawesome/free-solid-svg-icons"; import { faCheck, faXmark } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { DeleteActionModal,Skeleton, Tooltip } from "@app/components/v2"; import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import { DeleteActionModal, Skeleton, Tooltip } from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub, useProjectPermission } from "@app/context";
import { usePopUp } from "@app/hooks"; import { usePopUp } from "@app/hooks";
import { IntegrationAuth, TCloudIntegration } from "@app/hooks/api/types"; import { IntegrationAuth, TCloudIntegration } from "@app/hooks/api/types";
@@ -28,6 +30,8 @@ export const CloudIntegrationSection = ({
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
"deleteConfirmation" "deleteConfirmation"
] as const); ] as const);
const permission = useProjectPermission();
const { createNotification } = useNotificationContext();
const isEmpty = !isLoading && !cloudIntegrations?.length; const isEmpty = !isLoading && !cloudIntegrations?.length;
@@ -57,6 +61,18 @@ export const CloudIntegrationSection = ({
} flex h-32 flex-row items-center rounded-md border border-mineshaft-600 bg-mineshaft-800 p-4`} } flex h-32 flex-row items-center rounded-md border border-mineshaft-600 bg-mineshaft-800 p-4`}
onClick={() => { onClick={() => {
if (!cloudIntegration.isAvailable) return; if (!cloudIntegration.isAvailable) return;
if (
permission.cannot(
ProjectPermissionActions.Create,
ProjectPermissionSub.Integrations
)
) {
createNotification({
type: "error",
text: "Permission Denied. Kindly contact your project admin"
});
return;
}
onIntegrationStart(cloudIntegration.slug); onIntegrationStart(cloudIntegration.slug);
}} }}
key={cloudIntegration.slug} key={cloudIntegration.slug}
@@ -2,6 +2,7 @@ import { faArrowRight, faXmark } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { integrationSlugNameMapping } from "public/data/frequentConstants"; import { integrationSlugNameMapping } from "public/data/frequentConstants";
import { ProjectPermissionCan } from "@app/components/permissions";
import { import {
DeleteActionModal, DeleteActionModal,
EmptyState, EmptyState,
@@ -13,6 +14,7 @@ import {
Skeleton, Skeleton,
Tooltip Tooltip
} from "@app/components/v2"; } from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
import { usePopUp } from "@app/hooks"; import { usePopUp } from "@app/hooks";
import { TIntegration } from "@app/hooks/api/types"; import { TIntegration } from "@app/hooks/api/types";
@@ -125,11 +127,17 @@ export const IntegrationsSection = ({
)} )}
</div> </div>
<div className="flex cursor-default items-center"> <div className="flex cursor-default items-center">
<ProjectPermissionCan
I={ProjectPermissionActions.Delete}
a={ProjectPermissionSub.Integrations}
>
{(isAllowed) => (
<div className="ml-2 opacity-80 duration-200 hover:opacity-100"> <div className="ml-2 opacity-80 duration-200 hover:opacity-100">
<Tooltip content="Remove Integration"> <Tooltip content="Remove Integration">
<IconButton <IconButton
onClick={() => handlePopUpOpen("deleteConfirmation", integration)} onClick={() => handlePopUpOpen("deleteConfirmation", integration)}
ariaLabel="delete" ariaLabel="delete"
isDisabled={!isAllowed}
colorSchema="danger" colorSchema="danger"
variant="star" variant="star"
> >
@@ -137,6 +145,8 @@ export const IntegrationsSection = ({
</IconButton> </IconButton>
</Tooltip> </Tooltip>
</div> </div>
)}
</ProjectPermissionCan>
</div> </div>
</div> </div>
))} ))}
@@ -42,6 +42,7 @@ export const AutoCapitalizationSection = withProjectPermission(
<p className="mb-3 text-xl font-semibold">{t("settings.project.auto-capitalization")}</p> <p className="mb-3 text-xl font-semibold">{t("settings.project.auto-capitalization")}</p>
<ProjectPermissionCan I={ProjectPermissionActions.Edit} a={ProjectPermissionSub.Settings}> <ProjectPermissionCan I={ProjectPermissionActions.Edit} a={ProjectPermissionSub.Settings}>
{(isAllowed) => ( {(isAllowed) => (
<div className="w-max">
<Checkbox <Checkbox
className="data-[state=checked]:bg-primary" className="data-[state=checked]:bg-primary"
id="autoCapitalization" id="autoCapitalization"
@@ -53,6 +54,7 @@ export const AutoCapitalizationSection = withProjectPermission(
> >
{t("settings.project.auto-capitalization-description")} {t("settings.project.auto-capitalization-description")}
</Checkbox> </Checkbox>
</div>
)} )}
</ProjectPermissionCan> </ProjectPermissionCan>
</div> </div>
@@ -90,6 +90,7 @@ export const E2EESection = withProjectPermission(
</p> </p>
<ProjectPermissionCan I={ProjectPermissionActions.Edit} a={ProjectPermissionSub.Settings}> <ProjectPermissionCan I={ProjectPermissionActions.Edit} a={ProjectPermissionSub.Settings}>
{(isAllowed) => ( {(isAllowed) => (
<div className="w-max">
<Checkbox <Checkbox
className="data-[state=checked]:bg-primary" className="data-[state=checked]:bg-primary"
id="autoCapitalization" id="autoCapitalization"
@@ -101,6 +102,7 @@ export const E2EESection = withProjectPermission(
> >
End-to-end encryption enabled End-to-end encryption enabled
</Checkbox> </Checkbox>
</div>
)} )}
</ProjectPermissionCan> </ProjectPermissionCan>
</div> </div>
@@ -1,4 +1,4 @@
import { faArrowDown,faArrowUp, faPencil, faXmark } from "@fortawesome/free-solid-svg-icons"; import { faArrowDown, faArrowUp, faPencil, faXmark } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
@@ -16,9 +16,7 @@ import {
Tr Tr
} from "@app/components/v2"; } from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context"; import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
import { import { useReorderWsEnvironment } from "@app/hooks/api";
useReorderWsEnvironment
} from "@app/hooks/api";
import { UsePopUpState } from "@app/hooks/usePopUp"; import { UsePopUpState } from "@app/hooks/usePopUp";
type Props = { type Props = {
@@ -39,18 +37,23 @@ export const EnvironmentTable = ({ handlePopUpOpen }: Props) => {
const { createNotification } = useNotificationContext(); const { createNotification } = useNotificationContext();
const reorderWsEnvironment = useReorderWsEnvironment(); const reorderWsEnvironment = useReorderWsEnvironment();
const handleReorderEnv= async (shouldMoveUp: boolean, name: string, slug: string) => { const handleReorderEnv = async (shouldMoveUp: boolean, name: string, slug: string) => {
try { try {
if (!currentWorkspace?._id) return; if (!currentWorkspace?._id) return;
const indexOfEnv = currentWorkspace.environments.findIndex((env) => env.name === name && env.slug === slug); const indexOfEnv = currentWorkspace.environments.findIndex(
(env) => env.name === name && env.slug === slug
);
// check that this reordering is possible // check that this reordering is possible
if (indexOfEnv === 0 && shouldMoveUp || indexOfEnv === currentWorkspace.environments.length - 1 && !shouldMoveUp) { if (
return (indexOfEnv === 0 && shouldMoveUp) ||
(indexOfEnv === currentWorkspace.environments.length - 1 && !shouldMoveUp)
) {
return;
} }
const indexToSwap = shouldMoveUp ? indexOfEnv - 1 : indexOfEnv + 1 const indexToSwap = shouldMoveUp ? indexOfEnv - 1 : indexOfEnv + 1;
await reorderWsEnvironment.mutateAsync({ await reorderWsEnvironment.mutateAsync({
workspaceID: currentWorkspace._id, workspaceID: currentWorkspace._id,
@@ -92,31 +95,48 @@ export const EnvironmentTable = ({ handlePopUpOpen }: Props) => {
<Td>{name}</Td> <Td>{name}</Td>
<Td>{slug}</Td> <Td>{slug}</Td>
<Td className="flex items-center justify-end"> <Td className="flex items-center justify-end">
<ProjectPermissionCan
I={ProjectPermissionActions.Edit}
a={ProjectPermissionSub.Environments}
>
{(isAllowed) => (
<IconButton <IconButton
className="mr-3 py-2" className="mr-3 py-2"
onClick={() => { onClick={() => {
handleReorderEnv(false, name, slug) handleReorderEnv(false, name, slug);
}} }}
colorSchema="primary" colorSchema="primary"
variant="plain" variant="plain"
ariaLabel="update" ariaLabel="update"
isDisabled={pos === currentWorkspace.environments.length - 1} isDisabled={pos === currentWorkspace.environments.length - 1 || !isAllowed}
> >
<FontAwesomeIcon icon={faArrowDown} /> <FontAwesomeIcon icon={faArrowDown} />
</IconButton> </IconButton>
)}
</ProjectPermissionCan>
<ProjectPermissionCan
I={ProjectPermissionActions.Edit}
a={ProjectPermissionSub.Environments}
>
{(isAllowed) => (
<IconButton <IconButton
className="mr-3 py-2" className="mr-3 py-2"
onClick={() => { onClick={() => {
handleReorderEnv(true, name, slug) handleReorderEnv(true, name, slug);
}} }}
colorSchema="primary" colorSchema="primary"
variant="plain" variant="plain"
ariaLabel="update" ariaLabel="update"
isDisabled={pos === 0} isDisabled={pos === 0 || !isAllowed}
> >
<FontAwesomeIcon icon={faArrowUp} /> <FontAwesomeIcon icon={faArrowUp} />
</IconButton> </IconButton>
<ProjectPermissionCan I={ProjectPermissionActions.Edit} a={ProjectPermissionSub.Environments} )}
</ProjectPermissionCan>
<ProjectPermissionCan
I={ProjectPermissionActions.Edit}
a={ProjectPermissionSub.Environments}
> >
{(isAllowed) => ( {(isAllowed) => (
<IconButton <IconButton
@@ -1,3 +1,4 @@
import { ProjectPermissionCan } from "@app/components/permissions";
import { import {
decryptAssymmetric, decryptAssymmetric,
decryptSymmetric decryptSymmetric
@@ -65,9 +66,19 @@ export const ProjectIndexSecretsSection = withProjectPermission(
secrets. To access individual secrets by name through the SDK and public API, please secrets. To access individual secrets by name through the SDK and public API, please
enable blind indexing. enable blind indexing.
</p> </p>
<Button onClick={onEnableBlindIndices} color="mineshaft" size="sm" type="submit"> <ProjectPermissionCan I={ProjectPermissionActions.Edit} a={ProjectPermissionSub.Settings}>
{(isAllowed) => (
<Button
onClick={onEnableBlindIndices}
isDisabled={!isAllowed}
color="mineshaft"
size="sm"
type="submit"
>
Enable Blind Indexing Enable Blind Indexing
</Button> </Button>
)}
</ProjectPermissionCan>
</div> </div>
) : ( ) : (
<div /> <div />