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

This commit is contained in:
Akhil Mohan
2023-08-28 20:27:31 +05:30
parent 4b0bc238fc
commit 7c698e755a
9 changed files with 333 additions and 262 deletions

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 roles = [
...(isOrgRole
? [
{
_id: "owner",
name: "Owner",
slug: "owner",
description: "Complete administration access over the organization.",
permissions: adminPermissions.rules
}
]
: []),
{
_id: "admin",
name: "Admin",
@@ -152,24 +163,19 @@ export const getRoles = async (req: Request, res: Response) => {
description: "Non-administrative role in an organization",
permissions: isOrgRole ? memberPermissions.rules : adminProjectPermissions.rules
},
{
_id: "viewer",
name: "Viewer",
slug: "viewer",
description: "Non-administrative role in an organization",
permissions: isOrgRole ? viewerProjectPermission.rules : viewerProjectPermission.rules
},
...(isOrgRole
? []
: [
{
_id: "viewer",
name: "Viewer",
slug: "viewer",
description: "Non-administrative role in an organization",
permissions: isOrgRole ? viewerProjectPermission.rules : viewerProjectPermission.rules
}
]),
...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({
message: "Successfully fetched role list",

View File

@@ -10,7 +10,7 @@ type Props = {
} & BoundCanProps<TProjectPermission>;
export const ProjectPermissionCan: FunctionComponent<Props> = ({
label = "Permission Denied. Kindly contact your org admin",
label = "Permission Denied. Kindly contact your project admin",
children,
passThrough = true,
...props

View File

@@ -4,7 +4,8 @@ import { useRouter } from "next/router";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
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 {
useDeleteIntegration,
@@ -31,191 +32,194 @@ type Props = {
frameworkIntegrations: Array<{ name: string; slug: string; image: string; docsLink: string }>;
};
export const IntegrationsPage = ({ frameworkIntegrations }: Props) => {
const { t } = useTranslation();
const { createNotification } = useNotificationContext();
const router = useRouter();
export const IntegrationsPage = withProjectPermission(
({ frameworkIntegrations }: Props) => {
const { t } = useTranslation();
const { createNotification } = useNotificationContext();
const router = useRouter();
const { currentWorkspace } = useWorkspace();
const workspaceId = currentWorkspace?._id || "";
const environments = currentWorkspace?.environments || [];
const { currentWorkspace } = useWorkspace();
const workspaceId = currentWorkspace?._id || "";
const environments = currentWorkspace?.environments || [];
const { data: latestWsKey } = useGetUserWsKey(workspaceId);
const { data: latestWsKey } = useGetUserWsKey(workspaceId);
const { popUp, handlePopUpOpen, handlePopUpToggle, handlePopUpClose } = usePopUp([
"activeBot"
] as const);
const { popUp, handlePopUpOpen, handlePopUpToggle, handlePopUpClose } = usePopUp([
"activeBot"
] as const);
const { data: cloudIntegrations, isLoading: isCloudIntegrationsLoading } =
useGetCloudIntegrations();
const { data: cloudIntegrations, isLoading: isCloudIntegrationsLoading } =
useGetCloudIntegrations();
const { data: integrationAuths, isLoading: isIntegrationAuthLoading } =
useGetWorkspaceAuthorizations(
workspaceId,
useCallback((data: IntegrationAuth[]) => {
const groupBy: Record<string, IntegrationAuth> = {};
data.forEach((el) => {
groupBy[el.integration] = el;
});
return groupBy;
}, [])
);
// mutation
const {
data: integrations,
isLoading: isIntegrationLoading,
isFetching: isIntegrationFetching
} = useGetWorkspaceIntegrations(workspaceId);
const { data: integrationAuths, isLoading: isIntegrationAuthLoading } =
useGetWorkspaceAuthorizations(
workspaceId,
useCallback((data: IntegrationAuth[]) => {
const groupBy: Record<string, IntegrationAuth> = {};
data.forEach((el) => {
groupBy[el.integration] = el;
});
return groupBy;
}, [])
);
// mutation
const {
data: integrations,
isLoading: isIntegrationLoading,
isFetching: isIntegrationFetching
} = useGetWorkspaceIntegrations(workspaceId);
const { data: bot } = useGetWorkspaceBot(workspaceId);
const { data: bot } = useGetWorkspaceBot(workspaceId);
// mutation
const { mutateAsync: updateBotActiveStatus, mutate: updateBotActiveStatusSync } =
useUpdateBotActiveStatus();
const { mutateAsync: deleteIntegration } = useDeleteIntegration();
const {
mutateAsync: deleteIntegrationAuth,
isLoading: isDeleteIntegrationAuthSuccess,
reset: resetDeleteIntegrationAuth
} = useDeleteIntegrationAuth();
// mutation
const { mutateAsync: updateBotActiveStatus, mutate: updateBotActiveStatusSync } =
useUpdateBotActiveStatus();
const { mutateAsync: deleteIntegration } = useDeleteIntegration();
const {
mutateAsync: deleteIntegrationAuth,
isLoading: isDeleteIntegrationAuthSuccess,
reset: resetDeleteIntegrationAuth
} = useDeleteIntegrationAuth();
// summary: this use effect is trigger when all integration auths are removed thus deactivate bot
// details: so onsuccessfully deleting an integration auth, immediately integration list is refeteched
// After the refetch is completed check if its empty. Then set bot active and reset the submit hook
useEffect(() => {
if (isDeleteIntegrationAuthSuccess && !isIntegrationFetching && !integrations?.length) {
if (bot?._id)
updateBotActiveStatusSync({
isActive: false,
botId: bot._id,
workspaceId
});
resetDeleteIntegrationAuth();
}
}, [isIntegrationFetching, isDeleteIntegrationAuthSuccess, integrations?.length]);
const handleProviderIntegration = async (provider: string) => {
const selectedCloudIntegration = cloudIntegrations?.find(({ slug }) => provider === slug);
if (!selectedCloudIntegration) return;
try {
if (bot && !bot.isActive) {
const botKey = generateBotKey(bot.publicKey, latestWsKey!);
await updateBotActiveStatus({
workspaceId,
botKey,
isActive: true,
botId: bot._id
});
// summary: this use effect is trigger when all integration auths are removed thus deactivate bot
// details: so onsuccessfully deleting an integration auth, immediately integration list is refeteched
// After the refetch is completed check if its empty. Then set bot active and reset the submit hook
useEffect(() => {
if (isDeleteIntegrationAuthSuccess && !isIntegrationFetching && !integrations?.length) {
if (bot?._id)
updateBotActiveStatusSync({
isActive: false,
botId: bot._id,
workspaceId
});
resetDeleteIntegrationAuth();
}
const integrationAuthForProvider = integrationAuths?.[provider];
if (!integrationAuthForProvider) {
redirectForProviderAuth(selectedCloudIntegration);
}, [isIntegrationFetching, isDeleteIntegrationAuthSuccess, integrations?.length]);
const handleProviderIntegration = async (provider: string) => {
const selectedCloudIntegration = cloudIntegrations?.find(({ slug }) => provider === slug);
if (!selectedCloudIntegration) return;
try {
if (bot && !bot.isActive) {
const botKey = generateBotKey(bot.publicKey, latestWsKey!);
await updateBotActiveStatus({
workspaceId,
botKey,
isActive: true,
botId: bot._id
});
}
const integrationAuthForProvider = integrationAuths?.[provider];
if (!integrationAuthForProvider) {
redirectForProviderAuth(selectedCloudIntegration);
return;
}
const url = redirectToIntegrationAppConfigScreen(provider, integrationAuthForProvider._id);
router.push(url);
} catch (error) {
console.error(error);
}
};
// function to strat integration for a provider
// confirmation to user passing the bot key for provider to get secret access
const handleProviderIntegrationStart = (provider: string) => {
if (!bot?.isActive) {
handlePopUpOpen("activeBot", { provider });
return;
}
handleProviderIntegration(provider);
};
const url = redirectToIntegrationAppConfigScreen(provider, integrationAuthForProvider._id);
router.push(url);
} catch (error) {
console.error(error);
}
};
const handleUserAcceptBotCondition = () => {
const { provider } = popUp.activeBot?.data as { provider: string };
handleProviderIntegration(provider);
handlePopUpClose("activeBot");
};
// function to strat integration for a provider
// confirmation to user passing the bot key for provider to get secret access
const handleProviderIntegrationStart = (provider: string) => {
if (!bot?.isActive) {
handlePopUpOpen("activeBot", { provider });
return;
}
handleProviderIntegration(provider);
};
const handleIntegrationDelete = async (integrationId: string, cb: () => void) => {
try {
await deleteIntegration({ id: integrationId, workspaceId });
if (cb) cb();
createNotification({
type: "success",
text: "Deleted integration"
});
} catch (err) {
console.log(err);
createNotification({
type: "error",
text: "Failed to delete integration"
});
}
};
const handleUserAcceptBotCondition = () => {
const { provider } = popUp.activeBot?.data as { provider: string };
handleProviderIntegration(provider);
handlePopUpClose("activeBot");
};
const handleIntegrationAuthRevoke = async (provider: string, cb?: () => void) => {
const integrationAuthForProvider = integrationAuths?.[provider];
if (!integrationAuthForProvider) return;
try {
await deleteIntegrationAuth({
id: integrationAuthForProvider._id,
workspaceId
});
if (cb) cb();
createNotification({
type: "success",
text: "Revoked provider authentication"
});
} catch (err) {
console.error(err);
createNotification({
type: "error",
text: "Failed to revoke provider authentication"
});
}
};
const handleIntegrationDelete = async (integrationId: string, cb: () => void) => {
try {
await deleteIntegration({ id: integrationId, workspaceId });
if (cb) cb();
createNotification({
type: "success",
text: "Deleted integration"
});
} catch (err) {
console.log(err);
createNotification({
type: "error",
text: "Failed to delete integration"
});
}
};
const handleIntegrationAuthRevoke = async (provider: string, cb?: () => void) => {
const integrationAuthForProvider = integrationAuths?.[provider];
if (!integrationAuthForProvider) return;
try {
await deleteIntegrationAuth({
id: integrationAuthForProvider._id,
workspaceId
});
if (cb) cb();
createNotification({
type: "success",
text: "Revoked provider authentication"
});
} catch (err) {
console.error(err);
createNotification({
type: "error",
text: "Failed to revoke provider authentication"
});
}
};
return (
<div className="container mx-auto max-w-7xl pb-12 text-white">
<IntegrationsSection
isLoading={isIntegrationLoading}
integrations={integrations}
environments={environments}
onIntegrationDelete={({ _id: id }, cb) => handleIntegrationDelete(id, cb)}
/>
<CloudIntegrationSection
isLoading={isCloudIntegrationsLoading || isIntegrationAuthLoading}
cloudIntegrations={cloudIntegrations}
integrationAuths={integrationAuths}
onIntegrationStart={handleProviderIntegrationStart}
onIntegrationRevoke={handleIntegrationAuthRevoke}
/>
<Modal
isOpen={popUp.activeBot?.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("activeBot", isOpen)}
>
<ModalContent
title={t("integrations.grant-access-to-secrets") as string}
footerContent={
<div className="flex items-center space-x-2">
<Button onClick={() => handleUserAcceptBotCondition()}>
{t("integrations.grant-access-button") as string}
</Button>
<Button
onClick={() => handlePopUpClose("activeBot")}
variant="outline_bg"
colorSchema="secondary"
>
Cancel
</Button>
</div>
}
return (
<div className="container mx-auto max-w-7xl pb-12 text-white">
<IntegrationsSection
isLoading={isIntegrationLoading}
integrations={integrations}
environments={environments}
onIntegrationDelete={({ _id: id }, cb) => handleIntegrationDelete(id, cb)}
/>
<CloudIntegrationSection
isLoading={isCloudIntegrationsLoading || isIntegrationAuthLoading}
cloudIntegrations={cloudIntegrations}
integrationAuths={integrationAuths}
onIntegrationStart={handleProviderIntegrationStart}
onIntegrationRevoke={handleIntegrationAuthRevoke}
/>
<Modal
isOpen={popUp.activeBot?.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("activeBot", isOpen)}
>
{t("integrations.why-infisical-needs-access")}
</ModalContent>
</Modal>
<FrameworkIntegrationSection frameworks={frameworkIntegrations} />
</div>
);
};
<ModalContent
title={t("integrations.grant-access-to-secrets") as string}
footerContent={
<div className="flex items-center space-x-2">
<Button onClick={() => handleUserAcceptBotCondition()}>
{t("integrations.grant-access-button") as string}
</Button>
<Button
onClick={() => handlePopUpClose("activeBot")}
variant="outline_bg"
colorSchema="secondary"
>
Cancel
</Button>
</div>
}
>
{t("integrations.why-infisical-needs-access")}
</ModalContent>
</Modal>
<FrameworkIntegrationSection frameworks={frameworkIntegrations} />
</div>
);
},
{ action: ProjectPermissionActions.Read, subject: ProjectPermissionSub.Integrations }
);

View File

@@ -2,7 +2,9 @@ import { useTranslation } from "react-i18next";
import { faCheck, faXmark } from "@fortawesome/free-solid-svg-icons";
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 { IntegrationAuth, TCloudIntegration } from "@app/hooks/api/types";
@@ -28,11 +30,13 @@ export const CloudIntegrationSection = ({
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
"deleteConfirmation"
] as const);
const permission = useProjectPermission();
const { createNotification } = useNotificationContext();
const isEmpty = !isLoading && !cloudIntegrations?.length;
const sortedCloudIntegrations = cloudIntegrations.sort((a, b) => a.name.localeCompare(b.name));
return (
<div>
<div className="m-4 mt-7 flex max-w-5xl flex-col items-start justify-between px-2 text-xl">
@@ -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`}
onClick={() => {
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);
}}
key={cloudIntegration.slug}

View File

@@ -2,6 +2,7 @@ import { faArrowRight, faXmark } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { integrationSlugNameMapping } from "public/data/frequentConstants";
import { ProjectPermissionCan } from "@app/components/permissions";
import {
DeleteActionModal,
EmptyState,
@@ -13,6 +14,7 @@ import {
Skeleton,
Tooltip
} from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
import { usePopUp } from "@app/hooks";
import { TIntegration } from "@app/hooks/api/types";
@@ -125,18 +127,26 @@ export const IntegrationsSection = ({
)}
</div>
<div className="flex cursor-default items-center">
<div className="ml-2 opacity-80 duration-200 hover:opacity-100">
<Tooltip content="Remove Integration">
<IconButton
onClick={() => handlePopUpOpen("deleteConfirmation", integration)}
ariaLabel="delete"
colorSchema="danger"
variant="star"
>
<FontAwesomeIcon icon={faXmark} className="px-0.5" />
</IconButton>
</Tooltip>
</div>
<ProjectPermissionCan
I={ProjectPermissionActions.Delete}
a={ProjectPermissionSub.Integrations}
>
{(isAllowed) => (
<div className="ml-2 opacity-80 duration-200 hover:opacity-100">
<Tooltip content="Remove Integration">
<IconButton
onClick={() => handlePopUpOpen("deleteConfirmation", integration)}
ariaLabel="delete"
isDisabled={!isAllowed}
colorSchema="danger"
variant="star"
>
<FontAwesomeIcon icon={faXmark} className="px-0.5" />
</IconButton>
</Tooltip>
</div>
)}
</ProjectPermissionCan>
</div>
</div>
))}

View File

@@ -42,17 +42,19 @@ export const AutoCapitalizationSection = withProjectPermission(
<p className="mb-3 text-xl font-semibold">{t("settings.project.auto-capitalization")}</p>
<ProjectPermissionCan I={ProjectPermissionActions.Edit} a={ProjectPermissionSub.Settings}>
{(isAllowed) => (
<Checkbox
className="data-[state=checked]:bg-primary"
id="autoCapitalization"
isDisabled={!isAllowed}
isChecked={currentWorkspace?.autoCapitalization ?? false}
onCheckedChange={(state) => {
handleToggleCapitalizationToggle(state as boolean);
}}
>
{t("settings.project.auto-capitalization-description")}
</Checkbox>
<div className="w-max">
<Checkbox
className="data-[state=checked]:bg-primary"
id="autoCapitalization"
isDisabled={!isAllowed}
isChecked={currentWorkspace?.autoCapitalization ?? false}
onCheckedChange={(state) => {
handleToggleCapitalizationToggle(state as boolean);
}}
>
{t("settings.project.auto-capitalization-description")}
</Checkbox>
</div>
)}
</ProjectPermissionCan>
</div>

View File

@@ -90,17 +90,19 @@ export const E2EESection = withProjectPermission(
</p>
<ProjectPermissionCan I={ProjectPermissionActions.Edit} a={ProjectPermissionSub.Settings}>
{(isAllowed) => (
<Checkbox
className="data-[state=checked]:bg-primary"
id="autoCapitalization"
isChecked={!bot.isActive}
isDisabled={!isAllowed}
onCheckedChange={async () => {
await toggleBotActivate();
}}
>
End-to-end encryption enabled
</Checkbox>
<div className="w-max">
<Checkbox
className="data-[state=checked]:bg-primary"
id="autoCapitalization"
isChecked={!bot.isActive}
isDisabled={!isAllowed}
onCheckedChange={async () => {
await toggleBotActivate();
}}
>
End-to-end encryption enabled
</Checkbox>
</div>
)}
</ProjectPermissionCan>
</div>

View File

@@ -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 { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
@@ -16,9 +16,7 @@ import {
Tr
} from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
import {
useReorderWsEnvironment
} from "@app/hooks/api";
import { useReorderWsEnvironment } from "@app/hooks/api";
import { UsePopUpState } from "@app/hooks/usePopUp";
type Props = {
@@ -39,18 +37,23 @@ export const EnvironmentTable = ({ handlePopUpOpen }: Props) => {
const { createNotification } = useNotificationContext();
const reorderWsEnvironment = useReorderWsEnvironment();
const handleReorderEnv= async (shouldMoveUp: boolean, name: string, slug: string) => {
const handleReorderEnv = async (shouldMoveUp: boolean, name: string, slug: string) => {
try {
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
if (indexOfEnv === 0 && shouldMoveUp || indexOfEnv === currentWorkspace.environments.length - 1 && !shouldMoveUp) {
return
if (
(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({
workspaceID: currentWorkspace._id,
@@ -92,31 +95,48 @@ export const EnvironmentTable = ({ handlePopUpOpen }: Props) => {
<Td>{name}</Td>
<Td>{slug}</Td>
<Td className="flex items-center justify-end">
<IconButton
className="mr-3 py-2"
onClick={() => {
handleReorderEnv(false, name, slug)
}}
colorSchema="primary"
variant="plain"
ariaLabel="update"
isDisabled={pos === currentWorkspace.environments.length - 1}
<ProjectPermissionCan
I={ProjectPermissionActions.Edit}
a={ProjectPermissionSub.Environments}
>
<FontAwesomeIcon icon={faArrowDown} />
</IconButton>
<IconButton
className="mr-3 py-2"
onClick={() => {
handleReorderEnv(true, name, slug)
}}
colorSchema="primary"
variant="plain"
ariaLabel="update"
isDisabled={pos === 0}
{(isAllowed) => (
<IconButton
className="mr-3 py-2"
onClick={() => {
handleReorderEnv(false, name, slug);
}}
colorSchema="primary"
variant="plain"
ariaLabel="update"
isDisabled={pos === currentWorkspace.environments.length - 1 || !isAllowed}
>
<FontAwesomeIcon icon={faArrowDown} />
</IconButton>
)}
</ProjectPermissionCan>
<ProjectPermissionCan
I={ProjectPermissionActions.Edit}
a={ProjectPermissionSub.Environments}
>
<FontAwesomeIcon icon={faArrowUp} />
</IconButton>
<ProjectPermissionCan I={ProjectPermissionActions.Edit} a={ProjectPermissionSub.Environments}
{(isAllowed) => (
<IconButton
className="mr-3 py-2"
onClick={() => {
handleReorderEnv(true, name, slug);
}}
colorSchema="primary"
variant="plain"
ariaLabel="update"
isDisabled={pos === 0 || !isAllowed}
>
<FontAwesomeIcon icon={faArrowUp} />
</IconButton>
)}
</ProjectPermissionCan>
<ProjectPermissionCan
I={ProjectPermissionActions.Edit}
a={ProjectPermissionSub.Environments}
>
{(isAllowed) => (
<IconButton

View File

@@ -1,3 +1,4 @@
import { ProjectPermissionCan } from "@app/components/permissions";
import {
decryptAssymmetric,
decryptSymmetric
@@ -24,7 +25,7 @@ export const ProjectIndexSecretsSection = withProjectPermission(
const { data: latestFileKey } = useGetUserWsKey(currentWorkspace?._id ?? "");
const { data: encryptedSecrets } = useGetWorkspaceSecrets(currentWorkspace?._id ?? "");
const nameWorkspaceSecrets = useNameWorkspaceSecrets();
const onEnableBlindIndices = async () => {
if (!currentWorkspace?._id) return;
if (!encryptedSecrets) return;
@@ -65,9 +66,19 @@ export const ProjectIndexSecretsSection = withProjectPermission(
secrets. To access individual secrets by name through the SDK and public API, please
enable blind indexing.
</p>
<Button onClick={onEnableBlindIndices} color="mineshaft" size="sm" type="submit">
Enable Blind Indexing
</Button>
<ProjectPermissionCan I={ProjectPermissionActions.Edit} a={ProjectPermissionSub.Settings}>
{(isAllowed) => (
<Button
onClick={onEnableBlindIndices}
isDisabled={!isAllowed}
color="mineshaft"
size="sm"
type="submit"
>
Enable Blind Indexing
</Button>
)}
</ProjectPermissionCan>
</div>
) : (
<div />