feat(rbac): implemented granular blocking of actions based on permissions on org level ui

This commit is contained in:
Akhil Mohan
2023-09-08 21:21:43 +05:30
parent 34fb7be1c4
commit aac3168c80
41 changed files with 1992 additions and 1652 deletions
@@ -292,7 +292,7 @@ export const createOrganizationPortalSession = async (req: Request, res: Respons
const { permission } = await getUserOrgPermissions(req.user._id, organizationId); const { permission } = await getUserOrgPermissions(req.user._id, organizationId);
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
GeneralPermissionActions.Create, GeneralPermissionActions.Edit,
OrgPermissionSubjects.Billing OrgPermissionSubjects.Billing
); );
@@ -0,0 +1,50 @@
import { FunctionComponent, ReactNode } from "react";
import { BoundCanProps, Can } from "@casl/react";
import {
OrgPermissionSubjects,
OrgWorkspacePermissionActions,
TOrgPermission,
useOrgPermission
} from "@app/context/OrgPermissionContext";
import { Tooltip } from "../v2";
type Props = {
label?: ReactNode;
} & BoundCanProps<TOrgPermission>;
export const OrgPermissionCan: FunctionComponent<Props> = ({
label = "Permission Denied. Kindly contact your org admin",
children,
passThrough = true,
...props
}) => {
const permission = useOrgPermission();
return (
<Can
{...props}
passThrough={passThrough}
ability={props?.ability || permission}
I={OrgWorkspacePermissionActions.Read}
a={OrgPermissionSubjects.Sso}
>
{(isAllowed, ability) => {
// akhilmhdh: This is set as type due to error in casl react type.
const finalChild =
typeof children === "function"
? children(isAllowed, ability as TOrgPermission)
: children;
if (!isAllowed && passThrough) {
return <Tooltip content={label}>{finalChild}</Tooltip>;
}
if (!isAllowed) return null;
return finalChild;
}}
</Can>
);
};
@@ -0,0 +1 @@
export { OrgPermissionCan } from "./OrgPermissionCan";
@@ -3,13 +3,13 @@ import { createContext, ReactNode, useContext } from "react";
import { useGetUserOrgPermissions } from "@app/hooks/api"; import { useGetUserOrgPermissions } from "@app/hooks/api";
import { useOrganization } from "../OrganizationContext"; import { useOrganization } from "../OrganizationContext";
import { TPermission } from "./types"; import { TOrgPermission } from "./types";
type Props = { type Props = {
children: ReactNode; children: ReactNode;
}; };
const PermissionContext = createContext<null | TPermission>(null); const OrgPermissionContext = createContext<null | TOrgPermission>(null);
export const OrgPermissionProvider = ({ children }: Props): JSX.Element => { export const OrgPermissionProvider = ({ children }: Props): JSX.Element => {
const { currentOrg } = useOrganization(); const { currentOrg } = useOrganization();
@@ -37,11 +37,13 @@ export const OrgPermissionProvider = ({ children }: Props): JSX.Element => {
); );
} }
return <PermissionContext.Provider value={permission}>{children}</PermissionContext.Provider>; return (
<OrgPermissionContext.Provider value={permission}>{children}</OrgPermissionContext.Provider>
);
}; };
export const useOrgPermission = () => { export const useOrgPermission = () => {
const ctx = useContext(PermissionContext); const ctx = useContext(OrgPermissionContext);
if (!ctx) { if (!ctx) {
throw new Error("useOrgPermission to be used within <OrgPermissionProvider>"); throw new Error("useOrgPermission to be used within <OrgPermissionProvider>");
} }
@@ -1 +1,7 @@
export { OrgPermissionProvider, useOrgPermission } from "./OrgPermissionContext"; export { OrgPermissionProvider, useOrgPermission } from "./OrgPermissionContext";
export type { TOrgPermission } from "./types";
export {
OrgGeneralPermissionActions,
OrgPermissionSubjects,
OrgWorkspacePermissionActions
} from "./types";
@@ -1,13 +1,13 @@
import { MongoAbility } from "@casl/ability"; import { MongoAbility } from "@casl/ability";
export enum GeneralPermissionActions { export enum OrgGeneralPermissionActions {
Read = "read", Read = "read",
Create = "create", Create = "create",
Edit = "edit", Edit = "edit",
Delete = "delete" Delete = "delete"
} }
export enum WorkspacePermissionActions { export enum OrgWorkspacePermissionActions {
Read = "read", Read = "read",
Create = "create" Create = "create"
} }
@@ -24,13 +24,13 @@ export enum OrgPermissionSubjects {
} }
export type OrgPermissionSet = export type OrgPermissionSet =
| [WorkspacePermissionActions, OrgPermissionSubjects.Workspace] | [OrgWorkspacePermissionActions, OrgPermissionSubjects.Workspace]
| [GeneralPermissionActions, OrgPermissionSubjects.Role] | [OrgGeneralPermissionActions, OrgPermissionSubjects.Role]
| [GeneralPermissionActions, OrgPermissionSubjects.Member] | [OrgGeneralPermissionActions, OrgPermissionSubjects.Member]
| [GeneralPermissionActions, OrgPermissionSubjects.Settings] | [OrgGeneralPermissionActions, OrgPermissionSubjects.Settings]
| [GeneralPermissionActions, OrgPermissionSubjects.IncidentAccount] | [OrgGeneralPermissionActions, OrgPermissionSubjects.IncidentAccount]
| [GeneralPermissionActions, OrgPermissionSubjects.Sso] | [OrgGeneralPermissionActions, OrgPermissionSubjects.Sso]
| [GeneralPermissionActions, OrgPermissionSubjects.SecretScanning] | [OrgGeneralPermissionActions, OrgPermissionSubjects.SecretScanning]
| [GeneralPermissionActions, OrgPermissionSubjects.Billing]; | [OrgGeneralPermissionActions, OrgPermissionSubjects.Billing];
export type TPermission = MongoAbility<OrgPermissionSet>; export type TOrgPermission = MongoAbility<OrgPermissionSet>;
+7 -1
View File
@@ -1,6 +1,12 @@
export { AuthProvider } from "./AuthContext"; export { AuthProvider } from "./AuthContext";
export { OrgProvider, useOrganization } from "./OrganizationContext"; export { OrgProvider, useOrganization } from "./OrganizationContext";
export { OrgPermissionProvider,useOrgPermission } from "./OrgPermissionContext"; export type { TOrgPermission } from "./OrgPermissionContext";
export {
OrgGeneralPermissionActions,
OrgPermissionSubjects,
OrgWorkspacePermissionActions
} from "./OrgPermissionContext";
export { OrgPermissionProvider, useOrgPermission } from "./OrgPermissionContext";
export { SubscriptionProvider, useSubscription } from "./SubscriptionContext"; export { SubscriptionProvider, useSubscription } from "./SubscriptionContext";
export { UserProvider, useUser } from "./UserContext"; export { UserProvider, useUser } from "./UserContext";
export { useWorkspace, WorkspaceProvider } from "./WorkspaceContext"; export { useWorkspace, WorkspaceProvider } from "./WorkspaceContext";
+1
View File
@@ -0,0 +1 @@
export { withPermission } from "./withPermission";
@@ -0,0 +1 @@
export { withPermission } from "./withPermission";
@@ -0,0 +1,62 @@
import { ComponentType } from "react";
import { Abilities, AbilityTuple, Generics, SubjectType } from "@casl/ability";
import { faLock } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { twMerge } from "tailwind-merge";
import { TOrgPermission, useOrgPermission } from "@app/context";
type Props<T extends Abilities> = (T extends AbilityTuple
? {
action: T[0];
subject: Extract<T[1], SubjectType>;
}
: {
action: string;
subject: string;
}) & { className?: string; containerClassName?: string };
export const withPermission = <T extends {}, J extends TOrgPermission>(
Component: ComponentType<T>,
{ action, subject, className, containerClassName }: Props<Generics<J>["abilities"]>
) => {
const HOC = (hocProps: T) => {
const permission = useOrgPermission();
// akhilmhdh: Set as any due to casl/react ts type bug
// REASON: casl due to its type checking can't seem to union even if union intersection is applied
if (permission.cannot(action as any, subject)) {
return (
<div
className={twMerge(
"container h-full mx-auto flex justify-center items-center",
containerClassName
)}
>
<div
className={twMerge(
"rounded-md bg-mineshaft-800 text-bunker-300 p-16 flex space-x-12 items-end",
className
)}
>
<div>
<FontAwesomeIcon icon={faLock} size="6x" />
</div>
<div>
<div className="text-4xl font-medium mb-2">Permission Denied</div>
<div className="text-sm">
You do not have permission to this page. <br /> Kindly contact your organization
administrator
</div>
</div>
</div>
</div>
);
}
return <Component {...hocProps} />;
};
HOC.displayName = "WithPermission";
return HOC;
};
+10 -3
View File
@@ -1,9 +1,12 @@
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import Head from "next/head"; import Head from "next/head";
import { OrgGeneralPermissionActions, OrgPermissionSubjects, TOrgPermission } from "@app/context";
import { withPermission } from "@app/hoc";
import { BillingSettingsPage } from "@app/views/Settings/BillingSettingsPage"; import { BillingSettingsPage } from "@app/views/Settings/BillingSettingsPage";
export default function SettingsBilling() { const SettingsBilling = withPermission<{}, TOrgPermission>(
() => {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
@@ -16,6 +19,10 @@ export default function SettingsBilling() {
<BillingSettingsPage /> <BillingSettingsPage />
</div> </div>
); );
} },
{ action: OrgGeneralPermissionActions.Delete, subject: OrgPermissionSubjects.Billing }
);
SettingsBilling.requireAuth = true; Object.assign(SettingsBilling, { requireAuth: true });
export default SettingsBilling;
+42 -10
View File
@@ -31,6 +31,7 @@ import * as Tabs from "@radix-ui/react-tabs";
import * as yup from "yup"; import * as yup from "yup";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import { OrgPermissionCan } from "@app/components/permissions";
import onboardingCheck from "@app/components/utilities/checks/OnboardingCheck"; import onboardingCheck from "@app/components/utilities/checks/OnboardingCheck";
import { import {
Button, Button,
@@ -42,9 +43,22 @@ import {
Skeleton, Skeleton,
UpgradePlanModal UpgradePlanModal
} from "@app/components/v2"; } from "@app/components/v2";
import { useSubscription, useUser, useWorkspace } from "@app/context";
import { fetchOrgUsers, useAddUserToWs, useCreateWorkspace, useRegisterUserAction,useUploadWsKey } from "@app/hooks/api";
import { useFetchServerStatus } from "@app/hooks/api/serverDetails"; import { useFetchServerStatus } from "@app/hooks/api/serverDetails";
import {
OrgPermissionSubjects,
OrgWorkspacePermissionActions,
useSubscription,
useUser,
useWorkspace
} from "@app/context";
import { withPermission } from "@app/hoc";
import {
fetchOrgUsers,
useAddUserToWs,
useCreateWorkspace,
useRegisterUserAction,
useUploadWsKey
} from "@app/hooks/api";
import { usePopUp } from "@app/hooks/usePopUp"; import { usePopUp } from "@app/hooks/usePopUp";
import { encryptAssymmetric } from "../../../../components/utilities/cryptography/crypto"; import { encryptAssymmetric } from "../../../../components/utilities/cryptography/crypto";
@@ -301,9 +315,7 @@ const LearningItem = ({
tabIndex={0} tabIndex={0}
onClick={async () => { onClick={async () => {
if (userAction && userAction !== "first_time_secrets_pushed") { if (userAction && userAction !== "first_time_secrets_pushed") {
await registerUserAction.mutateAsync( await registerUserAction.mutateAsync(userAction);
userAction
);
} }
}} }}
className={`group relative flex h-[5.5rem] w-full items-center justify-between overflow-hidden rounded-md border ${ className={`group relative flex h-[5.5rem] w-full items-center justify-between overflow-hidden rounded-md border ${
@@ -446,7 +458,8 @@ type TAddProjectFormData = yup.InferType<typeof formSchema>;
// #TODO: Update all the workspaceIds // #TODO: Update all the workspaceIds
export default function Organization() { const OrganizationPage = withPermission(
() => {
const { t } = useTranslation(); const { t } = useTranslation();
const router = useRouter(); const router = useRouter();
@@ -576,7 +589,13 @@ export default function Organization() {
onChange={(e) => setSearchFilter(e.target.value)} onChange={(e) => setSearchFilter(e.target.value)}
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />} leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
/> />
<OrgPermissionCan
I={OrgWorkspacePermissionActions.Create}
an={OrgPermissionSubjects.Workspace}
>
{(isAllowed) => (
<Button <Button
isDisabled={!isAllowed}
colorSchema="primary" colorSchema="primary"
leftIcon={<FontAwesomeIcon icon={faPlus} />} leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => { onClick={() => {
@@ -590,6 +609,8 @@ export default function Organization() {
> >
Add New Project Add New Project
</Button> </Button>
)}
</OrgPermissionCan>
</div> </div>
<div className="mt-4 grid w-full grid-cols-1 gap-4 lg:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4"> <div className="mt-4 grid w-full grid-cols-1 gap-4 lg:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4">
{isWorkspaceLoading && {isWorkspaceLoading &&
@@ -722,7 +743,9 @@ export default function Organization() {
</div> </div>
</div> </div>
<div <div
className={`w-28 pr-4 text-right text-sm font-semibold ${false && "text-green"}`} className={`w-28 pr-4 text-right text-sm font-semibold ${
false && "text-green"
}`}
> >
About 2 min About 2 min
</div> </div>
@@ -759,7 +782,9 @@ export default function Organization() {
{feature.description} {feature.description}
</div> </div>
<div className="flex w-full items-center"> <div className="flex w-full items-center">
<div className="text-[15px] font-light text-mineshaft-300">Setup time: 20 min</div> <div className="text-[15px] font-light text-mineshaft-300">
Setup time: 20 min
</div>
<a <a
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
@@ -850,6 +875,13 @@ export default function Organization() {
{/* <DeleteUserDialog isOpen={isDeleteOpen} closeModal={closeDeleteModal} submitModal={deleteMembership} userIdToBeDeleted={userIdToBeDeleted}/> */} {/* <DeleteUserDialog isOpen={isDeleteOpen} closeModal={closeDeleteModal} submitModal={deleteMembership} userIdToBeDeleted={userIdToBeDeleted}/> */}
</div> </div>
); );
} },
{
action: OrgWorkspacePermissionActions.Read,
subject: OrgPermissionSubjects.Workspace
}
);
Organization.requireAuth = true; Object.assign(OrganizationPage, { requireAuth: true });
export default OrganizationPage;
@@ -1,48 +1,60 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import Head from "next/head"; import Head from "next/head";
import { useRouter } from "next/router" import { useRouter } from "next/router";
import { OrgPermissionCan } from "@app/components/permissions";
import { Button } from "@app/components/v2"; import { Button } from "@app/components/v2";
import { OrgGeneralPermissionActions, OrgPermissionSubjects } from "@app/context";
import { withPermission } from "@app/hoc";
import { SecretScanningLogsTable } from "@app/views/SecretScanning/components"; import { SecretScanningLogsTable } from "@app/views/SecretScanning/components";
import createNewIntegrationSession from "../../../api/secret-scanning/createSecretScanningSession"; import createNewIntegrationSession from "../../../api/secret-scanning/createSecretScanningSession";
import getInstallationStatus from "../../../api/secret-scanning/getInstallationStatus"; import getInstallationStatus from "../../../api/secret-scanning/getInstallationStatus";
import linkGitAppInstallationWithOrganization from "../../../api/secret-scanning/linkGitAppInstallationWithOrganization"; import linkGitAppInstallationWithOrganization from "../../../api/secret-scanning/linkGitAppInstallationWithOrganization";
export default function SecretScanning() { const SecretScanning = withPermission(
const router = useRouter() () => {
const queryParams = router.query const router = useRouter();
const [integrationEnabled, setIntegrationStatus] = useState(false) const queryParams = router.query;
const [integrationEnabled, setIntegrationStatus] = useState(false);
useEffect(()=>{ useEffect(() => {
const linkInstallation = async () => { const linkInstallation = async () => {
if (typeof queryParams.state === "string" && typeof queryParams.installation_id === "string"){ if (
typeof queryParams.state === "string" &&
typeof queryParams.installation_id === "string"
) {
try { try {
const isLinked = await linkGitAppInstallationWithOrganization(queryParams.installation_id as string, queryParams.state as string) const isLinked = await linkGitAppInstallationWithOrganization(
if (isLinked){ queryParams.installation_id as string,
router.reload() queryParams.state as string
);
if (isLinked) {
router.reload();
} }
console.log("installation verification complete") console.log("installation verification complete");
}catch (e){ } catch (e) {
console.log("app installation is stale, start new session", e) console.log("app installation is stale, start new session", e);
}
} }
} }
};
const fetchInstallationStatus = async () => { const fetchInstallationStatus = async () => {
const status = await getInstallationStatus(String(localStorage.getItem("orgData.id"))) const status = await getInstallationStatus(String(localStorage.getItem("orgData.id")));
setIntegrationStatus(status) setIntegrationStatus(status);
} };
fetchInstallationStatus() fetchInstallationStatus();
linkInstallation() linkInstallation();
},[queryParams.state, queryParams.installation_id]) }, [queryParams.state, queryParams.installation_id]);
const generateNewIntegrationSession = async () => { const generateNewIntegrationSession = async () => {
const session = await createNewIntegrationSession(String(localStorage.getItem("orgData.id"))) const session = await createNewIntegrationSession(String(localStorage.getItem("orgData.id")));
router.push(`https://github.com/apps/infisical-radar/installations/new?state=${session.sessionId}`) router.push(
} `https://github.com/apps/infisical-radar/installations/new?state=${session.sessionId}`
);
};
return ( return (
<div> <div>
@@ -54,28 +66,56 @@ export default function SecretScanning() {
<div className="flex justify-center bg-bunker-800 text-white w-full h-full"> <div className="flex justify-center bg-bunker-800 text-white w-full h-full">
<div className="max-w-7xl px-6 w-full"> <div className="max-w-7xl px-6 w-full">
<div className="mt-6 text-3xl font-semibold text-gray-200">Secret Scanning</div> <div className="mt-6 text-3xl font-semibold text-gray-200">Secret Scanning</div>
<div className="mb-6 text-lg text-mineshaft-300">Automatically monitor your GitHub activity and prevent secret leaks</div> <div className="mb-6 text-lg text-mineshaft-300">
Automatically monitor your GitHub activity and prevent secret leaks
</div>
<div className="relative flex justify-between bg-mineshaft-800 border border-mineshaft-600 rounded-md p-6 mb-6"> <div className="relative flex justify-between bg-mineshaft-800 border border-mineshaft-600 rounded-md p-6 mb-6">
<div className="flex flex-col items-start"> <div className="flex flex-col items-start">
<div className="flex flex-row mb-1">Secret Scanning Status: {integrationEnabled ? <p className="text-green ml-1.5 font-semibold">Enabled</p> : <p className="text-red ml-1.5 font-semibold">Not enabled</p>}</div> <div className="flex flex-row mb-1">
<div>{integrationEnabled ? <p className="text-mineshaft-300">Your GitHub organization is connected to Infisical, and is being continuously monitored for secret leaks.</p> : <p className="text-mineshaft-300">Connect your GitHub organization to Infisical.</p>}</div> Secret Scanning Status:{" "}
{integrationEnabled ? (
<p className="text-green ml-1.5 font-semibold">Enabled</p>
) : (
<p className="text-red ml-1.5 font-semibold">Not enabled</p>
)}
</div>
<div>
{integrationEnabled ? (
<p className="text-mineshaft-300">
Your GitHub organization is connected to Infisical, and is being continuously
monitored for secret leaks.
</p>
) : (
<p className="text-mineshaft-300">
Connect your GitHub organization to Infisical.
</p>
)}
</div>
</div> </div>
{integrationEnabled ? ( {integrationEnabled ? (
<div> <div>
<div className="absolute right-[2.5rem] top-[2.5rem] animate-ping rounded-full h-6 w-6 bg-green flex items-center justify-center"/> <div className="absolute right-[2.5rem] top-[2.5rem] animate-ping rounded-full h-6 w-6 bg-green flex items-center justify-center" />
<div className="absolute right-[2.63rem] top-[2.63rem] animate-ping rounded-full h-5 w-5 bg-green flex items-center justify-center"/> <div className="absolute right-[2.63rem] top-[2.63rem] animate-ping rounded-full h-5 w-5 bg-green flex items-center justify-center" />
<div className="absolute right-[2.82rem] top-[2.82rem] animate-ping rounded-full h-3.5 w-3.5 bg-green flex items-center justify-center"/> <div className="absolute right-[2.82rem] top-[2.82rem] animate-ping rounded-full h-3.5 w-3.5 bg-green flex items-center justify-center" />
</div> </div>
) : ( ) : (
<div className="flex items-center h-[3.25rem]"> <div className="flex items-center h-[3.25rem]">
<OrgPermissionCan
I={OrgGeneralPermissionActions.Create}
a={OrgPermissionSubjects.SecretScanning}
>
{(isAllowed) => (
<Button <Button
variant="solid" variant="solid"
colorSchema="primary" colorSchema="primary"
onClick={generateNewIntegrationSession} onClick={generateNewIntegrationSession}
className="py-2 h-min" className="py-2 h-min"
isDisabled={!isAllowed}
> >
Integrate with GitHub Integrate with GitHub
</Button> </Button>
)}
</OrgPermissionCan>
</div> </div>
)} )}
</div> </div>
@@ -84,6 +124,10 @@ export default function SecretScanning() {
</div> </div>
</div> </div>
); );
} },
{ action: OrgGeneralPermissionActions.Read, subject: OrgPermissionSubjects.SecretScanning }
);
SecretScanning.requireAuth = true; Object.assign(SecretScanning, { requireAuth: true });
export default SecretScanning;
@@ -3,7 +3,8 @@ import { useTranslation } from "react-i18next";
import { motion } from "framer-motion"; import { motion } from "framer-motion";
import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2"; import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
import { useOrganization } from "@app/context"; import { OrgGeneralPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
import { withPermission } from "@app/hoc";
import { useGetRoles } from "@app/hooks/api"; import { useGetRoles } from "@app/hooks/api";
import { OrgMembersTable } from "./components/OrgMembersTable"; import { OrgMembersTable } from "./components/OrgMembersTable";
@@ -14,7 +15,8 @@ enum TabSections {
Roles = "roles" Roles = "roles"
} }
export const MembersPage = () => { export const MembersPage = withPermission(
() => {
const { t } = useTranslation(); const { t } = useTranslation();
const { currentOrg } = useOrganization(); const { currentOrg } = useOrganization();
@@ -55,4 +57,6 @@ export const MembersPage = () => {
</div> </div>
</div> </div>
); );
}; },
{ action: OrgGeneralPermissionActions.Read, subject: OrgPermissionSubjects.Member }
);
@@ -14,6 +14,7 @@ import { yupResolver } from "@hookform/resolvers/yup";
import * as yup from "yup"; import * as yup from "yup";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import { OrgPermissionCan } from "@app/components/permissions";
import { import {
decryptAssymmetric, decryptAssymmetric,
encryptAssymmetric encryptAssymmetric
@@ -41,7 +42,14 @@ import {
Tr, Tr,
UpgradePlanModal UpgradePlanModal
} from "@app/components/v2"; } from "@app/components/v2";
import { useOrganization, useSubscription, useUser, useWorkspace } from "@app/context"; import {
OrgGeneralPermissionActions,
OrgPermissionSubjects,
useOrganization,
useSubscription,
useUser,
useWorkspace
} from "@app/context";
import { usePopUp, useToggle } from "@app/hooks"; import { usePopUp, useToggle } from "@app/hooks";
import { import {
useAddUserToOrg, useAddUserToOrg,
@@ -297,7 +305,10 @@ export const OrgMembersTable = ({ roles = [] }: Props) => {
placeholder="Search members..." placeholder="Search members..."
/> />
</div> </div>
<OrgPermissionCan I={OrgGeneralPermissionActions.Create} a={OrgPermissionSubjects.Member}>
{(isAllowed) => (
<Button <Button
isDisabled={!isAllowed}
leftIcon={<FontAwesomeIcon icon={faPlus} />} leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => { onClick={() => {
if (!isLoadingSSOConfig && ssoConfig && ssoConfig.isActive) { if (!isLoadingSSOConfig && ssoConfig && ssoConfig.isActive) {
@@ -318,6 +329,8 @@ export const OrgMembersTable = ({ roles = [] }: Props) => {
> >
Add Member Add Member
</Button> </Button>
)}
</OrgPermissionCan>
</div> </div>
<div> <div>
<TableContainer> <TableContainer>
@@ -345,12 +358,18 @@ export const OrgMembersTable = ({ roles = [] }: Props) => {
<Td>{name}</Td> <Td>{name}</Td>
<Td>{email}</Td> <Td>{email}</Td>
<Td> <Td>
<OrgPermissionCan
I={OrgGeneralPermissionActions.Edit}
a={OrgPermissionSubjects.Member}
>
{(isAllowed) => (
<>
{status === "accepted" && ( {status === "accepted" && (
<Select <Select
defaultValue={ defaultValue={
role === "custom" ? findRoleFromId(customRole)?.slug : role role === "custom" ? findRoleFromId(customRole)?.slug : role
} }
isDisabled={userId === u?._id} isDisabled={userId === u?._id || !isAllowed}
className="w-40 bg-mineshaft-600" className="w-40 bg-mineshaft-600"
dropdownContainerClassName="border border-mineshaft-600 bg-mineshaft-800" dropdownContainerClassName="border border-mineshaft-600 bg-mineshaft-800"
onValueChange={(selectedRole) => onValueChange={(selectedRole) =>
@@ -371,6 +390,7 @@ export const OrgMembersTable = ({ roles = [] }: Props) => {
{(status === "invited" || status === "verified") && {(status === "invited" || status === "verified") &&
serverDetails?.emailConfigured && ( serverDetails?.emailConfigured && (
<Button <Button
isDisabled={!isAllowed}
className="w-40" className="w-40"
colorSchema="primary" colorSchema="primary"
variant="outline_bg" variant="outline_bg"
@@ -382,11 +402,15 @@ export const OrgMembersTable = ({ roles = [] }: Props) => {
{status === "completed" && ( {status === "completed" && (
<Button <Button
colorSchema="secondary" colorSchema="secondary"
isDisabled={!isAllowed}
onClick={() => onGrantAccess(u?._id, u?.publicKey)} onClick={() => onGrantAccess(u?._id, u?.publicKey)}
> >
Grant Access Grant Access
</Button> </Button>
)} )}
</>
)}
</OrgPermissionCan>
</Td> </Td>
<Td> <Td>
{userWs ? ( {userWs ? (
@@ -428,10 +452,15 @@ export const OrgMembersTable = ({ roles = [] }: Props) => {
</Td> </Td>
<Td> <Td>
{userId !== u?._id && ( {userId !== u?._id && (
<OrgPermissionCan
I={OrgGeneralPermissionActions.Delete}
a={OrgPermissionSubjects.Member}
>
{(isAllowed) => (
<IconButton <IconButton
ariaLabel="delete" ariaLabel="delete"
colorSchema="danger" colorSchema="danger"
isDisabled={userId === u?._id} isDisabled={userId === u?._id || !isAllowed}
onClick={() => onClick={() =>
handlePopUpOpen("removeMember", { id: orgMembershipId }) handlePopUpOpen("removeMember", { id: orgMembershipId })
} }
@@ -439,6 +468,8 @@ export const OrgMembersTable = ({ roles = [] }: Props) => {
<FontAwesomeIcon icon={faTrash} /> <FontAwesomeIcon icon={faTrash} />
</IconButton> </IconButton>
)} )}
</OrgPermissionCan>
)}
</Td> </Td>
</Tr> </Tr>
); );
@@ -38,10 +38,9 @@ export const BillingPermission = ({ isNonEditable, setValue, control }: Props) =
const [isCustom, setIsCustom] = useToggle(); const [isCustom, setIsCustom] = useToggle();
const selectedPermissionCategory = useMemo(() => { const selectedPermissionCategory = useMemo(() => {
let score = 0;
const actions = Object.keys(rule || {}) as Array<keyof typeof rule>; const actions = Object.keys(rule || {}) as Array<keyof typeof rule>;
const totalActions = PERMISSIONS.length; const totalActions = PERMISSIONS.length;
actions.forEach((key) => (score += rule[key] ? 1 : 0)); const score = actions.map((key) => (rule[key] ? 1 : 0)).reduce((a, b) => a + b, 0 as number);
if (isCustom) return Permission.Custom; if (isCustom) return Permission.Custom;
if (score === 0) return Permission.NoAccess; if (score === 0) return Permission.NoAccess;
@@ -52,11 +51,14 @@ export const BillingPermission = ({ isNonEditable, setValue, control }: Props) =
}, [rule, isCustom]); }, [rule, isCustom]);
useEffect(() => { useEffect(() => {
selectedPermissionCategory === Permission.Custom ? setIsCustom.on() : setIsCustom.off(); if (selectedPermissionCategory === Permission.Custom) setIsCustom.on();
else setIsCustom.off();
}, [selectedPermissionCategory]); }, [selectedPermissionCategory]);
const handlePermissionChange = (val: Permission) => { const handlePermissionChange = (val: Permission) => {
val === Permission.Custom ? setIsCustom.on() : setIsCustom.off(); if (val === Permission.Custom) setIsCustom.on();
else setIsCustom.off();
switch (val) { switch (val) {
case Permission.NoAccess: case Permission.NoAccess:
setValue( setValue(
@@ -38,10 +38,9 @@ export const IncidentContactPermission = ({ isNonEditable, setValue, control }:
const [isCustom, setIsCustom] = useToggle(); const [isCustom, setIsCustom] = useToggle();
const selectedPermissionCategory = useMemo(() => { const selectedPermissionCategory = useMemo(() => {
let score = 0;
const actions = Object.keys(rule || {}) as Array<keyof typeof rule>; const actions = Object.keys(rule || {}) as Array<keyof typeof rule>;
const totalActions = PERMISSIONS.length; const totalActions = PERMISSIONS.length;
actions.forEach((key) => (score += rule[key] ? 1 : 0)); const score = actions.map((key) => (rule[key] ? 1 : 0)).reduce((a, b) => a + b, 0 as number);
if (isCustom) return Permission.Custom; if (isCustom) return Permission.Custom;
if (score === 0) return Permission.NoAccess; if (score === 0) return Permission.NoAccess;
@@ -52,14 +51,16 @@ export const IncidentContactPermission = ({ isNonEditable, setValue, control }:
}, [rule, isCustom]); }, [rule, isCustom]);
useEffect(() => { useEffect(() => {
selectedPermissionCategory === Permission.Custom ? setIsCustom.on() : setIsCustom.off(); if (selectedPermissionCategory === Permission.Custom) setIsCustom.on();
else setIsCustom.off();
}, [selectedPermissionCategory]); }, [selectedPermissionCategory]);
const handlePermissionChange = (val: Permission) => { const handlePermissionChange = (val: Permission) => {
val === Permission.Custom ? setIsCustom.on() : setIsCustom.off(); if (val === Permission.Custom) setIsCustom.on();
else setIsCustom.off();
switch (val) { switch (val) {
case Permission.NoAccess: case Permission.NoAccess:
setIsCustom.off();
setValue( setValue(
"permissions.incident-contact", "permissions.incident-contact",
{ read: false, edit: false, create: false, delete: false }, { read: false, edit: false, create: false, delete: false },
@@ -67,7 +68,6 @@ export const IncidentContactPermission = ({ isNonEditable, setValue, control }:
); );
break; break;
case Permission.FullAccess: case Permission.FullAccess:
setIsCustom.off();
setValue( setValue(
"permissions.incident-contact", "permissions.incident-contact",
{ read: true, edit: true, create: true, delete: true }, { read: true, edit: true, create: true, delete: true },
@@ -75,7 +75,6 @@ export const IncidentContactPermission = ({ isNonEditable, setValue, control }:
); );
break; break;
case Permission.ReadOnly: case Permission.ReadOnly:
setIsCustom.off();
setValue( setValue(
"permissions.incident-contact", "permissions.incident-contact",
{ read: true, edit: false, create: false, delete: false }, { read: true, edit: false, create: false, delete: false },
@@ -83,7 +82,6 @@ export const IncidentContactPermission = ({ isNonEditable, setValue, control }:
); );
break; break;
default: default:
setIsCustom.on();
setValue( setValue(
"permissions.incident-contact", "permissions.incident-contact",
{ read: false, edit: false, create: false, delete: false }, { read: false, edit: false, create: false, delete: false },
@@ -31,31 +31,34 @@ const PERMISSIONS = [
] as const; ] as const;
export const MemberPermission = ({ isNonEditable, setValue, control }: Props) => { export const MemberPermission = ({ isNonEditable, setValue, control }: Props) => {
const memberRule = useWatch({ const rule = useWatch({
control, control,
name: "permissions.member" name: "permissions.member"
}); });
const [isCustom, setIsCustom] = useToggle(); const [isCustom, setIsCustom] = useToggle();
const selectedPermissionCategory = useMemo(() => { const selectedPermissionCategory = useMemo(() => {
let score = 0; const actions = Object.keys(rule || {}) as Array<keyof typeof rule>;
const actions = Object.keys(memberRule || {}) as Array<keyof typeof memberRule>;
const totalActions = PERMISSIONS.length; const totalActions = PERMISSIONS.length;
actions.forEach((key) => (score += memberRule[key] ? 1 : 0)); const score = actions.map((key) => (rule[key] ? 1 : 0)).reduce((a, b) => a + b, 0 as number);
if (isCustom) return Permission.Custom; if (isCustom) return Permission.Custom;
if (score === 0) return Permission.NoAccess; if (score === 0) return Permission.NoAccess;
if (score === totalActions) return Permission.FullAccess; if (score === totalActions) return Permission.FullAccess;
if (score === 1 && memberRule.read) return Permission.ReadOnly; if (score === 1 && rule.read) return Permission.ReadOnly;
return Permission.Custom; return Permission.Custom;
}, [memberRule, isCustom]); }, [rule, isCustom]);
useEffect(() => { useEffect(() => {
selectedPermissionCategory === Permission.Custom ? setIsCustom.on() : setIsCustom.off(); if (selectedPermissionCategory === Permission.Custom) setIsCustom.on();
else setIsCustom.off();
}, [selectedPermissionCategory]); }, [selectedPermissionCategory]);
const handlePermissionChange = (val: Permission) => { const handlePermissionChange = (val: Permission) => {
if (val === Permission.Custom) setIsCustom.on();
else setIsCustom.off();
switch (val) { switch (val) {
case Permission.NoAccess: case Permission.NoAccess:
setValue( setValue(
@@ -31,34 +31,36 @@ const PERMISSIONS = [
] as const; ] as const;
export const RolePermission = ({ isNonEditable, setValue, control }: Props) => { export const RolePermission = ({ isNonEditable, setValue, control }: Props) => {
const roleRule = useWatch({ const rule = useWatch({
control, control,
name: "permissions.role" name: "permissions.role"
}); });
const [isCustom, setIsCustom] = useToggle(); const [isCustom, setIsCustom] = useToggle();
const selectedPermissionCategory = useMemo(() => { const selectedPermissionCategory = useMemo(() => {
let score = 0; const actions = Object.keys(rule || {}) as Array<keyof typeof rule>;
const actions = Object.keys(roleRule || {}) as Array<keyof typeof roleRule>;
const totalActions = PERMISSIONS.length; const totalActions = PERMISSIONS.length;
actions.forEach((key) => (score += roleRule[key] ? 1 : 0)); const score = actions.map((key) => (rule[key] ? 1 : 0)).reduce((a, b) => a + b, 0 as number);
if (isCustom) return Permission.Custom; if (isCustom) return Permission.Custom;
if (score === 0) return Permission.NoAccess; if (score === 0) return Permission.NoAccess;
if (score === totalActions) return Permission.FullAccess; if (score === totalActions) return Permission.FullAccess;
if (score === 1 && roleRule.read) return Permission.ReadOnly; if (score === 1 && rule.read) return Permission.ReadOnly;
return Permission.Custom; return Permission.Custom;
}, [roleRule, isCustom]); }, [rule, isCustom]);
useEffect(() => { useEffect(() => {
selectedPermissionCategory === Permission.Custom ? setIsCustom.on() : setIsCustom.off(); if (selectedPermissionCategory === Permission.Custom) setIsCustom.on();
else setIsCustom.off();
}, [selectedPermissionCategory]); }, [selectedPermissionCategory]);
const handlePermissionChange = (val: Permission) => { const handlePermissionChange = (val: Permission) => {
if (val === Permission.Custom) setIsCustom.on();
else setIsCustom.off();
switch (val) { switch (val) {
case Permission.NoAccess: case Permission.NoAccess:
setIsCustom.off();
setValue( setValue(
"permissions.role", "permissions.role",
{ read: false, edit: false, create: false, delete: false }, { read: false, edit: false, create: false, delete: false },
@@ -66,7 +68,6 @@ export const RolePermission = ({ isNonEditable, setValue, control }: Props) => {
); );
break; break;
case Permission.FullAccess: case Permission.FullAccess:
setIsCustom.off();
setValue( setValue(
"permissions.role", "permissions.role",
{ read: true, edit: true, create: true, delete: true }, { read: true, edit: true, create: true, delete: true },
@@ -74,7 +75,6 @@ export const RolePermission = ({ isNonEditable, setValue, control }: Props) => {
); );
break; break;
case Permission.ReadOnly: case Permission.ReadOnly:
setIsCustom.off();
setValue( setValue(
"permissions.role", "permissions.role",
{ read: true, edit: false, create: false, delete: false }, { read: true, edit: false, create: false, delete: false },
@@ -82,7 +82,6 @@ export const RolePermission = ({ isNonEditable, setValue, control }: Props) => {
); );
break; break;
default: default:
setIsCustom.on();
setValue( setValue(
"permissions.role", "permissions.role",
{ read: false, edit: false, create: false, delete: false }, { read: false, edit: false, create: false, delete: false },
@@ -38,10 +38,9 @@ export const SecretScannigPermission = ({ isNonEditable, setValue, control }: Pr
const [isCustom, setIsCustom] = useToggle(); const [isCustom, setIsCustom] = useToggle();
const selectedPermissionCategory = useMemo(() => { const selectedPermissionCategory = useMemo(() => {
let score = 0;
const actions = Object.keys(rule || {}) as Array<keyof typeof rule>; const actions = Object.keys(rule || {}) as Array<keyof typeof rule>;
const totalActions = PERMISSIONS.length; const totalActions = PERMISSIONS.length;
actions.forEach((key) => (score += rule[key] ? 1 : 0)); const score = actions.map((key) => (rule[key] ? 1 : 0)).reduce((a, b) => a + b, 0 as number);
if (isCustom) return Permission.Custom; if (isCustom) return Permission.Custom;
if (score === 0) return Permission.NoAccess; if (score === 0) return Permission.NoAccess;
@@ -52,13 +51,16 @@ export const SecretScannigPermission = ({ isNonEditable, setValue, control }: Pr
}, [rule, isCustom]); }, [rule, isCustom]);
useEffect(() => { useEffect(() => {
selectedPermissionCategory === Permission.Custom ? setIsCustom.on() : setIsCustom.off(); if (selectedPermissionCategory === Permission.Custom) setIsCustom.on();
else setIsCustom.off();
}, [selectedPermissionCategory]); }, [selectedPermissionCategory]);
const handlePermissionChange = (val: Permission) => { const handlePermissionChange = (val: Permission) => {
if (val === Permission.Custom) setIsCustom.on();
else setIsCustom.off();
switch (val) { switch (val) {
case Permission.NoAccess: case Permission.NoAccess:
setIsCustom.off();
setValue( setValue(
"permissions.secret-scanning", "permissions.secret-scanning",
{ read: false, edit: false, create: false, delete: false }, { read: false, edit: false, create: false, delete: false },
@@ -66,7 +68,6 @@ export const SecretScannigPermission = ({ isNonEditable, setValue, control }: Pr
); );
break; break;
case Permission.FullAccess: case Permission.FullAccess:
setIsCustom.off();
setValue( setValue(
"permissions.secret-scanning", "permissions.secret-scanning",
{ read: true, edit: true, create: true, delete: true }, { read: true, edit: true, create: true, delete: true },
@@ -74,7 +75,6 @@ export const SecretScannigPermission = ({ isNonEditable, setValue, control }: Pr
); );
break; break;
case Permission.ReadOnly: case Permission.ReadOnly:
setIsCustom.off();
setValue( setValue(
"permissions.secret-scanning", "permissions.secret-scanning",
{ read: true, edit: false, create: false, delete: false }, { read: true, edit: false, create: false, delete: false },
@@ -82,7 +82,6 @@ export const SecretScannigPermission = ({ isNonEditable, setValue, control }: Pr
); );
break; break;
default: default:
setIsCustom.on();
setValue( setValue(
"permissions.secret-scanning", "permissions.secret-scanning",
{ read: false, edit: false, create: false, delete: false }, { read: false, edit: false, create: false, delete: false },
@@ -38,10 +38,9 @@ export const SettingsPermission = ({ isNonEditable, setValue, control }: Props)
const [isCustom, setIsCustom] = useToggle(); const [isCustom, setIsCustom] = useToggle();
const selectedPermissionCategory = useMemo(() => { const selectedPermissionCategory = useMemo(() => {
let score = 0;
const actions = Object.keys(rule || {}) as Array<keyof typeof rule>; const actions = Object.keys(rule || {}) as Array<keyof typeof rule>;
const totalActions = PERMISSIONS.length; const totalActions = PERMISSIONS.length;
actions.forEach((key) => (score += rule[key] ? 1 : 0)); const score = actions.map((key) => (rule[key] ? 1 : 0)).reduce((a, b) => a + b, 0 as number);
if (isCustom) return Permission.Custom; if (isCustom) return Permission.Custom;
if (score === 0) return Permission.NoAccess; if (score === 0) return Permission.NoAccess;
@@ -52,13 +51,16 @@ export const SettingsPermission = ({ isNonEditable, setValue, control }: Props)
}, [rule, isCustom]); }, [rule, isCustom]);
useEffect(() => { useEffect(() => {
selectedPermissionCategory === Permission.Custom ? setIsCustom.on() : setIsCustom.off(); if (selectedPermissionCategory === Permission.Custom) setIsCustom.on();
else setIsCustom.off();
}, [selectedPermissionCategory]); }, [selectedPermissionCategory]);
const handlePermissionChange = (val: Permission) => { const handlePermissionChange = (val: Permission) => {
if (val === Permission.Custom) setIsCustom.on();
else setIsCustom.off();
switch (val) { switch (val) {
case Permission.NoAccess: case Permission.NoAccess:
setIsCustom.off();
setValue( setValue(
"permissions.settings", "permissions.settings",
{ read: false, edit: false, create: false, delete: false }, { read: false, edit: false, create: false, delete: false },
@@ -66,7 +68,6 @@ export const SettingsPermission = ({ isNonEditable, setValue, control }: Props)
); );
break; break;
case Permission.FullAccess: case Permission.FullAccess:
setIsCustom.off();
setValue( setValue(
"permissions.settings", "permissions.settings",
{ read: true, edit: true, create: true, delete: true }, { read: true, edit: true, create: true, delete: true },
@@ -74,7 +75,6 @@ export const SettingsPermission = ({ isNonEditable, setValue, control }: Props)
); );
break; break;
case Permission.ReadOnly: case Permission.ReadOnly:
setIsCustom.off();
setValue( setValue(
"permissions.settings", "permissions.settings",
{ read: true, edit: false, create: false, delete: false }, { read: true, edit: false, create: false, delete: false },
@@ -82,7 +82,6 @@ export const SettingsPermission = ({ isNonEditable, setValue, control }: Props)
); );
break; break;
default: default:
setIsCustom.on();
setValue( setValue(
"permissions.settings", "permissions.settings",
{ read: false, edit: false, create: false, delete: false }, { read: false, edit: false, create: false, delete: false },
@@ -38,10 +38,9 @@ export const SsoPermission = ({ isNonEditable, setValue, control }: Props) => {
const [isCustom, setIsCustom] = useToggle(); const [isCustom, setIsCustom] = useToggle();
const selectedPermissionCategory = useMemo(() => { const selectedPermissionCategory = useMemo(() => {
let score = 0;
const actions = Object.keys(rule || {}) as Array<keyof typeof rule>; const actions = Object.keys(rule || {}) as Array<keyof typeof rule>;
const totalActions = PERMISSIONS.length; const totalActions = PERMISSIONS.length;
actions.forEach((key) => (score += rule[key] ? 1 : 0)); const score = actions.map((key) => (rule[key] ? 1 : 0)).reduce((a, b) => a + b, 0 as number);
if (isCustom) return Permission.Custom; if (isCustom) return Permission.Custom;
if (score === 0) return Permission.NoAccess; if (score === 0) return Permission.NoAccess;
@@ -52,13 +51,16 @@ export const SsoPermission = ({ isNonEditable, setValue, control }: Props) => {
}, [rule, isCustom]); }, [rule, isCustom]);
useEffect(() => { useEffect(() => {
selectedPermissionCategory === Permission.Custom ? setIsCustom.on() : setIsCustom.off(); if (selectedPermissionCategory === Permission.Custom) setIsCustom.on();
else setIsCustom.off();
}, [selectedPermissionCategory]); }, [selectedPermissionCategory]);
const handlePermissionChange = (val: Permission) => { const handlePermissionChange = (val: Permission) => {
if (val === Permission.Custom) setIsCustom.on();
else setIsCustom.off();
switch (val) { switch (val) {
case Permission.NoAccess: case Permission.NoAccess:
setIsCustom.off();
setValue( setValue(
"permissions.sso", "permissions.sso",
{ read: false, edit: false, create: false, delete: false }, { read: false, edit: false, create: false, delete: false },
@@ -66,7 +68,6 @@ export const SsoPermission = ({ isNonEditable, setValue, control }: Props) => {
); );
break; break;
case Permission.FullAccess: case Permission.FullAccess:
setIsCustom.off();
setValue( setValue(
"permissions.sso", "permissions.sso",
{ read: true, edit: true, create: true, delete: true }, { read: true, edit: true, create: true, delete: true },
@@ -74,7 +75,6 @@ export const SsoPermission = ({ isNonEditable, setValue, control }: Props) => {
); );
break; break;
case Permission.ReadOnly: case Permission.ReadOnly:
setIsCustom.off();
setValue( setValue(
"permissions.sso", "permissions.sso",
{ read: true, edit: false, create: false, delete: false }, { read: true, edit: false, create: false, delete: false },
@@ -82,7 +82,6 @@ export const SsoPermission = ({ isNonEditable, setValue, control }: Props) => {
); );
break; break;
default: default:
setIsCustom.on();
setValue( setValue(
"permissions.sso", "permissions.sso",
{ read: false, edit: false, create: false, delete: false }, { read: false, edit: false, create: false, delete: false },
@@ -36,10 +36,9 @@ export const WorkspacePermission = ({ isNonEditable, setValue, control }: Props)
const [isCustom, setIsCustom] = useToggle(); const [isCustom, setIsCustom] = useToggle();
const selectedPermissionCategory = useMemo(() => { const selectedPermissionCategory = useMemo(() => {
let score = 0;
const actions = Object.keys(rule || {}) as Array<keyof typeof rule>; const actions = Object.keys(rule || {}) as Array<keyof typeof rule>;
const totalActions = PERMISSIONS.length; const totalActions = PERMISSIONS.length;
actions.forEach((key) => (score += rule[key] ? 1 : 0)); const score = actions.map((key) => (rule[key] ? 1 : 0)).reduce((a, b) => a + b, 0 as number);
if (isCustom) return Permission.Custom; if (isCustom) return Permission.Custom;
if (score === 0) return Permission.NoAccess; if (score === 0) return Permission.NoAccess;
@@ -50,11 +49,14 @@ export const WorkspacePermission = ({ isNonEditable, setValue, control }: Props)
}, [rule, isCustom]); }, [rule, isCustom]);
useEffect(() => { useEffect(() => {
selectedPermissionCategory === Permission.Custom ? setIsCustom.on() : setIsCustom.off(); if (selectedPermissionCategory === Permission.Custom) setIsCustom.on();
else setIsCustom.off();
}, [selectedPermissionCategory]); }, [selectedPermissionCategory]);
const handlePermissionChange = (val: Permission) => { const handlePermissionChange = (val: Permission) => {
val === Permission.Custom ? setIsCustom.on() : setIsCustom.off(); if (val === Permission.Custom) setIsCustom.on();
else setIsCustom.off();
switch (val) { switch (val) {
case Permission.NoAccess: case Permission.NoAccess:
setValue("permissions.workspace", { read: false, create: false }, { shouldDirty: true }); setValue("permissions.workspace", { read: false, create: false }, { shouldDirty: true });
@@ -1,28 +1,47 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { OrgPermissionCan } from "@app/components/permissions";
import { OrgGeneralPermissionActions, OrgPermissionSubjects } from "@app/context";
import updateRiskStatus, { RiskStatus } from "@app/pages/api/secret-scanning/updateRiskStatus"; import updateRiskStatus, { RiskStatus } from "@app/pages/api/secret-scanning/updateRiskStatus";
export const RiskStatusSelection = ({riskId, currentSelection}: {riskId: any, currentSelection: any }) => { export const RiskStatusSelection = ({
riskId,
currentSelection
}: {
riskId: any;
currentSelection: any;
}) => {
const [selectedRiskStatus, setSelectedRiskStatus] = useState(currentSelection); const [selectedRiskStatus, setSelectedRiskStatus] = useState(currentSelection);
useEffect(()=>{ useEffect(() => {
if (currentSelection !== selectedRiskStatus){ if (currentSelection !== selectedRiskStatus) {
const updateSelection = async () =>{ const updateSelection = async () => {
await updateRiskStatus(String(localStorage.getItem("orgData.id")), riskId, selectedRiskStatus) await updateRiskStatus(
String(localStorage.getItem("orgData.id")),
riskId,
selectedRiskStatus
);
};
updateSelection();
} }
updateSelection() }, [selectedRiskStatus]);
}
},[selectedRiskStatus])
return ( return (
<OrgPermissionCan I={OrgGeneralPermissionActions.Edit} a={OrgPermissionSubjects.SecretScanning}>
{(isAllowed) => (
<select <select
disabled={!isAllowed}
value={selectedRiskStatus} value={selectedRiskStatus}
onChange={(e) => setSelectedRiskStatus(e.target.value)} onChange={(e) => setSelectedRiskStatus(e.target.value)}
className="block w-full py-2 px-3 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500" className="block w-full py-2 px-3 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"
> >
<option>Unresolved</option> <option>Unresolved</option>
<option value={RiskStatus.RESOLVED_FALSE_POSITIVE}>This is a false positive, resolved</option> <option value={RiskStatus.RESOLVED_FALSE_POSITIVE}>
This is a false positive, resolved
</option>
<option value={RiskStatus.RESOLVED_REVOKED}>I have rotated the secret, resolved</option> <option value={RiskStatus.RESOLVED_REVOKED}>I have rotated the secret, resolved</option>
<option value={RiskStatus.RESOLVED_NOT_REVOKED}>No rotate needed, resolved</option> <option value={RiskStatus.RESOLVED_NOT_REVOKED}>No rotate needed, resolved</option>
</select> </select>
)}
</OrgPermissionCan>
); );
} };
@@ -1,5 +1,11 @@
import { OrgPermissionCan } from "@app/components/permissions";
import { Button } from "@app/components/v2"; import { Button } from "@app/components/v2";
import { useOrganization,useSubscription } from "@app/context"; import {
OrgGeneralPermissionActions,
OrgPermissionSubjects,
useOrganization,
useSubscription
} from "@app/context";
import { import {
useCreateCustomerPortalSession, useCreateCustomerPortalSession,
useGetOrgPlanBillingInfo, useGetOrgPlanBillingInfo,
@@ -16,18 +22,16 @@ export const PreviewSection = () => {
const getOrgTrialUrl = useGetOrgTrialUrl(); const getOrgTrialUrl = useGetOrgTrialUrl();
const createCustomerPortalSession = useCreateCustomerPortalSession(); const createCustomerPortalSession = useCreateCustomerPortalSession();
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp([ const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["managePlan"] as const);
"managePlan"
] as const);
const formatAmount = (amount: number) => { const formatAmount = (amount: number) => {
const formattedTotal = (Math.floor(amount) / 100).toLocaleString("en-US", { const formattedTotal = (Math.floor(amount) / 100).toLocaleString("en-US", {
style: "currency", style: "currency",
currency: "USD", currency: "USD"
}); });
return formattedTotal; return formattedTotal;
} };
const formatDate = (date: number) => { const formatDate = (date: number) => {
const createdDate = new Date(date * 1000); const createdDate = new Date(date * 1000);
@@ -37,12 +41,10 @@ export const PreviewSection = () => {
const formattedDate: string = `${day}/${month}/${year}`; const formattedDate: string = `${day}/${month}/${year}`;
return formattedDate; return formattedDate;
} };
function formatPlanSlug(slug: string) { function formatPlanSlug(slug: string) {
return slug return slug.replace(/(\b[a-z])/g, (match) => match.toUpperCase()).replace(/-/g, " ");
.replace(/(\b[a-z])/g, match => match.toUpperCase())
.replace(/-/g, " ");
} }
const handleUpgradeBtnClick = async () => { const handleUpgradeBtnClick = async () => {
@@ -64,23 +66,35 @@ export const PreviewSection = () => {
} catch (err) { } catch (err) {
console.error(err); console.error(err);
} }
} };
return ( return (
<div> <div>
{subscription && subscription?.slug !== "enterprise" && subscription?.slug !== "pro" && subscription?.slug !== "pro-annual" && ( {subscription &&
<div className="p-4 bg-mineshaft-900 rounded-lg flex-1 border border-mineshaft-600 mb-6 flex items-center bg-mineshaft-600"> subscription?.slug !== "enterprise" &&
subscription?.slug !== "pro" &&
subscription?.slug !== "pro-annual" && (
<div className="p-4 rounded-lg flex-1 border border-mineshaft-600 mb-6 flex items-center bg-mineshaft-600">
<div className="flex-1"> <div className="flex-1">
<h2 className="text-xl font-semibold text-mineshaft-50">Become Infisical</h2> <h2 className="text-xl font-semibold text-mineshaft-50">Become Infisical</h2>
<p className="text-gray-400 mt-4">Unlimited members, projects, RBAC, smart alerts, and so much more</p> <p className="text-gray-400 mt-4">
Unlimited members, projects, RBAC, smart alerts, and so much more
</p>
</div> </div>
<OrgPermissionCan
I={OrgGeneralPermissionActions.Create}
a={OrgPermissionSubjects.Billing}
>
{(isAllowed) => (
<Button <Button
// onClick={() => handlePopUpOpen("managePlan")}
onClick={() => handleUpgradeBtnClick()} onClick={() => handleUpgradeBtnClick()}
color="mineshaft" color="mineshaft"
isDisabled={!isAllowed}
> >
{!subscription.has_used_trial ? "Start Pro Free Trial" : "Upgrade Plan"} {!subscription.has_used_trial ? "Start Pro Free Trial" : "Upgrade Plan"}
</Button> </Button>
)}
</OrgPermissionCan>
</div> </div>
)} )}
{!isLoading && subscription && data && ( {!isLoading && subscription && data && (
@@ -88,8 +102,15 @@ export const PreviewSection = () => {
<div className="p-4 bg-mineshaft-900 rounded-lg flex-1 mr-4 border border-mineshaft-600"> <div className="p-4 bg-mineshaft-900 rounded-lg flex-1 mr-4 border border-mineshaft-600">
<p className="mb-2 text-gray-400">Current plan</p> <p className="mb-2 text-gray-400">Current plan</p>
<p className="text-2xl text-mineshaft-50 font-semibold mb-8"> <p className="text-2xl text-mineshaft-50 font-semibold mb-8">
{`${formatPlanSlug(subscription.slug)} ${subscription.status === "trialing" ? "(Trial)" : ""}`} {`${formatPlanSlug(subscription.slug)} ${
subscription.status === "trialing" ? "(Trial)" : ""
}`}
</p> </p>
<OrgPermissionCan
I={OrgGeneralPermissionActions.Edit}
a={OrgPermissionSubjects.Billing}
>
{(isAllowed) => (
<button <button
type="button" type="button"
onClick={async () => { onClick={async () => {
@@ -97,15 +118,20 @@ export const PreviewSection = () => {
const { url } = await createCustomerPortalSession.mutateAsync(currentOrg._id); const { url } = await createCustomerPortalSession.mutateAsync(currentOrg._id);
window.location.href = url; window.location.href = url;
}} }}
disabled={!isAllowed}
className="text-primary" className="text-primary"
> >
Manage plan &rarr; Manage plan &rarr;
</button> </button>
)}
</OrgPermissionCan>
</div> </div>
<div className="p-4 bg-mineshaft-900 rounded-lg flex-1 border border-mineshaft-600 mr-4"> <div className="p-4 bg-mineshaft-900 rounded-lg flex-1 border border-mineshaft-600 mr-4">
<p className="mb-2 text-gray-400">Price</p> <p className="mb-2 text-gray-400">Price</p>
<p className="text-2xl mb-8 text-mineshaft-50 font-semibold"> <p className="text-2xl mb-8 text-mineshaft-50 font-semibold">
{subscription.status === "trialing" ? "$0.00 / month" : `${formatAmount(data.amount)} / ${data.interval}`} {subscription.status === "trialing"
? "$0.00 / month"
: `${formatAmount(data.amount)} / ${data.interval}`}
</p> </p>
</div> </div>
<div className="p-4 bg-mineshaft-900 rounded-lg flex-1 border border-mineshaft-600"> <div className="p-4 bg-mineshaft-900 rounded-lg flex-1 border border-mineshaft-600">
@@ -116,10 +142,7 @@ export const PreviewSection = () => {
</div> </div>
</div> </div>
)} )}
<ManagePlansModal <ManagePlansModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
popUp={popUp}
handlePopUpToggle={handlePopUpToggle}
/>
</div> </div>
); );
} };
@@ -4,20 +4,16 @@ import { yupResolver } from "@hookform/resolvers/yup";
import * as yup from "yup"; import * as yup from "yup";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import { import { OrgPermissionCan } from "@app/components/permissions";
Button, import { Button, FormControl, Input } from "@app/components/v2";
FormControl, import { OrgGeneralPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
Input import { useGetOrgBillingDetails, useUpdateOrgBillingDetails } from "@app/hooks/api";
} from "@app/components/v2";
import { useOrganization } from "@app/context";
import {
useGetOrgBillingDetails,
useUpdateOrgBillingDetails
} from "@app/hooks/api";
const schema = yup.object({ const schema = yup
.object({
name: yup.string().required("Company name is required") name: yup.string().required("Company name is required")
}).required(); })
.required();
export const CompanyNameSection = () => { export const CompanyNameSection = () => {
const { createNotification } = useNotificationContext(); const { createNotification } = useNotificationContext();
@@ -59,40 +55,38 @@ export const CompanyNameSection = () => {
type: "error" type: "error"
}); });
} }
} };
return ( return (
<form <form
onSubmit={handleSubmit(onFormSubmit)} onSubmit={handleSubmit(onFormSubmit)}
className="p-4 bg-mineshaft-900 mb-6 rounded-lg border border-mineshaft-600" className="p-4 bg-mineshaft-900 mb-6 rounded-lg border border-mineshaft-600"
> >
<h2 className="text-xl font-semibold flex-1 text-mineshaft-100 mb-8"> <h2 className="text-xl font-semibold flex-1 text-mineshaft-100 mb-8">Business name</h2>
Business name
</h2>
<div className="max-w-md"> <div className="max-w-md">
<Controller <Controller
defaultValue="" defaultValue=""
render={({ field, fieldState: { error } }) => ( render={({ field, fieldState: { error } }) => (
<FormControl isError={Boolean(error)} errorText={error?.message}> <FormControl isError={Boolean(error)} errorText={error?.message}>
<Input <Input placeholder="Acme Corp" {...field} className="bg-mineshaft-800" />
placeholder="Acme Corp"
{...field}
className="bg-mineshaft-800"
/>
</FormControl> </FormControl>
)} )}
control={control} control={control}
name="name" name="name"
/> />
</div> </div>
<OrgPermissionCan I={OrgGeneralPermissionActions.Edit} a={OrgPermissionSubjects.Billing}>
{(isAllowed) => (
<Button <Button
type="submit" type="submit"
colorSchema="secondary" colorSchema="secondary"
isLoading={isLoading} isLoading={isLoading}
isDisabled={isLoading} isDisabled={isLoading || !isAllowed}
> >
Save Save
</Button> </Button>
)}
</OrgPermissionCan>
</form> </form>
); );
} };
@@ -4,19 +4,16 @@ import { yupResolver } from "@hookform/resolvers/yup";
import * as yup from "yup"; import * as yup from "yup";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import { import { OrgPermissionCan } from "@app/components/permissions";
Button, import { Button, FormControl, Input } from "@app/components/v2";
FormControl, import { OrgGeneralPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
Input} from "@app/components/v2"; import { useGetOrgBillingDetails, useUpdateOrgBillingDetails } from "@app/hooks/api";
import { useOrganization } from "@app/context";
import {
useGetOrgBillingDetails,
useUpdateOrgBillingDetails
} from "@app/hooks/api";
const schema = yup.object({ const schema = yup
.object({
email: yup.string().required("Email is required") email: yup.string().required("Email is required")
}).required(); })
.required();
export const InvoiceEmailSection = () => { export const InvoiceEmailSection = () => {
const { createNotification } = useNotificationContext(); const { createNotification } = useNotificationContext();
@@ -59,39 +56,38 @@ export const InvoiceEmailSection = () => {
type: "error" type: "error"
}); });
} }
} };
return ( return (
<form <form
onSubmit={handleSubmit(onFormSubmit)} onSubmit={handleSubmit(onFormSubmit)}
className="p-4 bg-mineshaft-900 mb-6 rounded-lg border border-mineshaft-600" className="p-4 bg-mineshaft-900 mb-6 rounded-lg border border-mineshaft-600"
> >
<h2 className="text-xl font-semibold flex-1 text-white mb-8"> <h2 className="text-xl font-semibold flex-1 text-white mb-8">Invoice email recipient</h2>
Invoice email recipient
</h2>
<div className="max-w-md"> <div className="max-w-md">
<Controller <Controller
defaultValue="" defaultValue=""
render={({ field, fieldState: { error } }) => ( render={({ field, fieldState: { error } }) => (
<FormControl isError={Boolean(error)} errorText={error?.message}> <FormControl isError={Boolean(error)} errorText={error?.message}>
<Input <Input placeholder="[email protected]" {...field} className="bg-mineshaft-800" />
placeholder="[email protected]"
{...field}
className="bg-mineshaft-800"
/>
</FormControl> </FormControl>
)} )}
control={control} control={control}
name="email" name="email"
/> />
</div> </div>
<OrgPermissionCan I={OrgGeneralPermissionActions.Edit} a={OrgPermissionSubjects.Billing}>
{(isAllowed) => (
<Button <Button
type="submit" type="submit"
colorSchema="secondary" colorSchema="secondary"
isLoading={isLoading} isLoading={isLoading}
isDisabled={isLoading || !isAllowed}
> >
Save Save
</Button> </Button>
)}
</OrgPermissionCan>
</form> </form>
); );
} };
@@ -1,11 +1,9 @@
import { faPlus } from "@fortawesome/free-solid-svg-icons"; import { faPlus } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { import { OrgPermissionCan } from "@app/components/permissions";
Button import { Button } from "@app/components/v2";
} from "@app/components/v2"; import { OrgGeneralPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
import { useOrganization } from "@app/context";
import { useAddOrgPmtMethod } from "@app/hooks/api"; import { useAddOrgPmtMethod } from "@app/hooks/api";
import { PmtMethodsTable } from "./PmtMethodsTable"; import { PmtMethodsTable } from "./PmtMethodsTable";
@@ -23,24 +21,27 @@ export const PmtMethodsSection = () => {
}); });
window.location.href = url; window.location.href = url;
} };
return ( return (
<div className="p-4 bg-mineshaft-900 mb-6 rounded-lg border border-mineshaft-600"> <div className="p-4 bg-mineshaft-900 mb-6 rounded-lg border border-mineshaft-600">
<div className="flex items-center mb-8"> <div className="flex items-center mb-8">
<h2 className="text-xl font-semibold flex-1 text-white"> <h2 className="text-xl font-semibold flex-1 text-white">Payment methods</h2>
Payment methods <OrgPermissionCan I={OrgGeneralPermissionActions.Create} a={OrgPermissionSubjects.Billing}>
</h2> {(isAllowed) => (
<Button <Button
onClick={handleAddPmtMethodBtnClick} onClick={handleAddPmtMethodBtnClick}
colorSchema="secondary" colorSchema="secondary"
isLoading={isLoading} isLoading={isLoading}
leftIcon={<FontAwesomeIcon icon={faPlus} />} leftIcon={<FontAwesomeIcon icon={faPlus} />}
isDisabled={!isAllowed}
> >
Add method Add method
</Button> </Button>
)}
</OrgPermissionCan>
</div> </div>
<PmtMethodsTable /> <PmtMethodsTable />
</div> </div>
); );
} };
@@ -1,6 +1,7 @@
import { faCreditCard, faXmark } from "@fortawesome/free-solid-svg-icons"; import { faCreditCard, faXmark } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { OrgPermissionCan } from "@app/components/permissions";
import { import {
EmptyState, EmptyState,
IconButton, IconButton,
@@ -13,7 +14,7 @@ import {
THead, THead,
Tr Tr
} from "@app/components/v2"; } from "@app/components/v2";
import { useOrganization } from "@app/context"; import { OrgGeneralPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
import { useDeleteOrgPmtMethod, useGetOrgPmtMethods } from "@app/hooks/api"; import { useDeleteOrgPmtMethod, useGetOrgPmtMethods } from "@app/hooks/api";
export const PmtMethodsTable = () => { export const PmtMethodsTable = () => {
@@ -52,17 +53,25 @@ export const PmtMethodsTable = () => {
<Td>{last4}</Td> <Td>{last4}</Td>
<Td>{`${exp_month}/${exp_year}`}</Td> <Td>{`${exp_month}/${exp_year}`}</Td>
<Td> <Td>
<OrgPermissionCan
I={OrgGeneralPermissionActions.Delete}
a={OrgPermissionSubjects.Billing}
>
{(isAllowed) => (
<IconButton <IconButton
onClick={async () => { onClick={async () => {
await handleDeletePmtMethodBtnClick(_id); await handleDeletePmtMethodBtnClick(_id);
}} }}
size="lg" size="lg"
isDisabled={!isAllowed}
colorSchema="danger" colorSchema="danger"
variant="plain" variant="plain"
ariaLabel="update" ariaLabel="update"
> >
<FontAwesomeIcon icon={faXmark} /> <FontAwesomeIcon icon={faXmark} />
</IconButton> </IconButton>
)}
</OrgPermissionCan>
</Td> </Td>
</Tr> </Tr>
))} ))}
@@ -1,8 +1,9 @@
import { faPlus } from "@fortawesome/free-solid-svg-icons"; import { faPlus } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { OrgPermissionCan } from "@app/components/permissions";
import { Button } from "@app/components/v2"; import { Button } from "@app/components/v2";
import { OrgGeneralPermissionActions, OrgPermissionSubjects } from "@app/context";
import { usePopUp } from "@app/hooks/usePopUp"; import { usePopUp } from "@app/hooks/usePopUp";
import { TaxIDModal } from "./TaxIDModal"; import { TaxIDModal } from "./TaxIDModal";
@@ -16,16 +17,19 @@ export const TaxIDSection = () => {
return ( return (
<div className="p-4 bg-mineshaft-900 mb-6 rounded-lg border border-mineshaft-600"> <div className="p-4 bg-mineshaft-900 mb-6 rounded-lg border border-mineshaft-600">
<div className="flex items-center mb-8"> <div className="flex items-center mb-8">
<h2 className="text-xl font-semibold flex-1 text-white"> <h2 className="text-xl font-semibold flex-1 text-white">Tax ID</h2>
Tax ID <OrgPermissionCan I={OrgGeneralPermissionActions.Edit} a={OrgPermissionSubjects.Billing}>
</h2> {(isAllowed) => (
<Button <Button
onClick={() => handlePopUpOpen("addTaxID")} onClick={() => handlePopUpOpen("addTaxID")}
colorSchema="secondary" colorSchema="secondary"
isDisabled={!isAllowed}
leftIcon={<FontAwesomeIcon icon={faPlus} />} leftIcon={<FontAwesomeIcon icon={faPlus} />}
> >
Add method Add method
</Button> </Button>
)}
</OrgPermissionCan>
</div> </div>
<TaxIDTable /> <TaxIDTable />
<TaxIDModal <TaxIDModal
@@ -35,4 +39,4 @@ export const TaxIDSection = () => {
/> />
</div> </div>
); );
} };
@@ -1,6 +1,7 @@
import { faFileInvoice, faXmark } from "@fortawesome/free-solid-svg-icons"; import { faFileInvoice, faXmark } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { OrgPermissionCan } from "@app/components/permissions";
import { import {
EmptyState, EmptyState,
IconButton, IconButton,
@@ -13,7 +14,7 @@ import {
THead, THead,
Tr Tr
} from "@app/components/v2"; } from "@app/components/v2";
import { useOrganization } from "@app/context"; import { OrgGeneralPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
import { useDeleteOrgTaxId, useGetOrgTaxIds } from "@app/hooks/api"; import { useDeleteOrgTaxId, useGetOrgTaxIds } from "@app/hooks/api";
const taxIDTypeLabelMap: { [key: string]: string } = { const taxIDTypeLabelMap: { [key: string]: string } = {
@@ -101,6 +102,11 @@ export const TaxIDTable = () => {
<Td>{taxIDTypeLabelMap[type]}</Td> <Td>{taxIDTypeLabelMap[type]}</Td>
<Td>{value}</Td> <Td>{value}</Td>
<Td> <Td>
<OrgPermissionCan
I={OrgGeneralPermissionActions.Delete}
a={OrgPermissionSubjects.Billing}
>
{(isAllowed) => (
<IconButton <IconButton
onClick={async () => { onClick={async () => {
await handleDeleteTaxIdBtnClick(_id); await handleDeleteTaxIdBtnClick(_id);
@@ -109,9 +115,12 @@ export const TaxIDTable = () => {
colorSchema="danger" colorSchema="danger"
variant="plain" variant="plain"
ariaLabel="update" ariaLabel="update"
isDisabled={!isAllowed}
> >
<FontAwesomeIcon icon={faXmark} /> <FontAwesomeIcon icon={faXmark} />
</IconButton> </IconButton>
)}
</OrgPermissionCan>
</Td> </Td>
</Tr> </Tr>
))} ))}
@@ -1,5 +1,8 @@
import { Fragment } from "react" import { Fragment } from "react";
import { Tab } from "@headlessui/react" import { Tab } from "@headlessui/react";
import { OrgGeneralPermissionActions, OrgPermissionSubjects } from "@app/context";
import { withPermission } from "@app/hoc";
import { BillingCloudTab } from "../BillingCloudTab"; import { BillingCloudTab } from "../BillingCloudTab";
import { BillingDetailsTab } from "../BillingDetailsTab"; import { BillingDetailsTab } from "../BillingDetailsTab";
@@ -13,7 +16,8 @@ const tabs = [
{ name: "Billing details", key: "tab-billing-details" } { name: "Billing details", key: "tab-billing-details" }
]; ];
export const BillingTabGroup = () => { export const BillingTabGroup = withPermission(
() => {
return ( return (
<Tab.Group> <Tab.Group>
<Tab.List className="mt-8 mb-6 border-b-2 border-mineshaft-800"> <Tab.List className="mt-8 mb-6 border-b-2 border-mineshaft-800">
@@ -22,7 +26,9 @@ export const BillingTabGroup = () => {
{({ selected }) => ( {({ selected }) => (
<button <button
type="button" type="button"
className={`w-30 py-2 mx-2 mr-4 font-medium text-sm outline-none ${selected ? "border-b border-white text-white" : "text-mineshaft-400"}`} className={`w-30 py-2 mx-2 mr-4 font-medium text-sm outline-none ${
selected ? "border-b border-white text-white" : "text-mineshaft-400"
}`}
> >
{tab.name} {tab.name}
</button> </button>
@@ -46,4 +52,6 @@ export const BillingTabGroup = () => {
</Tab.Panels> </Tab.Panels>
</Tab.Group> </Tab.Group>
); );
} },
{ action: OrgGeneralPermissionActions.Read, subject: OrgPermissionSubjects.Billing }
);
@@ -1,9 +1,15 @@
import { OrgGeneralPermissionActions, OrgPermissionSubjects } from "@app/context";
import { withPermission } from "@app/hoc";
import { OrgSSOSection } from "./OrgSSOSection"; import { OrgSSOSection } from "./OrgSSOSection";
export const OrgAuthTab = () => { export const OrgAuthTab = withPermission(
() => {
return ( return (
<div> <div>
<OrgSSOSection /> <OrgSSOSection />
</div> </div>
); );
} },
{ action: OrgGeneralPermissionActions.Read, subject: OrgPermissionSubjects.Sso }
);
@@ -2,13 +2,15 @@ import { faPlus } 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";
import { OrgPermissionCan } from "@app/components/permissions";
import { Button, Switch, UpgradePlanModal } from "@app/components/v2"; import { Button, Switch, UpgradePlanModal } from "@app/components/v2";
import { useOrganization, useSubscription } from "@app/context";
import { import {
useCreateSSOConfig, OrgGeneralPermissionActions,
useGetSSOConfig, OrgPermissionSubjects,
useUpdateSSOConfig useOrganization,
} from "@app/hooks/api"; useSubscription
} from "@app/context";
import { useCreateSSOConfig, useGetSSOConfig, useUpdateSSOConfig } from "@app/hooks/api";
import { usePopUp } from "@app/hooks/usePopUp"; import { usePopUp } from "@app/hooks/usePopUp";
import { SSOModal } from "./SSOModal"; import { SSOModal } from "./SSOModal";
@@ -17,7 +19,7 @@ const ssoAuthProviderMap: { [key: string]: string } = {
"okta-saml": "Okta SAML", "okta-saml": "Okta SAML",
"azure-saml": "Azure SAML", "azure-saml": "Azure SAML",
"jumpcloud-saml": "JumpCloud SAML" "jumpcloud-saml": "JumpCloud SAML"
} };
export const OrgSSOSection = (): JSX.Element => { export const OrgSSOSection = (): JSX.Element => {
const { currentOrg } = useOrganization(); const { currentOrg } = useOrganization();
@@ -52,7 +54,7 @@ export const OrgSSOSection = (): JSX.Element => {
type: "error" type: "error"
}); });
} }
} };
const addSSOBtnClick = async () => { const addSSOBtnClick = async () => {
try { try {
@@ -77,50 +79,62 @@ export const OrgSSOSection = (): JSX.Element => {
} catch (err) { } catch (err) {
console.error(err); console.error(err);
} }
} };
return ( return (
<div className="p-4 bg-mineshaft-900 mb-6 rounded-lg border border-mineshaft-600"> <div className="p-4 bg-mineshaft-900 mb-6 rounded-lg border border-mineshaft-600">
<div className="flex items-center mb-8"> <div className="flex items-center mb-8">
<h2 className="text-xl font-semibold flex-1 text-white"> <h2 className="text-xl font-semibold flex-1 text-white">SAML SSO Configuration</h2>
SAML SSO Configuration
</h2>
{!isLoading && ( {!isLoading && (
<OrgPermissionCan I={OrgGeneralPermissionActions.Create} a={OrgPermissionSubjects.Sso}>
{(isAllowed) => (
<Button <Button
onClick={addSSOBtnClick} onClick={addSSOBtnClick}
colorSchema="secondary" colorSchema="secondary"
isDisabled={!isAllowed}
leftIcon={<FontAwesomeIcon icon={faPlus} />} leftIcon={<FontAwesomeIcon icon={faPlus} />}
> >
{data ? "Update SAML SSO" : "Set up SAML SSO"} {data ? "Update SAML SSO" : "Set up SAML SSO"}
</Button> </Button>
)} )}
</OrgPermissionCan>
)}
</div> </div>
{data && ( {data && (
<div className="mb-4"> <div className="mb-4">
<OrgPermissionCan I={OrgGeneralPermissionActions.Edit} a={OrgPermissionSubjects.Sso}>
{(isAllowed) => (
<Switch <Switch
id="enable-saml-sso" id="enable-saml-sso"
onCheckedChange={(value) => handleSamlSSOToggle(value)} onCheckedChange={(value) => handleSamlSSOToggle(value)}
isChecked={data ? data.isActive : false} isChecked={data ? data.isActive : false}
isDisabled={!isAllowed}
> >
Enable SAML SSO Enable SAML SSO
</Switch> </Switch>
)}
</OrgPermissionCan>
</div> </div>
)} )}
<div className="mb-4"> <div className="mb-4">
<h3 className="text-mineshaft-400 text-sm">SSO identifier</h3> <h3 className="text-mineshaft-400 text-sm">SSO identifier</h3>
<p className="text-gray-400 text-md">{(data && data._id !== "") ? data._id : "-"}</p> <p className="text-gray-400 text-md">{data && data._id !== "" ? data._id : "-"}</p>
</div> </div>
<div className="mb-4"> <div className="mb-4">
<h3 className="text-mineshaft-400 text-sm">Type</h3> <h3 className="text-mineshaft-400 text-sm">Type</h3>
<p className="text-gray-400 text-md">{(data && data.authProvider !== "") ? ssoAuthProviderMap[data.authProvider] : "-"}</p> <p className="text-gray-400 text-md">
{data && data.authProvider !== "" ? ssoAuthProviderMap[data.authProvider] : "-"}
</p>
</div> </div>
<div className="mb-4"> <div className="mb-4">
<h3 className="text-mineshaft-400 text-sm">Entrypoint</h3> <h3 className="text-mineshaft-400 text-sm">Entrypoint</h3>
<p className="text-gray-400 text-md">{(data && data.entryPoint !== "") ? data.entryPoint : "-"}</p> <p className="text-gray-400 text-md">
{data && data.entryPoint !== "" ? data.entryPoint : "-"}
</p>
</div> </div>
<div className="mb-4"> <div className="mb-4">
<h3 className="text-mineshaft-400 text-sm">Issuer</h3> <h3 className="text-mineshaft-400 text-sm">Issuer</h3>
<p className="text-gray-400 text-md">{(data && data.issuer !== "") ? data.issuer : "-"}</p> <p className="text-gray-400 text-md">{data && data.issuer !== "" ? data.issuer : "-"}</p>
</div> </div>
<SSOModal <SSOModal
popUp={popUp} popUp={popUp}
@@ -6,10 +6,8 @@ export const OrgGeneralTab = () => {
return ( return (
<div> <div>
<OrgNameChangeSection /> <OrgNameChangeSection />
<div className="p-4 bg-mineshaft-900 rounded-lg border border-mineshaft-600 mb-6">
<OrgServiceAccountsTable /> <OrgServiceAccountsTable />
</div>
<OrgIncidentContactsSection /> <OrgIncidentContactsSection />
</div> </div>
); );
} };
@@ -3,17 +3,9 @@ import { yupResolver } from "@hookform/resolvers/yup";
import * as yup from "yup"; import * as yup from "yup";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import { import { Button, FormControl, Input, Modal, ModalContent } from "@app/components/v2";
Button,
FormControl,
Input,
Modal,
ModalContent
} from "@app/components/v2";
import { useOrganization } from "@app/context"; import { useOrganization } from "@app/context";
import { import { useAddIncidentContact } from "@app/hooks/api";
useAddIncidentContact
} from "@app/hooks/api";
import { useFetchServerStatus } from "@app/hooks/api/serverDetails"; import { useFetchServerStatus } from "@app/hooks/api/serverDetails";
import { UsePopUpState } from "@app/hooks/usePopUp"; import { UsePopUpState } from "@app/hooks/usePopUp";
@@ -36,12 +28,10 @@ export const AddOrgIncidentContactModal = ({
}: Props) => { }: Props) => {
const { createNotification } = useNotificationContext(); const { createNotification } = useNotificationContext();
const { currentOrg } = useOrganization(); const { currentOrg } = useOrganization();
const { data: serverDetails } = useFetchServerStatus() const { data: serverDetails } = useFetchServerStatus();
const { const { control, handleSubmit, reset } = useForm<TAddContactForm>({
control, resolver: yupResolver(addContactFormSchema)
handleSubmit, });
reset
} = useForm<TAddContactForm>({ resolver: yupResolver(addContactFormSchema) });
const { mutateAsync, isLoading } = useAddIncidentContact(); const { mutateAsync, isLoading } = useAddIncidentContact();
@@ -59,7 +49,7 @@ export const AddOrgIncidentContactModal = ({
type: "success" type: "success"
}); });
if (serverDetails?.emailConfigured){ if (serverDetails?.emailConfigured) {
handlePopUpClose("addContact"); handlePopUpClose("addContact");
} }
@@ -71,7 +61,7 @@ export const AddOrgIncidentContactModal = ({
type: "error" type: "error"
}); });
} }
} };
return ( return (
<Modal <Modal
@@ -96,13 +86,8 @@ export const AddOrgIncidentContactModal = ({
</FormControl> </FormControl>
)} )}
/> />
<div className="mt-8 flex items-center"> <div className="mt-8 flex items-center space-x-4">
<Button <Button size="sm" type="submit" isLoading={isLoading} isDisabled={isLoading}>
size="sm"
type="submit"
isLoading={isLoading}
isDisabled={isLoading}
>
Add Incident Contact Add Incident Contact
</Button> </Button>
<Button <Button
@@ -117,4 +102,4 @@ export const AddOrgIncidentContactModal = ({
</ModalContent> </ModalContent>
</Modal> </Modal>
); );
} };
@@ -2,15 +2,17 @@ import { useTranslation } from "react-i18next";
import { faPlus } from "@fortawesome/free-solid-svg-icons"; import { faPlus } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { import { OrgPermissionCan } from "@app/components/permissions";
Button import { Button } from "@app/components/v2";
} from "@app/components/v2"; import { OrgGeneralPermissionActions, OrgPermissionSubjects } from "@app/context";
import { withPermission } from "@app/hoc";
import { usePopUp } from "@app/hooks"; import { usePopUp } from "@app/hooks";
import { AddOrgIncidentContactModal } from "./AddOrgIncidentContactModal"; import { AddOrgIncidentContactModal } from "./AddOrgIncidentContactModal";
import { OrgIncidentContactsTable } from "./OrgIncidentContactsTable"; import { OrgIncidentContactsTable } from "./OrgIncidentContactsTable";
export const OrgIncidentContactsSection = () => { export const OrgIncidentContactsSection = withPermission(
() => {
const { t } = useTranslation(); const { t } = useTranslation();
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([ const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
"addContact" "addContact"
@@ -22,14 +24,22 @@ export const OrgIncidentContactsSection = () => {
<p className="min-w-max text-xl font-semibold"> <p className="min-w-max text-xl font-semibold">
{t("section.incident.incident-contacts")} {t("section.incident.incident-contacts")}
</p> </p>
<OrgPermissionCan
I={OrgGeneralPermissionActions.Create}
a={OrgPermissionSubjects.IncidentAccount}
>
{(isAllowed) => (
<Button <Button
colorSchema="secondary" colorSchema="secondary"
type="submit" type="submit"
isDisabled={!isAllowed}
leftIcon={<FontAwesomeIcon icon={faPlus} />} leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => handlePopUpOpen("addContact")} onClick={() => handlePopUpOpen("addContact")}
> >
Add contact Add contact
</Button> </Button>
)}
</OrgPermissionCan>
</div> </div>
<OrgIncidentContactsTable /> <OrgIncidentContactsTable />
<AddOrgIncidentContactModal <AddOrgIncidentContactModal
@@ -39,5 +49,6 @@ export const OrgIncidentContactsSection = () => {
/> />
</div> </div>
); );
} },
{ action: OrgGeneralPermissionActions.Read, subject: OrgPermissionSubjects.IncidentAccount }
);
@@ -3,6 +3,7 @@ import { faContactBook, faMagnifyingGlass, faTrash } from "@fortawesome/free-sol
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";
import { OrgPermissionCan } from "@app/components/permissions";
import { import {
DeleteActionModal, DeleteActionModal,
EmptyState, EmptyState,
@@ -17,7 +18,7 @@ import {
THead, THead,
Tr Tr
} from "@app/components/v2"; } from "@app/components/v2";
import { useOrganization } from "@app/context"; import { OrgGeneralPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
import { usePopUp } from "@app/hooks"; import { usePopUp } from "@app/hooks";
import { useDeleteIncidentContact, useGetOrgIncidentContact } from "@app/hooks/api"; import { useDeleteIncidentContact, useGetOrgIncidentContact } from "@app/hooks/api";
@@ -83,13 +84,21 @@ export const OrgIncidentContactsTable = () => {
<Tr key={email}> <Tr key={email}>
<Td className="w-full">{email}</Td> <Td className="w-full">{email}</Td>
<Td className="mr-4"> <Td className="mr-4">
<OrgPermissionCan
I={OrgGeneralPermissionActions.Delete}
an={OrgPermissionSubjects.IncidentAccount}
>
{(isAllowed) => (
<IconButton <IconButton
ariaLabel="delete" ariaLabel="delete"
colorSchema="danger" colorSchema="danger"
onClick={() => handlePopUpOpen("removeContact", { email })} onClick={() => handlePopUpOpen("removeContact", { email })}
isDisabled={!isAllowed}
> >
<FontAwesomeIcon icon={faTrash} /> <FontAwesomeIcon icon={faTrash} />
</IconButton> </IconButton>
)}
</OrgPermissionCan>
</Td> </Td>
</Tr> </Tr>
))} ))}
@@ -4,8 +4,10 @@ import { yupResolver } from "@hookform/resolvers/yup";
import * as yup from "yup"; import * as yup from "yup";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import { OrgPermissionCan } from "@app/components/permissions";
import { Button, FormControl, Input } from "@app/components/v2"; import { Button, FormControl, Input } from "@app/components/v2";
import { useOrganization } from "@app/context"; import { OrgGeneralPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
import { withPermission } from "@app/hoc";
import { useRenameOrg } from "@app/hooks/api"; import { useRenameOrg } from "@app/hooks/api";
const formSchema = yup.object({ const formSchema = yup.object({
@@ -14,14 +16,13 @@ const formSchema = yup.object({
type FormData = yup.InferType<typeof formSchema>; type FormData = yup.InferType<typeof formSchema>;
export const OrgNameChangeSection = (): JSX.Element => { export const OrgNameChangeSection = withPermission(
(): JSX.Element => {
const { currentOrg } = useOrganization(); const { currentOrg } = useOrganization();
const { createNotification } = useNotificationContext(); const { createNotification } = useNotificationContext();
const { const { handleSubmit, control, reset } = useForm<FormData>({
handleSubmit, resolver: yupResolver(formSchema)
control, });
reset
} = useForm<FormData>({ resolver: yupResolver(formSchema) });
const { mutateAsync, isLoading } = useRenameOrg(); const { mutateAsync, isLoading } = useRenameOrg();
useEffect(() => { useEffect(() => {
@@ -54,9 +55,7 @@ export const OrgNameChangeSection = (): JSX.Element => {
onSubmit={handleSubmit(onFormSubmit)} onSubmit={handleSubmit(onFormSubmit)}
className="p-4 bg-mineshaft-900 mb-6 rounded-lg border border-mineshaft-600" className="p-4 bg-mineshaft-900 mb-6 rounded-lg border border-mineshaft-600"
> >
<p className="text-xl font-semibold text-mineshaft-100 mb-4"> <p className="text-xl font-semibold text-mineshaft-100 mb-4">Organization name</p>
Organization name
</p>
<div className="mb-2 max-w-md"> <div className="mb-2 max-w-md">
<Controller <Controller
defaultValue="" defaultValue=""
@@ -69,14 +68,25 @@ export const OrgNameChangeSection = (): JSX.Element => {
name="name" name="name"
/> />
</div> </div>
<OrgPermissionCan I={OrgGeneralPermissionActions.Edit} a={OrgPermissionSubjects.Settings}>
{(isAllowed) => (
<Button <Button
isLoading={isLoading} isLoading={isLoading}
isDisabled={!isAllowed}
colorSchema="primary" colorSchema="primary"
variant="outline_bg" variant="outline_bg"
type="submit" type="submit"
> >
Save Save
</Button> </Button>
)}
</OrgPermissionCan>
</form> </form>
); );
}; },
{
action: OrgGeneralPermissionActions.Read,
subject: OrgPermissionSubjects.Settings,
containerClassName: "mb-4"
}
);
@@ -34,7 +34,13 @@ import {
THead, THead,
Tr Tr
} from "@app/components/v2"; } from "@app/components/v2";
import { useOrganization, useWorkspace } from "@app/context"; import {
OrgGeneralPermissionActions,
OrgPermissionSubjects,
useOrganization,
useWorkspace
} from "@app/context";
import { withPermission } from "@app/hoc";
import { usePopUp, useToggle } from "@app/hooks"; import { usePopUp, useToggle } from "@app/hooks";
import { import {
// useCreateServiceAccount, // useCreateServiceAccount,
@@ -62,7 +68,8 @@ import // Controller,
// type TAddServiceAccountForm = yup.InferType<typeof addServiceAccountFormSchema>; // type TAddServiceAccountForm = yup.InferType<typeof addServiceAccountFormSchema>;
export const OrgServiceAccountsTable = () => { export const OrgServiceAccountsTable = withPermission(
() => {
const router = useRouter(); const router = useRouter();
const { currentOrg } = useOrganization(); const { currentOrg } = useOrganization();
const { currentWorkspace } = useWorkspace(); const { currentWorkspace } = useWorkspace();
@@ -139,7 +146,9 @@ export const OrgServiceAccountsTable = () => {
const filteredServiceAccounts = useMemo( const filteredServiceAccounts = useMemo(
() => () =>
serviceAccounts.filter(({ name }) => name.toLowerCase().includes(searchServiceAccountFilter)), serviceAccounts.filter(({ name }) =>
name.toLowerCase().includes(searchServiceAccountFilter)
),
[serviceAccounts, searchServiceAccountFilter] [serviceAccounts, searchServiceAccountFilter]
); );
@@ -274,7 +283,7 @@ export const OrgServiceAccountsTable = () => {
}; };
return ( return (
<div className="w-full"> <div className="p-4 bg-mineshaft-900 rounded-lg border border-mineshaft-600 mb-6">
<div className="mb-4 flex justify-between"> <div className="mb-4 flex justify-between">
<p className="text-xl font-semibold text-mineshaft-100">Service Accounts</p> <p className="text-xl font-semibold text-mineshaft-100">Service Accounts</p>
<Button <Button
@@ -371,4 +380,10 @@ export const OrgServiceAccountsTable = () => {
/> />
</div> </div>
); );
}; },
{
action: OrgGeneralPermissionActions.Read,
subject: OrgPermissionSubjects.Settings,
containerClassName: "mb-4"
}
);
@@ -1,33 +1,14 @@
import { Fragment } from "react" import { Fragment } from "react";
import { Tab } from "@headlessui/react" import { Tab } from "@headlessui/react";
import { useOrganization,useUser } from "@app/context";
import {
useGetOrgUsers
} from "@app/hooks/api";
import { OrgAuthTab } from "../OrgAuthTab"; import { OrgAuthTab } from "../OrgAuthTab";
import { OrgGeneralTab } from "../OrgGeneralTab"; import { OrgGeneralTab } from "../OrgGeneralTab";
export const OrgTabGroup = () => { const tabs = [
const { currentOrg } = useOrganization();
const { user } = useUser();
const { data } = useGetOrgUsers(currentOrg?._id ?? "");
const isRoleSufficient = data?.some((orgUser) => {
return orgUser.role !== "member" && orgUser.user._id === user._id;
});
const tabs = [
{ name: "General", key: "tab-org-general" }, { name: "General", key: "tab-org-general" },
];
if (isRoleSufficient) {
tabs.push(
{ name: "Authentication", key: "tab-org-auth" } { name: "Authentication", key: "tab-org-auth" }
); ];
} export const OrgTabGroup = () => {
return ( return (
<Tab.Group> <Tab.Group>
<Tab.List className="mb-6 border-b-2 border-mineshaft-800 w-full"> <Tab.List className="mb-6 border-b-2 border-mineshaft-800 w-full">
@@ -36,7 +17,9 @@ export const OrgTabGroup = () => {
{({ selected }) => ( {({ selected }) => (
<button <button
type="button" type="button"
className={`w-30 py-2 mx-2 mr-4 font-medium text-sm outline-none ${selected ? "border-b border-white text-white" : "text-mineshaft-400"}`} className={`w-30 py-2 mx-2 mr-4 font-medium text-sm outline-none ${
selected ? "border-b border-white text-white" : "text-mineshaft-400"
}`}
> >
{tab.name} {tab.name}
</button> </button>
@@ -48,12 +31,10 @@ export const OrgTabGroup = () => {
<Tab.Panel> <Tab.Panel>
<OrgGeneralTab /> <OrgGeneralTab />
</Tab.Panel> </Tab.Panel>
{isRoleSufficient && (
<Tab.Panel> <Tab.Panel>
<OrgAuthTab /> <OrgAuthTab />
</Tab.Panel> </Tab.Panel>
)}
</Tab.Panels> </Tab.Panels>
</Tab.Group> </Tab.Group>
); );
} };