Add default org and project-level no access roles

This commit is contained in:
Tuan Dang
2023-11-26 17:02:03 +07:00
parent 4e050cfe7a
commit 63df0dba64
21 changed files with 84 additions and 38 deletions
@@ -15,7 +15,7 @@ import {
} from "../../helpers/organization"; } from "../../helpers/organization";
import { addMembershipsOrg } from "../../helpers/membershipOrg"; import { addMembershipsOrg } from "../../helpers/membershipOrg";
import { BadRequestError, UnauthorizedRequestError } from "../../utils/errors"; import { BadRequestError, UnauthorizedRequestError } from "../../utils/errors";
import { ACCEPTED, ADMIN, CUSTOM, MEMBER } from "../../variables"; import { ACCEPTED, ADMIN, CUSTOM, MEMBER, NO_ACCESS } from "../../variables";
import * as reqValidator from "../../validation/organization"; import * as reqValidator from "../../validation/organization";
import { validateRequest } from "../../helpers/validation"; import { validateRequest } from "../../helpers/validation";
import { import {
@@ -153,7 +153,7 @@ export const updateOrganizationMembership = async (req: Request, res: Response)
OrgPermissionSubjects.Member OrgPermissionSubjects.Member
); );
const isCustomRole = ![ADMIN, MEMBER].includes(role); const isCustomRole = ![ADMIN, MEMBER, NO_ACCESS].includes(role);
if (isCustomRole) { if (isCustomRole) {
const orgRole = await Role.findOne({ const orgRole = await Role.findOne({
slug: role, slug: role,
@@ -30,7 +30,7 @@ import {
} from "../../ee/services/ProjectRoleService"; } from "../../ee/services/ProjectRoleService";
import { ForbiddenError } from "@casl/ability"; import { ForbiddenError } from "@casl/ability";
import { BadRequestError, ForbiddenRequestError, ResourceNotFoundError } from "../../utils/errors"; import { BadRequestError, ForbiddenRequestError, ResourceNotFoundError } from "../../utils/errors";
import { ADMIN, CUSTOM, MEMBER, VIEWER } from "../../variables"; import { ADMIN, CUSTOM, MEMBER, NO_ACCESS, VIEWER } from "../../variables";
interface V2PushSecret { interface V2PushSecret {
type: string; // personal or shared type: string; // personal or shared
@@ -556,7 +556,7 @@ export const addMachineToWorkspace = async (req: Request, res: Response) => {
let customRole; let customRole;
if (role) { if (role) {
const isCustomRole = ![ADMIN, MEMBER, VIEWER].includes(role); const isCustomRole = ![ADMIN, MEMBER, VIEWER, NO_ACCESS].includes(role);
if (isCustomRole) { if (isCustomRole) {
customRole = await Role.findOne({ customRole = await Role.findOne({
slug: role, slug: role,
@@ -632,7 +632,7 @@ export const addMachineToWorkspace = async (req: Request, res: Response) => {
let customRole; let customRole;
if (role) { if (role) {
const isCustomRole = ![ADMIN, MEMBER, VIEWER].includes(role); const isCustomRole = ![ADMIN, MEMBER, VIEWER, NO_ACCESS].includes(role);
if (isCustomRole) { if (isCustomRole) {
customRole = await Role.findOne({ customRole = await Role.findOne({
slug: role, slug: role,
@@ -15,6 +15,7 @@ import {
adminProjectPermissions, adminProjectPermissions,
getAuthDataProjectPermissions, getAuthDataProjectPermissions,
memberProjectPermissions, memberProjectPermissions,
noAccessProjectPermissions,
viewerProjectPermission viewerProjectPermission
} from "../../services/ProjectRoleService"; } from "../../services/ProjectRoleService";
import { import {
@@ -22,7 +23,8 @@ import {
OrgPermissionSubjects, OrgPermissionSubjects,
adminPermissions, adminPermissions,
getUserOrgPermissions, getUserOrgPermissions,
memberPermissions memberPermissions,
noAccessPermissions
} from "../../services/RoleService"; } from "../../services/RoleService";
import { BadRequestError } from "../../../utils/errors"; import { BadRequestError } from "../../../utils/errors";
import { Role } from "../../models"; import { Role } from "../../models";
@@ -195,6 +197,13 @@ export const getRoles = async (req: Request, res: Response) => {
description: "Complete administration access over the organization", description: "Complete administration access over the organization",
permissions: isOrgRole ? adminPermissions.rules : adminProjectPermissions.rules permissions: isOrgRole ? adminPermissions.rules : adminProjectPermissions.rules
}, },
{
_id: "no-access",
name: "No Access",
slug: "no-access",
description: "No access to any resources in the organization",
permissions: isOrgRole ? noAccessPermissions.rules : noAccessProjectPermissions.rules
},
{ {
_id: "member", _id: "member",
name: isOrgRole ? "Member" : "Developer", name: isOrgRole ? "Member" : "Developer",
@@ -25,7 +25,7 @@ import { BadRequestError, ForbiddenRequestError, ResourceNotFoundError, Unauthor
import { extractIPDetails, isValidIpOrCidr } from "../../../utils/ip"; import { extractIPDetails, isValidIpOrCidr } from "../../../utils/ip";
import { EEAuditLogService, EELicenseService } from "../../services"; import { EEAuditLogService, EELicenseService } from "../../services";
import { getAuthSecret } from "../../../config"; import { getAuthSecret } from "../../../config";
import { ADMIN, AuthTokenType, CUSTOM, MEMBER } from "../../../variables"; import { ADMIN, AuthTokenType, CUSTOM, MEMBER, NO_ACCESS } from "../../../variables";
import { import {
OrgPermissionActions, OrgPermissionActions,
OrgPermissionSubjects OrgPermissionSubjects
@@ -161,7 +161,7 @@ export const createMachineIdentity = async (req: Request, res: Response) => {
const organization = await Organization.findById(organizationId); const organization = await Organization.findById(organizationId);
if (!organization) throw BadRequestError({ message: `Organization with id ${organizationId} not found` }); if (!organization) throw BadRequestError({ message: `Organization with id ${organizationId} not found` });
const isCustomRole = ![ADMIN, MEMBER].includes(role); const isCustomRole = ![ADMIN, MEMBER, NO_ACCESS].includes(role);
let customRole; let customRole;
if (isCustomRole) { if (isCustomRole) {
@@ -298,7 +298,7 @@ export const updateMachineIdentity = async (req: Request, res: Response) => {
let customRole; let customRole;
if (role) { if (role) {
const isCustomRole = ![ADMIN, MEMBER].includes(role); const isCustomRole = ![ADMIN, MEMBER, NO_ACCESS].includes(role);
if (isCustomRole) { if (isCustomRole) {
customRole = await Role.findOne({ customRole = await Role.findOne({
slug: role, slug: role,
+1 -1
View File
@@ -66,7 +66,7 @@ class EELicenseService {
secretVersioning: true, secretVersioning: true,
pitRecovery: false, pitRecovery: false,
ipAllowlisting: false, ipAllowlisting: false,
rbac: false, rbac: true,
customRateLimits: false, customRateLimits: false,
customAlerts: false, customAlerts: false,
auditLogs: false, auditLogs: false,
+14 -3
View File
@@ -18,7 +18,7 @@ import {
Membership, Membership,
ServiceTokenData ServiceTokenData
} from "../../models"; } from "../../models";
import { ADMIN, CUSTOM, MEMBER, VIEWER } from "../../variables"; import { ADMIN, CUSTOM, MEMBER, NO_ACCESS, VIEWER } from "../../variables";
import { checkIPAgainstBlocklist } from "../../utils/ip"; import { checkIPAgainstBlocklist } from "../../utils/ip";
import { BadRequestError } from "../../utils/errors"; import { BadRequestError } from "../../utils/errors";
@@ -262,6 +262,13 @@ const buildViewerPermission = () => {
export const viewerProjectPermission = buildViewerPermission(); export const viewerProjectPermission = buildViewerPermission();
const buildNoAccessProjectPermission = () => {
const { build } = new AbilityBuilder<MongoAbility<ProjectPermissionSet>>(createMongoAbility);
return build({ conditionsMatcher });
}
export const noAccessProjectPermissions = buildNoAccessProjectPermission();
/** /**
* Return permissions for user/service pertaining to workspace with id [workspaceId] * Return permissions for user/service pertaining to workspace with id [workspaceId]
* *
@@ -275,7 +282,7 @@ export const getAuthDataProjectPermissions = async ({
authData: AuthData; authData: AuthData;
workspaceId: Types.ObjectId; workspaceId: Types.ObjectId;
}) => { }) => {
let role: "admin" | "member" | "viewer" | "custom"; let role: "admin" | "member" | "viewer" | "no-access" | "custom";
let customRole; let customRole;
switch (authData.actor.type) { switch (authData.actor.type) {
@@ -338,6 +345,8 @@ export const getAuthDataProjectPermissions = async ({
return { permission: memberProjectPermissions }; return { permission: memberProjectPermissions };
case VIEWER: case VIEWER:
return { permission: viewerProjectPermission }; return { permission: viewerProjectPermission };
case NO_ACCESS:
return { permission: noAccessProjectPermissions };
case CUSTOM: { case CUSTOM: {
if (!customRole) throw UnauthorizedRequestError(); if (!customRole) throw UnauthorizedRequestError();
return { return {
@@ -353,7 +362,7 @@ export const getAuthDataProjectPermissions = async ({
} }
export const getRolePermissions = async (role: string, workspaceId: string) => { export const getRolePermissions = async (role: string, workspaceId: string) => {
const isCustomRole = ![ADMIN, MEMBER, VIEWER].includes(role); const isCustomRole = ![ADMIN, MEMBER, VIEWER, NO_ACCESS].includes(role);
if (isCustomRole) { if (isCustomRole) {
const workspaceRole = await Role.findOne({ const workspaceRole = await Role.findOne({
slug: role, slug: role,
@@ -375,6 +384,8 @@ export const getRolePermissions = async (role: string, workspaceId: string) => {
return memberProjectPermissions; return memberProjectPermissions;
case VIEWER: case VIEWER:
return viewerProjectPermission; return viewerProjectPermission;
case NO_ACCESS:
return noAccessProjectPermissions;
default: default:
throw BadRequestError({ message: "Role not found" }); throw BadRequestError({ message: "Role not found" });
} }
+13 -2
View File
@@ -3,7 +3,7 @@ import { AbilityBuilder, MongoAbility, RawRuleOf, createMongoAbility } from "@ca
import { MembershipOrg } from "../../models"; import { MembershipOrg } from "../../models";
import { IRole, Role } from "../models"; import { IRole, Role } from "../models";
import { BadRequestError, UnauthorizedRequestError } from "../../utils/errors"; import { BadRequestError, UnauthorizedRequestError } from "../../utils/errors";
import { ACCEPTED, ADMIN, CUSTOM, MEMBER } from "../../variables"; import { ACCEPTED, ADMIN, CUSTOM, MEMBER, NO_ACCESS} from "../../variables";
import { conditionsMatcher } from "./ProjectRoleService"; import { conditionsMatcher } from "./ProjectRoleService";
export enum OrgPermissionActions { export enum OrgPermissionActions {
@@ -116,6 +116,13 @@ const buildMemberPermission = () => {
export const memberPermissions = buildMemberPermission(); export const memberPermissions = buildMemberPermission();
const buildNoAccessPermission = () => {
const { build } = new AbilityBuilder<MongoAbility<OrgPermissionSet>>(createMongoAbility);
return build({ conditionsMatcher });
}
export const noAccessPermissions = buildNoAccessPermission();
export const getUserOrgPermissions = async (userId: string, orgId: string) => { export const getUserOrgPermissions = async (userId: string, orgId: string) => {
// TODO(akhilmhdh): speed this up by pulling from cache later // TODO(akhilmhdh): speed this up by pulling from cache later
@@ -137,6 +144,8 @@ export const getUserOrgPermissions = async (userId: string, orgId: string) => {
if (membership.role === MEMBER) return { permission: memberPermissions, membership }; if (membership.role === MEMBER) return { permission: memberPermissions, membership };
if (membership.role === NO_ACCESS) return { permission: noAccessPermissions, membership }
if (membership.role === CUSTOM) { if (membership.role === CUSTOM) {
const permission = createMongoAbility<OrgPermissionSet>(membership.customRole.permissions, { const permission = createMongoAbility<OrgPermissionSet>(membership.customRole.permissions, {
conditionsMatcher conditionsMatcher
@@ -148,7 +157,7 @@ export const getUserOrgPermissions = async (userId: string, orgId: string) => {
}; };
export const getOrgRolePermissions = async (role: string, orgId: string) => { export const getOrgRolePermissions = async (role: string, orgId: string) => {
const isCustomRole = ![ADMIN, MEMBER].includes(role); const isCustomRole = ![ADMIN, MEMBER, NO_ACCESS].includes(role);
if (isCustomRole) { if (isCustomRole) {
const orgRole = await Role.findOne({ const orgRole = await Role.findOne({
slug: role, slug: role,
@@ -168,6 +177,8 @@ export const getOrgRolePermissions = async (role: string, orgId: string) => {
return adminPermissions; return adminPermissions;
case MEMBER: case MEMBER:
return memberPermissions; return memberPermissions;
case NO_ACCESS:
return noAccessPermissions;
default: default:
throw BadRequestError({ message: "User org role not found" }); throw BadRequestError({ message: "User org role not found" });
} }
+1 -1
View File
@@ -17,7 +17,7 @@ export const validateMembership = async ({
}: { }: {
userId: Types.ObjectId | string; userId: Types.ObjectId | string;
workspaceId: Types.ObjectId | string; workspaceId: Types.ObjectId | string;
acceptedRoles?: Array<"admin" | "member" | "custom" | "viewer">; acceptedRoles?: Array<"admin" | "member" | "custom" | "viewer" | "no-access">;
}) => { }) => {
const membership = await Membership.findOne({ const membership = await Membership.findOne({
user: userId, user: userId,
+1 -1
View File
@@ -18,7 +18,7 @@ export const validateMembershipOrg = async ({
}: { }: {
userId: Types.ObjectId; userId: Types.ObjectId;
organizationId: Types.ObjectId; organizationId: Types.ObjectId;
acceptedRoles?: Array<"owner" | "admin" | "member" | "custom">; acceptedRoles?: Array<"owner" | "admin" | "member" | "custom" | "no-access">;
acceptedStatuses?: Array<"invited" | "accepted">; acceptedStatuses?: Array<"invited" | "accepted">;
}) => { }) => {
const membershipOrg = await MembershipOrg.findOne({ const membershipOrg = await MembershipOrg.findOne({
+3 -3
View File
@@ -1,11 +1,11 @@
import { Schema, Types, model } from "mongoose"; import { Schema, Types, model } from "mongoose";
import { ADMIN, CUSTOM, MEMBER, VIEWER } from "../variables"; import { ADMIN, CUSTOM, MEMBER, NO_ACCESS, VIEWER } from "../variables";
export interface IMachineMembership { export interface IMachineMembership {
_id: Types.ObjectId; _id: Types.ObjectId;
machineIdentity: Types.ObjectId; machineIdentity: Types.ObjectId;
workspace: Types.ObjectId; workspace: Types.ObjectId;
role: "admin" | "member" | "viewer" | "custom"; role: "admin" | "member" | "viewer" | "no-access" | "custom";
customRole: Types.ObjectId; customRole: Types.ObjectId;
} }
@@ -22,7 +22,7 @@ const machineMembershipSchema = new Schema<IMachineMembership>(
}, },
role: { role: {
type: String, type: String,
enum: [ADMIN, MEMBER, VIEWER, CUSTOM], enum: [ADMIN, MEMBER, VIEWER, CUSTOM, NO_ACCESS],
required: true required: true
}, },
customRole: { customRole: {
+3 -3
View File
@@ -1,11 +1,11 @@
import { Schema, Types, model } from "mongoose"; import { Schema, Types, model } from "mongoose";
import { ADMIN, CUSTOM, MEMBER } from "../variables"; import { ADMIN, CUSTOM, MEMBER, NO_ACCESS} from "../variables";
export interface IMachineMembershipOrg { export interface IMachineMembershipOrg {
_id: Types.ObjectId; _id: Types.ObjectId;
machineIdentity: Types.ObjectId; machineIdentity: Types.ObjectId;
organization: Types.ObjectId; organization: Types.ObjectId;
role: "admin" | "member" | "viewer" | "custom"; role: "admin" | "member" | "viewer" | "no-access" | "custom";
customRole: Types.ObjectId; customRole: Types.ObjectId;
} }
@@ -21,7 +21,7 @@ const machineMembershipOrgSchema = new Schema<IMachineMembershipOrg>(
}, },
role: { role: {
type: String, type: String,
enum: [ADMIN, MEMBER, CUSTOM], enum: [ADMIN, MEMBER, NO_ACCESS, CUSTOM],
required: true required: true
}, },
customRole: { customRole: {
+3 -3
View File
@@ -1,5 +1,5 @@
import { Schema, Types, model } from "mongoose"; import { Schema, Types, model } from "mongoose";
import { ADMIN, CUSTOM, MEMBER, VIEWER } from "../variables"; import { ADMIN, CUSTOM, MEMBER, NO_ACCESS, VIEWER } from "../variables";
export interface IMembershipPermission { export interface IMembershipPermission {
environmentSlug: string; environmentSlug: string;
@@ -11,7 +11,7 @@ export interface IMembership {
user: Types.ObjectId; user: Types.ObjectId;
inviteEmail?: string; inviteEmail?: string;
workspace: Types.ObjectId; workspace: Types.ObjectId;
role: "admin" | "member" | "viewer" | "custom"; role: "admin" | "member" | "viewer" | "no-access" | "custom";
customRole: Types.ObjectId; customRole: Types.ObjectId;
deniedPermissions: IMembershipPermission[]; deniedPermissions: IMembershipPermission[];
} }
@@ -44,7 +44,7 @@ const membershipSchema = new Schema<IMembership>(
}, },
role: { role: {
type: String, type: String,
enum: [ADMIN, MEMBER, VIEWER, CUSTOM], enum: [ADMIN, MEMBER, VIEWER, NO_ACCESS, CUSTOM],
required: true required: true
}, },
customRole: { customRole: {
+3 -3
View File
@@ -1,12 +1,12 @@
import { Document, Schema, Types, model } from "mongoose"; import { Document, Schema, Types, model } from "mongoose";
import { ACCEPTED, ADMIN, CUSTOM, INVITED, MEMBER } from "../variables"; import { ACCEPTED, ADMIN, CUSTOM, INVITED, MEMBER, NO_ACCESS } from "../variables";
export interface IMembershipOrg extends Document { export interface IMembershipOrg extends Document {
_id: Types.ObjectId; _id: Types.ObjectId;
user: Types.ObjectId; user: Types.ObjectId;
inviteEmail: string; inviteEmail: string;
organization: Types.ObjectId; organization: Types.ObjectId;
role: "owner" | "admin" | "member" | "custom"; role: "owner" | "admin" | "member" | "no-access" | "custom";
customRole: Types.ObjectId; customRole: Types.ObjectId;
status: "invited" | "accepted"; status: "invited" | "accepted";
} }
@@ -26,7 +26,7 @@ const membershipOrgSchema = new Schema(
}, },
role: { role: {
type: String, type: String,
enum: [ADMIN, MEMBER, CUSTOM], enum: [ADMIN, MEMBER, NO_ACCESS, CUSTOM],
required: true required: true
}, },
status: { status: {
+1
View File
@@ -3,6 +3,7 @@ export const OWNER = "owner"; // depreciated
export const ADMIN = "admin"; export const ADMIN = "admin";
export const MEMBER = "member"; export const MEMBER = "member";
export const VIEWER = "viewer"; export const VIEWER = "viewer";
export const NO_ACCESS = "no-access";
export const CUSTOM = "custom"; export const CUSTOM = "custom";
// membership statuses // membership statuses
@@ -28,7 +28,7 @@ export type MachineMembershipOrg = {
_id: string; _id: string;
machineIdentity: MachineIdentity; machineIdentity: MachineIdentity;
organization: string; organization: string;
role: "admin" | "member" | "viewer" | "custom"; role: "admin" | "member" | "viewer" | "no-access" | "custom";
customRole?: TRole<string>; customRole?: TRole<string>;
createdAt: string; createdAt: string;
updatedAt: string; updatedAt: string;
@@ -38,7 +38,7 @@ export type MachineMembership = {
_id: string; _id: string;
machineIdentity: MachineIdentity; machineIdentity: MachineIdentity;
organization: string; organization: string;
role: "admin" | "member" | "viewer" | "custom"; role: "admin" | "member" | "viewer" | "no-access" | "custom";
customRole?: TRole<string>; customRole?: TRole<string>;
createdAt: string; createdAt: string;
updatedAt: string; updatedAt: string;
+1 -1
View File
@@ -44,7 +44,7 @@ export type OrgUser = {
}; };
inviteEmail: string; inviteEmail: string;
organization: string; organization: string;
role: "owner" | "admin" | "member" | "custom"; role: "owner" | "admin" | "member" | "no-access" | "custom";
status: "invited" | "accepted" | "verified" | "completed"; status: "invited" | "accepted" | "verified" | "completed";
deniedPermissions: any[]; deniedPermissions: any[];
customRole: string; customRole: string;
@@ -25,7 +25,14 @@ export const MembersPage = withPermission(
<Tabs defaultValue={TabSections.Member}> <Tabs defaultValue={TabSections.Member}>
<TabList> <TabList>
<Tab value={TabSections.Member}>People</Tab> <Tab value={TabSections.Member}>People</Tab>
<Tab value={TabSections.MachineIdentities}>Machine Identities</Tab> <Tab value={TabSections.MachineIdentities}>
<div className="flex items-center">
<p>Machine Identities</p>
<div className="ml-2 rounded-md text-yellow text-sm inline-block bg-yellow/20 px-1.5 pb-[0.03rem] pt-[0.04rem] opacity-80 hover:opacity-100 cursor-default">
Beta
</div>
</div>
</Tab>
<Tab value={TabSections.Roles}>Organization Roles</Tab> <Tab value={TabSections.Roles}>Organization Roles</Tab>
</TabList> </TabList>
<TabPanel value={TabSections.Member}> <TabPanel value={TabSections.Member}>
@@ -94,7 +94,7 @@ export const OrgRoleTable = ({
{isRolesLoading && <TableSkeleton columns={4} innerKey="org-roles" />} {isRolesLoading && <TableSkeleton columns={4} innerKey="org-roles" />}
{(roles as TRole<undefined>[])?.map((role) => { {(roles as TRole<undefined>[])?.map((role) => {
const { _id: id, name, slug } = role; const { _id: id, name, slug } = role;
const isNonMutatable = ["owner", "admin", "member"].includes(slug); const isNonMutatable = ["owner", "admin", "member", "no-access"].includes(slug);
return ( return (
<Tr key={`role-list-${id}`}> <Tr key={`role-list-${id}`}>
@@ -30,7 +30,14 @@ export const MembersPage = withProjectPermission(
<Tabs defaultValue={TabSections.Member}> <Tabs defaultValue={TabSections.Member}>
<TabList> <TabList>
<Tab value={TabSections.Member}>People</Tab> <Tab value={TabSections.Member}>People</Tab>
<Tab value={TabSections.MachineIdentities}>Machine Identities</Tab> <Tab value={TabSections.MachineIdentities}>
<div className="flex items-center">
<p>Machine Identities</p>
<div className="ml-2 rounded-md text-yellow text-sm inline-block bg-yellow/20 px-1.5 pb-[0.03rem] pt-[0.04rem] opacity-80 hover:opacity-100 cursor-default">
Beta
</div>
</div>
</Tab>
<Tab value={TabSections.ServiceTokens}>Service Tokens</Tab> <Tab value={TabSections.ServiceTokens}>Service Tokens</Tab>
<Tab value={TabSections.Roles}>Project Roles</Tab> <Tab value={TabSections.Roles}>Project Roles</Tab>
</TabList> </TabList>
@@ -188,7 +188,7 @@ export const MachineIdentityTable = ({
{!isLoading && data && data?.length === 0 && ( {!isLoading && data && data?.length === 0 && (
<Tr> <Tr>
<Td colSpan={7}> <Td colSpan={7}>
<EmptyState title="No machine identities have been added to this project" icon={faServer} /> <EmptyState title="No MIs have been added to this project" icon={faServer} />
</Td> </Td>
</Tr> </Tr>
)} )}
@@ -101,7 +101,7 @@ export const ProjectRoleList = ({ onSelectRole }: Props) => {
{isRolesLoading && <TableSkeleton columns={4} innerKey="org-roles" />} {isRolesLoading && <TableSkeleton columns={4} innerKey="org-roles" />}
{(roles as TRole<string>[])?.map((role) => { {(roles as TRole<string>[])?.map((role) => {
const { _id: id, name, slug } = role; const { _id: id, name, slug } = role;
const isNonMutatable = ["admin", "member", "viewer"].includes(slug); const isNonMutatable = ["admin", "member", "viewer", "no-access"].includes(slug);
return ( return (
<Tr key={`role-list-${id}`}> <Tr key={`role-list-${id}`}>