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
@@ -3,7 +3,8 @@ import { useTranslation } from "react-i18next";
import { motion } from "framer-motion";
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 { OrgMembersTable } from "./components/OrgMembersTable";
@@ -14,45 +15,48 @@ enum TabSections {
Roles = "roles"
}
export const MembersPage = () => {
const { t } = useTranslation();
const { currentOrg } = useOrganization();
export const MembersPage = withPermission(
() => {
const { t } = useTranslation();
const { currentOrg } = useOrganization();
const orgId = currentOrg?._id || "";
const orgId = currentOrg?._id || "";
const { data: roles } = useGetRoles({
orgId
});
const { data: roles } = useGetRoles({
orgId
});
return (
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
<div className="mb-6 w-full py-6 px-6 max-w-7xl mx-auto">
<p className="mr-4 mb-4 text-3xl font-semibold text-white">
{t("section.members.org-members")}
</p>
<Tabs defaultValue={TabSections.Member}>
<TabList>
<Tab value={TabSections.Member}>Members</Tab>
{process.env.NEXT_PUBLIC_NEW_PERMISSION_FLAG === "true" && (
<Tab value={TabSections.Roles}>Roles</Tab>
)}
</TabList>
<TabPanel value={TabSections.Member}>
<motion.div
key="panel-1"
transition={{ duration: 0.15 }}
initial={{ opacity: 0, translateX: 30 }}
animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: 30 }}
>
<OrgMembersTable roles={roles} />
</motion.div>
</TabPanel>
<TabPanel value={TabSections.Roles}>
<OrgRoleTabSection roles={roles} />
</TabPanel>
</Tabs>
return (
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
<div className="mb-6 w-full py-6 px-6 max-w-7xl mx-auto">
<p className="mr-4 mb-4 text-3xl font-semibold text-white">
{t("section.members.org-members")}
</p>
<Tabs defaultValue={TabSections.Member}>
<TabList>
<Tab value={TabSections.Member}>Members</Tab>
{process.env.NEXT_PUBLIC_NEW_PERMISSION_FLAG === "true" && (
<Tab value={TabSections.Roles}>Roles</Tab>
)}
</TabList>
<TabPanel value={TabSections.Member}>
<motion.div
key="panel-1"
transition={{ duration: 0.15 }}
initial={{ opacity: 0, translateX: 30 }}
animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: 30 }}
>
<OrgMembersTable roles={roles} />
</motion.div>
</TabPanel>
<TabPanel value={TabSections.Roles}>
<OrgRoleTabSection roles={roles} />
</TabPanel>
</Tabs>
</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 { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import { OrgPermissionCan } from "@app/components/permissions";
import {
decryptAssymmetric,
encryptAssymmetric
@@ -41,7 +42,14 @@ import {
Tr,
UpgradePlanModal
} 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 {
useAddUserToOrg,
@@ -297,27 +305,32 @@ export const OrgMembersTable = ({ roles = [] }: Props) => {
placeholder="Search members..."
/>
</div>
<Button
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => {
if (!isLoadingSSOConfig && ssoConfig && ssoConfig.isActive) {
createNotification({
text: "You cannot invite users when SAML SSO is configured for your organization",
type: "error"
});
<OrgPermissionCan I={OrgGeneralPermissionActions.Create} a={OrgPermissionSubjects.Member}>
{(isAllowed) => (
<Button
isDisabled={!isAllowed}
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => {
if (!isLoadingSSOConfig && ssoConfig && ssoConfig.isActive) {
createNotification({
text: "You cannot invite users when SAML SSO is configured for your organization",
type: "error"
});
return;
}
return;
}
if (isMoreUsersNotAllowed) {
handlePopUpOpen("upgradePlan");
} else {
handlePopUpOpen("addMember");
}
}}
>
Add Member
</Button>
if (isMoreUsersNotAllowed) {
handlePopUpOpen("upgradePlan");
} else {
handlePopUpOpen("addMember");
}
}}
>
Add Member
</Button>
)}
</OrgPermissionCan>
</div>
<div>
<TableContainer>
@@ -345,48 +358,59 @@ export const OrgMembersTable = ({ roles = [] }: Props) => {
<Td>{name}</Td>
<Td>{email}</Td>
<Td>
{status === "accepted" && (
<Select
defaultValue={
role === "custom" ? findRoleFromId(customRole)?.slug : role
}
isDisabled={userId === u?._id}
className="w-40 bg-mineshaft-600"
dropdownContainerClassName="border border-mineshaft-600 bg-mineshaft-800"
onValueChange={(selectedRole) =>
onRoleChange(orgMembershipId, selectedRole)
}
>
{roles
.filter(({ slug }) =>
slug === "owner" ? isIamOwner || role === "owner" : true
)
.map(({ slug, name: roleName }) => (
<SelectItem value={slug} key={`owner-option-${slug}`}>
{roleName}
</SelectItem>
))}
</Select>
)}
{(status === "invited" || status === "verified") &&
serverDetails?.emailConfigured && (
<Button
className="w-40"
colorSchema="primary"
variant="outline_bg"
onClick={() => onAddUserToOrg(email)}
>
Resend Invite
</Button>
<OrgPermissionCan
I={OrgGeneralPermissionActions.Edit}
a={OrgPermissionSubjects.Member}
>
{(isAllowed) => (
<>
{status === "accepted" && (
<Select
defaultValue={
role === "custom" ? findRoleFromId(customRole)?.slug : role
}
isDisabled={userId === u?._id || !isAllowed}
className="w-40 bg-mineshaft-600"
dropdownContainerClassName="border border-mineshaft-600 bg-mineshaft-800"
onValueChange={(selectedRole) =>
onRoleChange(orgMembershipId, selectedRole)
}
>
{roles
.filter(({ slug }) =>
slug === "owner" ? isIamOwner || role === "owner" : true
)
.map(({ slug, name: roleName }) => (
<SelectItem value={slug} key={`owner-option-${slug}`}>
{roleName}
</SelectItem>
))}
</Select>
)}
{(status === "invited" || status === "verified") &&
serverDetails?.emailConfigured && (
<Button
isDisabled={!isAllowed}
className="w-40"
colorSchema="primary"
variant="outline_bg"
onClick={() => onAddUserToOrg(email)}
>
Resend Invite
</Button>
)}
{status === "completed" && (
<Button
colorSchema="secondary"
isDisabled={!isAllowed}
onClick={() => onGrantAccess(u?._id, u?.publicKey)}
>
Grant Access
</Button>
)}
</>
)}
{status === "completed" && (
<Button
colorSchema="secondary"
onClick={() => onGrantAccess(u?._id, u?.publicKey)}
>
Grant Access
</Button>
)}
</OrgPermissionCan>
</Td>
<Td>
{userWs ? (
@@ -428,16 +452,23 @@ export const OrgMembersTable = ({ roles = [] }: Props) => {
</Td>
<Td>
{userId !== u?._id && (
<IconButton
ariaLabel="delete"
colorSchema="danger"
isDisabled={userId === u?._id}
onClick={() =>
handlePopUpOpen("removeMember", { id: orgMembershipId })
}
<OrgPermissionCan
I={OrgGeneralPermissionActions.Delete}
a={OrgPermissionSubjects.Member}
>
<FontAwesomeIcon icon={faTrash} />
</IconButton>
{(isAllowed) => (
<IconButton
ariaLabel="delete"
colorSchema="danger"
isDisabled={userId === u?._id || !isAllowed}
onClick={() =>
handlePopUpOpen("removeMember", { id: orgMembershipId })
}
>
<FontAwesomeIcon icon={faTrash} />
</IconButton>
)}
</OrgPermissionCan>
)}
</Td>
</Tr>
@@ -38,10 +38,9 @@ export const BillingPermission = ({ isNonEditable, setValue, control }: Props) =
const [isCustom, setIsCustom] = useToggle();
const selectedPermissionCategory = useMemo(() => {
let score = 0;
const actions = Object.keys(rule || {}) as Array<keyof typeof rule>;
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 (score === 0) return Permission.NoAccess;
@@ -52,11 +51,14 @@ export const BillingPermission = ({ isNonEditable, setValue, control }: Props) =
}, [rule, isCustom]);
useEffect(() => {
selectedPermissionCategory === Permission.Custom ? setIsCustom.on() : setIsCustom.off();
if (selectedPermissionCategory === Permission.Custom) setIsCustom.on();
else setIsCustom.off();
}, [selectedPermissionCategory]);
const handlePermissionChange = (val: Permission) => {
val === Permission.Custom ? setIsCustom.on() : setIsCustom.off();
if (val === Permission.Custom) setIsCustom.on();
else setIsCustom.off();
switch (val) {
case Permission.NoAccess:
setValue(
@@ -38,10 +38,9 @@ export const IncidentContactPermission = ({ isNonEditable, setValue, control }:
const [isCustom, setIsCustom] = useToggle();
const selectedPermissionCategory = useMemo(() => {
let score = 0;
const actions = Object.keys(rule || {}) as Array<keyof typeof rule>;
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 (score === 0) return Permission.NoAccess;
@@ -52,14 +51,16 @@ export const IncidentContactPermission = ({ isNonEditable, setValue, control }:
}, [rule, isCustom]);
useEffect(() => {
selectedPermissionCategory === Permission.Custom ? setIsCustom.on() : setIsCustom.off();
if (selectedPermissionCategory === Permission.Custom) setIsCustom.on();
else setIsCustom.off();
}, [selectedPermissionCategory]);
const handlePermissionChange = (val: Permission) => {
val === Permission.Custom ? setIsCustom.on() : setIsCustom.off();
if (val === Permission.Custom) setIsCustom.on();
else setIsCustom.off();
switch (val) {
case Permission.NoAccess:
setIsCustom.off();
setValue(
"permissions.incident-contact",
{ read: false, edit: false, create: false, delete: false },
@@ -67,7 +68,6 @@ export const IncidentContactPermission = ({ isNonEditable, setValue, control }:
);
break;
case Permission.FullAccess:
setIsCustom.off();
setValue(
"permissions.incident-contact",
{ read: true, edit: true, create: true, delete: true },
@@ -75,7 +75,6 @@ export const IncidentContactPermission = ({ isNonEditable, setValue, control }:
);
break;
case Permission.ReadOnly:
setIsCustom.off();
setValue(
"permissions.incident-contact",
{ read: true, edit: false, create: false, delete: false },
@@ -83,7 +82,6 @@ export const IncidentContactPermission = ({ isNonEditable, setValue, control }:
);
break;
default:
setIsCustom.on();
setValue(
"permissions.incident-contact",
{ read: false, edit: false, create: false, delete: false },
@@ -31,31 +31,34 @@ const PERMISSIONS = [
] as const;
export const MemberPermission = ({ isNonEditable, setValue, control }: Props) => {
const memberRule = useWatch({
const rule = useWatch({
control,
name: "permissions.member"
});
const [isCustom, setIsCustom] = useToggle();
const selectedPermissionCategory = useMemo(() => {
let score = 0;
const actions = Object.keys(memberRule || {}) as Array<keyof typeof memberRule>;
const actions = Object.keys(rule || {}) as Array<keyof typeof rule>;
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 (score === 0) return Permission.NoAccess;
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;
}, [memberRule, isCustom]);
}, [rule, isCustom]);
useEffect(() => {
selectedPermissionCategory === Permission.Custom ? setIsCustom.on() : setIsCustom.off();
if (selectedPermissionCategory === Permission.Custom) setIsCustom.on();
else setIsCustom.off();
}, [selectedPermissionCategory]);
const handlePermissionChange = (val: Permission) => {
if (val === Permission.Custom) setIsCustom.on();
else setIsCustom.off();
switch (val) {
case Permission.NoAccess:
setValue(
@@ -31,34 +31,36 @@ const PERMISSIONS = [
] as const;
export const RolePermission = ({ isNonEditable, setValue, control }: Props) => {
const roleRule = useWatch({
const rule = useWatch({
control,
name: "permissions.role"
});
const [isCustom, setIsCustom] = useToggle();
const selectedPermissionCategory = useMemo(() => {
let score = 0;
const actions = Object.keys(roleRule || {}) as Array<keyof typeof roleRule>;
const actions = Object.keys(rule || {}) as Array<keyof typeof rule>;
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 (score === 0) return Permission.NoAccess;
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;
}, [roleRule, isCustom]);
}, [rule, isCustom]);
useEffect(() => {
selectedPermissionCategory === Permission.Custom ? setIsCustom.on() : setIsCustom.off();
if (selectedPermissionCategory === Permission.Custom) setIsCustom.on();
else setIsCustom.off();
}, [selectedPermissionCategory]);
const handlePermissionChange = (val: Permission) => {
if (val === Permission.Custom) setIsCustom.on();
else setIsCustom.off();
switch (val) {
case Permission.NoAccess:
setIsCustom.off();
setValue(
"permissions.role",
{ read: false, edit: false, create: false, delete: false },
@@ -66,7 +68,6 @@ export const RolePermission = ({ isNonEditable, setValue, control }: Props) => {
);
break;
case Permission.FullAccess:
setIsCustom.off();
setValue(
"permissions.role",
{ read: true, edit: true, create: true, delete: true },
@@ -74,7 +75,6 @@ export const RolePermission = ({ isNonEditable, setValue, control }: Props) => {
);
break;
case Permission.ReadOnly:
setIsCustom.off();
setValue(
"permissions.role",
{ read: true, edit: false, create: false, delete: false },
@@ -82,7 +82,6 @@ export const RolePermission = ({ isNonEditable, setValue, control }: Props) => {
);
break;
default:
setIsCustom.on();
setValue(
"permissions.role",
{ read: false, edit: false, create: false, delete: false },
@@ -38,10 +38,9 @@ export const SecretScannigPermission = ({ isNonEditable, setValue, control }: Pr
const [isCustom, setIsCustom] = useToggle();
const selectedPermissionCategory = useMemo(() => {
let score = 0;
const actions = Object.keys(rule || {}) as Array<keyof typeof rule>;
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 (score === 0) return Permission.NoAccess;
@@ -52,13 +51,16 @@ export const SecretScannigPermission = ({ isNonEditable, setValue, control }: Pr
}, [rule, isCustom]);
useEffect(() => {
selectedPermissionCategory === Permission.Custom ? setIsCustom.on() : setIsCustom.off();
if (selectedPermissionCategory === Permission.Custom) setIsCustom.on();
else setIsCustom.off();
}, [selectedPermissionCategory]);
const handlePermissionChange = (val: Permission) => {
if (val === Permission.Custom) setIsCustom.on();
else setIsCustom.off();
switch (val) {
case Permission.NoAccess:
setIsCustom.off();
setValue(
"permissions.secret-scanning",
{ read: false, edit: false, create: false, delete: false },
@@ -66,7 +68,6 @@ export const SecretScannigPermission = ({ isNonEditable, setValue, control }: Pr
);
break;
case Permission.FullAccess:
setIsCustom.off();
setValue(
"permissions.secret-scanning",
{ read: true, edit: true, create: true, delete: true },
@@ -74,7 +75,6 @@ export const SecretScannigPermission = ({ isNonEditable, setValue, control }: Pr
);
break;
case Permission.ReadOnly:
setIsCustom.off();
setValue(
"permissions.secret-scanning",
{ read: true, edit: false, create: false, delete: false },
@@ -82,7 +82,6 @@ export const SecretScannigPermission = ({ isNonEditable, setValue, control }: Pr
);
break;
default:
setIsCustom.on();
setValue(
"permissions.secret-scanning",
{ read: false, edit: false, create: false, delete: false },
@@ -38,10 +38,9 @@ export const SettingsPermission = ({ isNonEditable, setValue, control }: Props)
const [isCustom, setIsCustom] = useToggle();
const selectedPermissionCategory = useMemo(() => {
let score = 0;
const actions = Object.keys(rule || {}) as Array<keyof typeof rule>;
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 (score === 0) return Permission.NoAccess;
@@ -52,13 +51,16 @@ export const SettingsPermission = ({ isNonEditable, setValue, control }: Props)
}, [rule, isCustom]);
useEffect(() => {
selectedPermissionCategory === Permission.Custom ? setIsCustom.on() : setIsCustom.off();
if (selectedPermissionCategory === Permission.Custom) setIsCustom.on();
else setIsCustom.off();
}, [selectedPermissionCategory]);
const handlePermissionChange = (val: Permission) => {
if (val === Permission.Custom) setIsCustom.on();
else setIsCustom.off();
switch (val) {
case Permission.NoAccess:
setIsCustom.off();
setValue(
"permissions.settings",
{ read: false, edit: false, create: false, delete: false },
@@ -66,7 +68,6 @@ export const SettingsPermission = ({ isNonEditable, setValue, control }: Props)
);
break;
case Permission.FullAccess:
setIsCustom.off();
setValue(
"permissions.settings",
{ read: true, edit: true, create: true, delete: true },
@@ -74,7 +75,6 @@ export const SettingsPermission = ({ isNonEditable, setValue, control }: Props)
);
break;
case Permission.ReadOnly:
setIsCustom.off();
setValue(
"permissions.settings",
{ read: true, edit: false, create: false, delete: false },
@@ -82,7 +82,6 @@ export const SettingsPermission = ({ isNonEditable, setValue, control }: Props)
);
break;
default:
setIsCustom.on();
setValue(
"permissions.settings",
{ read: false, edit: false, create: false, delete: false },
@@ -38,10 +38,9 @@ export const SsoPermission = ({ isNonEditable, setValue, control }: Props) => {
const [isCustom, setIsCustom] = useToggle();
const selectedPermissionCategory = useMemo(() => {
let score = 0;
const actions = Object.keys(rule || {}) as Array<keyof typeof rule>;
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 (score === 0) return Permission.NoAccess;
@@ -52,13 +51,16 @@ export const SsoPermission = ({ isNonEditable, setValue, control }: Props) => {
}, [rule, isCustom]);
useEffect(() => {
selectedPermissionCategory === Permission.Custom ? setIsCustom.on() : setIsCustom.off();
if (selectedPermissionCategory === Permission.Custom) setIsCustom.on();
else setIsCustom.off();
}, [selectedPermissionCategory]);
const handlePermissionChange = (val: Permission) => {
if (val === Permission.Custom) setIsCustom.on();
else setIsCustom.off();
switch (val) {
case Permission.NoAccess:
setIsCustom.off();
setValue(
"permissions.sso",
{ read: false, edit: false, create: false, delete: false },
@@ -66,7 +68,6 @@ export const SsoPermission = ({ isNonEditable, setValue, control }: Props) => {
);
break;
case Permission.FullAccess:
setIsCustom.off();
setValue(
"permissions.sso",
{ read: true, edit: true, create: true, delete: true },
@@ -74,7 +75,6 @@ export const SsoPermission = ({ isNonEditable, setValue, control }: Props) => {
);
break;
case Permission.ReadOnly:
setIsCustom.off();
setValue(
"permissions.sso",
{ read: true, edit: false, create: false, delete: false },
@@ -82,7 +82,6 @@ export const SsoPermission = ({ isNonEditable, setValue, control }: Props) => {
);
break;
default:
setIsCustom.on();
setValue(
"permissions.sso",
{ read: false, edit: false, create: false, delete: false },
@@ -36,10 +36,9 @@ export const WorkspacePermission = ({ isNonEditable, setValue, control }: Props)
const [isCustom, setIsCustom] = useToggle();
const selectedPermissionCategory = useMemo(() => {
let score = 0;
const actions = Object.keys(rule || {}) as Array<keyof typeof rule>;
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 (score === 0) return Permission.NoAccess;
@@ -50,11 +49,14 @@ export const WorkspacePermission = ({ isNonEditable, setValue, control }: Props)
}, [rule, isCustom]);
useEffect(() => {
selectedPermissionCategory === Permission.Custom ? setIsCustom.on() : setIsCustom.off();
if (selectedPermissionCategory === Permission.Custom) setIsCustom.on();
else setIsCustom.off();
}, [selectedPermissionCategory]);
const handlePermissionChange = (val: Permission) => {
val === Permission.Custom ? setIsCustom.on() : setIsCustom.off();
if (val === Permission.Custom) setIsCustom.on();
else setIsCustom.off();
switch (val) {
case Permission.NoAccess:
setValue("permissions.workspace", { read: false, create: false }, { shouldDirty: true });