mirror of
https://github.com/fleetdm/fleet
synced 2026-05-23 17:08:53 +00:00
Fleet UI: MDM settings page (#8977)
This commit is contained in:
parent
3ff0945bd0
commit
929421f3bd
14 changed files with 505 additions and 0 deletions
|
|
@ -73,6 +73,7 @@ type InitialStateType = {
|
|||
isSandboxMode?: boolean;
|
||||
isFreeTier?: boolean;
|
||||
isPremiumTier?: boolean;
|
||||
isMdmEnabled?: boolean;
|
||||
isGlobalAdmin?: boolean;
|
||||
isGlobalMaintainer?: boolean;
|
||||
isGlobalObserver?: boolean;
|
||||
|
|
@ -109,6 +110,7 @@ export const initialState = {
|
|||
isSandboxMode: false,
|
||||
isFreeTier: undefined,
|
||||
isPremiumTier: undefined,
|
||||
isMdmEnabled: undefined,
|
||||
isGlobalAdmin: undefined,
|
||||
isGlobalMaintainer: undefined,
|
||||
isGlobalObserver: undefined,
|
||||
|
|
@ -151,6 +153,8 @@ const setPermissions = (
|
|||
isSandboxMode: permissions.isSandboxMode(config),
|
||||
isFreeTier: permissions.isFreeTier(config),
|
||||
isPremiumTier: permissions.isPremiumTier(config),
|
||||
// isMdmEnabled: permissions.isMdmEnabled(config),
|
||||
isMdmEnabled: true, // MDM TODO: Remove when backend is merged
|
||||
isGlobalAdmin: permissions.isGlobalAdmin(user),
|
||||
isGlobalMaintainer: permissions.isGlobalMaintainer(user),
|
||||
isGlobalObserver: permissions.isGlobalObserver(user),
|
||||
|
|
@ -255,6 +259,7 @@ const AppProvider = ({ children }: Props): JSX.Element => {
|
|||
isSandboxMode: state.isSandboxMode,
|
||||
isFreeTier: state.isFreeTier,
|
||||
isPremiumTier: state.isPremiumTier,
|
||||
isMdmEnabled: state.isMdmEnabled,
|
||||
isGlobalAdmin: state.isGlobalAdmin,
|
||||
isGlobalMaintainer: state.isGlobalMaintainer,
|
||||
isGlobalObserver: state.isGlobalObserver,
|
||||
|
|
|
|||
|
|
@ -212,6 +212,7 @@ export interface IConfig {
|
|||
};
|
||||
};
|
||||
};
|
||||
mdm_enabled?: boolean;
|
||||
}
|
||||
|
||||
export interface IWebhookSettings {
|
||||
|
|
|
|||
|
|
@ -1,3 +1,18 @@
|
|||
export interface IMdmApple {
|
||||
common_name: string;
|
||||
serial_number: string;
|
||||
issuer: string;
|
||||
renew_date: string;
|
||||
}
|
||||
|
||||
export interface IMdmAppleBm {
|
||||
default_team?: string;
|
||||
apple_id: string;
|
||||
organization_name: string;
|
||||
mdm_server_url: string;
|
||||
renew_date: string;
|
||||
}
|
||||
|
||||
export interface IMdmEnrollmentCardData {
|
||||
status: "Enrolled (manual)" | "Enrolled (automatic)" | "Unenrolled";
|
||||
hosts: number;
|
||||
|
|
|
|||
|
|
@ -184,6 +184,7 @@ const OrgSettingsForm = ({
|
|||
handleSubmit={onFormSubmit}
|
||||
isUpdatingSettings={isUpdatingSettings}
|
||||
/>
|
||||
// <Mdm /> // MDM TODO: Uncomment this as a shortcut to view MDM page before Gabe's work is done
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
293
frontend/pages/admin/IntegrationsPage/cards/Mdm/Mdm.tsx
Normal file
293
frontend/pages/admin/IntegrationsPage/cards/Mdm/Mdm.tsx
Normal file
|
|
@ -0,0 +1,293 @@
|
|||
import React, { useContext, useState } from "react";
|
||||
import { useQuery } from "react-query";
|
||||
import FileSaver from "file-saver";
|
||||
|
||||
import { AppContext } from "context/app";
|
||||
import { NotificationContext } from "context/notification";
|
||||
|
||||
import mdmAppleAPI from "services/entities/mdm_apple";
|
||||
import mdmAppleBmAPI from "services/entities/mdm_apple_bm";
|
||||
import { IMdmApple, IMdmAppleBm } from "interfaces/mdm";
|
||||
|
||||
import Button from "components/buttons/Button";
|
||||
import CustomLink from "components/CustomLink";
|
||||
import Spinner from "components/Spinner";
|
||||
import DataError from "components/DataError";
|
||||
import Icon from "components/Icon";
|
||||
import TooltipWrapper from "components/TooltipWrapper";
|
||||
|
||||
import RequestModal from "./components/RequestModal";
|
||||
import EditTeamModal from "./components/EditTeamModal";
|
||||
|
||||
// MDM TODO: key validation?
|
||||
// import { isValidKeys } from "../../..";
|
||||
|
||||
const baseClass = "mdm-integrations";
|
||||
|
||||
const readableDate = (date: string) => {
|
||||
const dateString = new Date(date);
|
||||
|
||||
return new Intl.DateTimeFormat(navigator.language, {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
}).format(dateString);
|
||||
};
|
||||
|
||||
const Mdm = (): JSX.Element => {
|
||||
const { isPremiumTier } = useContext(AppContext);
|
||||
const { renderFlash } = useContext(NotificationContext);
|
||||
|
||||
const [showRequestModal, setShowRequestModal] = useState(false);
|
||||
const [showEditTeamModal, setShowEditTeamModal] = useState(false);
|
||||
|
||||
const {
|
||||
data: mdmApple,
|
||||
isLoading: isLoadingMdmApple,
|
||||
error: errorMdmApple,
|
||||
} = useQuery<IMdmApple, Error, IMdmApple>(
|
||||
["mdmAppleAPI"],
|
||||
() => mdmAppleAPI.getAppleAPNInfo(),
|
||||
{
|
||||
enabled: isPremiumTier,
|
||||
staleTime: 5000,
|
||||
}
|
||||
);
|
||||
|
||||
const {
|
||||
data: mdmAppleBm,
|
||||
isLoading: isLoadingMdmAppleBm,
|
||||
error: errorMdmAppleBm,
|
||||
} = useQuery<IMdmAppleBm, Error, IMdmAppleBm>(
|
||||
["mdmAppleBmAPI"],
|
||||
() => mdmAppleBmAPI.getAppleBMInfo(),
|
||||
{
|
||||
enabled: isPremiumTier,
|
||||
staleTime: 5000,
|
||||
}
|
||||
);
|
||||
|
||||
// MDM TODO: Test manually after backend is merged
|
||||
const {
|
||||
data: keys,
|
||||
error: fetchKeysError,
|
||||
isFetching: isFetchingKeys,
|
||||
} = useQuery<string, Error>(["keys"], () => mdmAppleBmAPI.loadKeys(), {
|
||||
enabled: isPremiumTier,
|
||||
refetchOnWindowFocus: false,
|
||||
});
|
||||
|
||||
const toggleRequestModal = () => {
|
||||
setShowRequestModal(!showRequestModal);
|
||||
};
|
||||
|
||||
const toggleEditTeamModal = () => {
|
||||
setShowEditTeamModal(!showEditTeamModal);
|
||||
};
|
||||
|
||||
const onDownloadKeys = (evt: React.MouseEvent) => {
|
||||
evt.preventDefault();
|
||||
|
||||
// MDM TODO: Confirm error flash message
|
||||
if (isFetchingKeys || fetchKeysError) {
|
||||
renderFlash(
|
||||
"error",
|
||||
"Your MDM business manager keys could not be downloaded. Please try again."
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (keys) {
|
||||
// MDM TODO: Validate keys like we validate certificates?
|
||||
// if (keys && isValidKeys(keys)) {
|
||||
const filename = "fleet.pem";
|
||||
const file = new global.window.File([keys], filename, {
|
||||
type: "application/x-pem-file",
|
||||
});
|
||||
|
||||
FileSaver.saveAs(file);
|
||||
} else {
|
||||
renderFlash(
|
||||
"error",
|
||||
"Your MDM business manager keys could not be downloaded. Please TODO ACTION."
|
||||
);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const renderMdmAppleSection = () => {
|
||||
if (errorMdmApple) {
|
||||
return <DataError />;
|
||||
}
|
||||
|
||||
if (!mdmApple) {
|
||||
return (
|
||||
<>
|
||||
<div className={`${baseClass}__section-description`}>
|
||||
Connect Fleet to Apple Push Certificates Portal to change settings
|
||||
and install software on your macOS hosts.
|
||||
</div>
|
||||
<div className={`${baseClass}__section-instructions`}>
|
||||
<p>
|
||||
1. Request a certificate signing request (CSR) and key for Apple
|
||||
Push Notification Service (APNs) and a certificate and key for
|
||||
Simple Certificate Enrollment Protocol (SCEP).
|
||||
</p>
|
||||
<Button onClick={toggleRequestModal} variant="brand">
|
||||
Request
|
||||
</Button>
|
||||
<p>2. Go to your email to download your CSR.</p>
|
||||
<p>
|
||||
3.{" "}
|
||||
<CustomLink
|
||||
url="https://identity.apple.com/pushcert/"
|
||||
text="Sign in to Apple Push Certificates Portal"
|
||||
newTab
|
||||
/>
|
||||
<br />
|
||||
If you don’t have an Apple ID, select <b>Create yours now</b>.
|
||||
</p>
|
||||
<p>
|
||||
4. In Apple Push Certificates Portal, select{" "}
|
||||
<b>Create a Certificate</b>, upload your CSR, and download your
|
||||
APNs certificate.
|
||||
</p>
|
||||
<p>
|
||||
5. Deploy Fleet with <b>mdm</b> configuration.{" "}
|
||||
<CustomLink url="https://www.youtube.com" text="See how" newTab />
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={`${baseClass}__section-description`}>
|
||||
To change settings and install software on your macOS hosts, Apple
|
||||
Inc. requires an Apple Push Notification service (APNs) certificate.
|
||||
</div>
|
||||
<div className={`${baseClass}__section-information`}>
|
||||
<h4>Common name (CN)</h4>
|
||||
<p>{mdmApple.common_name}</p>
|
||||
<h4>Serial number</h4>
|
||||
<p>{mdmApple.serial_number}</p>
|
||||
<h4>Issuer</h4>
|
||||
<p>{mdmApple.issuer}</p>
|
||||
<h4>Renew date</h4>
|
||||
<p>{readableDate(mdmApple.renew_date)}</p>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const renderMdmAppleBm = () => {
|
||||
if (errorMdmAppleBm) {
|
||||
return <DataError />;
|
||||
}
|
||||
|
||||
if (!mdmAppleBm) {
|
||||
return (
|
||||
<>
|
||||
<div className={`${baseClass}__section-description`}>
|
||||
Connect Fleet to your Apple Business Manager account to
|
||||
automatically enroll macOS hosts to Fleet when they’re first
|
||||
unboxed.
|
||||
</div>
|
||||
<div className={`${baseClass}__section-instructions`}>
|
||||
<p>1. Download your public and private keys.</p>
|
||||
<Button onClick={onDownloadKeys} variant="brand">
|
||||
Download
|
||||
</Button>
|
||||
<p>
|
||||
2. Sign in to{" "}
|
||||
<CustomLink
|
||||
url="https://business.apple.com/"
|
||||
text="Apple Business Manager"
|
||||
newTab
|
||||
/>
|
||||
<br />
|
||||
If your organization doesn’t have an account, select{" "}
|
||||
<b>Enroll now</b>.
|
||||
</p>
|
||||
<p>
|
||||
3. In Apple Business Manager, upload your public key and download
|
||||
your server token.
|
||||
</p>
|
||||
<p>
|
||||
4. Deploy Fleet with <b>mdm</b> configuration.{" "}
|
||||
<CustomLink
|
||||
url="https://business.apple.com/"
|
||||
text="See how"
|
||||
newTab
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={`${baseClass}__section-description`}>
|
||||
To use automatically enroll macOS hosts to Fleet when they’re first
|
||||
unboxed, Apple Inc. requires a server token.
|
||||
</div>
|
||||
<div className={`${baseClass}__section-information`}>
|
||||
<h4>
|
||||
<TooltipWrapper tipContent="macOS hosts will be added to this team when they’re first unboxed.">
|
||||
Team
|
||||
</TooltipWrapper>
|
||||
</h4>
|
||||
<p>
|
||||
{mdmAppleBm.default_team || "No team"}{" "}
|
||||
<Button
|
||||
className={`${baseClass}__edit-team-btn`}
|
||||
onClick={toggleEditTeamModal}
|
||||
variant="text-icon"
|
||||
>
|
||||
Edit <Icon name="pencil" />
|
||||
</Button>
|
||||
</p>
|
||||
<h4>Apple ID</h4>
|
||||
<p>{mdmAppleBm.apple_id}</p>
|
||||
<h4>Organization name</h4>
|
||||
<p>{mdmAppleBm.organization_name}</p>
|
||||
<h4>MDM Server URL</h4>
|
||||
<p>{mdmAppleBm.mdm_server_url}</p>
|
||||
<h4>Renew date</h4>
|
||||
<p>{readableDate(mdmAppleBm.renew_date)}</p>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
<div className={`${baseClass}__section`}>
|
||||
<h2>Apple Push Certificates Portal</h2>
|
||||
{isLoadingMdmApple ? <Spinner /> : renderMdmAppleSection()}
|
||||
</div>
|
||||
{isPremiumTier && (
|
||||
<div className={`${baseClass}__section`}>
|
||||
<h2>Apple Business Manager</h2>
|
||||
{isLoadingMdmAppleBm ? <Spinner /> : renderMdmAppleBm()}
|
||||
</div>
|
||||
)}
|
||||
{showRequestModal && (
|
||||
<RequestModal
|
||||
onCancel={toggleRequestModal}
|
||||
onRequest={toggleRequestModal}
|
||||
/>
|
||||
)}
|
||||
{showEditTeamModal && (
|
||||
<EditTeamModal
|
||||
onCancel={toggleEditTeamModal}
|
||||
onEdit={toggleEditTeamModal}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Mdm;
|
||||
54
frontend/pages/admin/IntegrationsPage/cards/Mdm/_styles.scss
Normal file
54
frontend/pages/admin/IntegrationsPage/cards/Mdm/_styles.scss
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
.mdm-integrations {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 65%;
|
||||
gap: 80px;
|
||||
|
||||
&__section {
|
||||
margin: 0 0 $pad-large;
|
||||
|
||||
h2 {
|
||||
padding-bottom: $pad-small;
|
||||
max-width: 100%;
|
||||
font-size: $medium;
|
||||
font-weight: $regular;
|
||||
color: $core-fleet-black;
|
||||
border-bottom: solid 1px $ui-fleet-blue-15;
|
||||
margin: 0 0 $pad-xxlarge;
|
||||
@media (min-width: $break-990) {
|
||||
max-width: 65%;
|
||||
}
|
||||
}
|
||||
|
||||
h4 {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.mdm-integrations__edit-team-btn {
|
||||
margin-left: 12px;
|
||||
|
||||
.children-wrapper {
|
||||
gap: $pad-small;
|
||||
}
|
||||
}
|
||||
|
||||
.component__tooltip-wrapper__tip-text {
|
||||
max-width: initial;
|
||||
}
|
||||
}
|
||||
|
||||
&__section-description,
|
||||
&__section-instructions,
|
||||
&__section-information {
|
||||
font-size: $x-small;
|
||||
color: $core-fleet-black;
|
||||
width: 100%;
|
||||
@media (min-width: $break-990) {
|
||||
width: 60%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
import React from "react";
|
||||
|
||||
import Modal from "components/Modal";
|
||||
import Button from "components/buttons/Button";
|
||||
|
||||
interface IEditTeamModal {
|
||||
onCancel: () => void;
|
||||
onEdit: () => void;
|
||||
}
|
||||
|
||||
const baseClass = "edit-team-modal";
|
||||
|
||||
const EditTeamModal = ({ onCancel, onEdit }: IEditTeamModal): JSX.Element => {
|
||||
return (
|
||||
<Modal title="Edit team" onExit={onCancel} className={baseClass}>
|
||||
<>
|
||||
Cool beans
|
||||
<div className="modal-cta-wrap">
|
||||
<Button onClick={onEdit} variant="brand">
|
||||
Save
|
||||
</Button>
|
||||
<Button onClick={onCancel} variant="inverse">
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditTeamModal;
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
import React from "react";
|
||||
|
||||
import Modal from "components/Modal";
|
||||
import Button from "components/buttons/Button";
|
||||
|
||||
interface IRequestModal {
|
||||
onCancel: () => void;
|
||||
onRequest: () => void;
|
||||
}
|
||||
|
||||
const baseClass = "request-modal";
|
||||
|
||||
const RequestModal = ({ onCancel, onRequest }: IRequestModal): JSX.Element => {
|
||||
return (
|
||||
<Modal title="Request" onExit={onCancel} className={baseClass}>
|
||||
<>
|
||||
Cool beans
|
||||
<div className="modal-cta-wrap">
|
||||
<Button onClick={onRequest} variant="brand">
|
||||
Request
|
||||
</Button>
|
||||
<Button onClick={onCancel} variant="inverse">
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default RequestModal;
|
||||
12
frontend/services/entities/mdm_apple.ts
Normal file
12
frontend/services/entities/mdm_apple.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
import { sendRequest } from "services/mock_service/service/service"; // MDM TODO: Replace when backend is merged
|
||||
// import sendRequest from "services";
|
||||
import endpoints from "utilities/endpoints";
|
||||
|
||||
export default {
|
||||
getAppleAPNInfo: () => {
|
||||
const { MDM_APPLE } = endpoints;
|
||||
const path = MDM_APPLE;
|
||||
return sendRequest("GET", path);
|
||||
},
|
||||
};
|
||||
31
frontend/services/entities/mdm_apple_bm.ts
Normal file
31
frontend/services/entities/mdm_apple_bm.ts
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
import { sendRequest } from "services/mock_service/service/service"; // MDM TODO: Replace when backend is merged
|
||||
// import sendRequest from "services";
|
||||
import endpoints from "utilities/endpoints";
|
||||
|
||||
export default {
|
||||
getAppleBMInfo: () => {
|
||||
const { MDM_APPLE_BM } = endpoints;
|
||||
const path = MDM_APPLE_BM;
|
||||
return sendRequest("GET", path);
|
||||
},
|
||||
loadKeys: () => {
|
||||
const { MDM_APPLE_BM_KEYS } = endpoints;
|
||||
const path = MDM_APPLE_BM_KEYS;
|
||||
|
||||
// MDM TODO: Originally written for certificate_chain for certificate, refactor for keys when backend is merged
|
||||
return sendRequest("GET", path).then(({ certificate_chain }) => {
|
||||
let decodedKeys;
|
||||
try {
|
||||
decodedKeys = global.window.atob(certificate_chain);
|
||||
} catch (err) {
|
||||
return Promise.reject(`Unable to decode keys: ${err}`);
|
||||
}
|
||||
if (!decodedKeys) {
|
||||
return Promise.reject("Missing or undefined keys.");
|
||||
}
|
||||
|
||||
return Promise.resolve(decodedKeys);
|
||||
});
|
||||
},
|
||||
};
|
||||
|
|
@ -22,6 +22,8 @@ const REQUEST_RESPONSE_MAPPINGS: IResponses = {
|
|||
// request query string is hostname, uuid, or mac address; response is host detail excluding any
|
||||
// expensive data operations
|
||||
"targets?query={*}": RESPONSES.hosts,
|
||||
"mdm/apple": RESPONSES.mdmApple,
|
||||
"mdm/apple_bm": RESPONSES.mdmAppleBm,
|
||||
},
|
||||
POST: {
|
||||
// request body is ISelectedTargets
|
||||
|
|
|
|||
|
|
@ -11,6 +11,24 @@ const count = {
|
|||
targets_missing_in_action: 0,
|
||||
};
|
||||
|
||||
// MDM TODO: Remove mock when backend is merged
|
||||
const mdmApple = {
|
||||
common_name: "Mock backend response APSP:04b46ce0-xxxx-xxxx-xxxx-xxxxxxxx",
|
||||
serial_number: "Mock backend response 123938388712",
|
||||
issuer:
|
||||
"Mock backend response Apple Application Integration 2 Certification Authority",
|
||||
renew_date: "2023-09-30T00:00:00Z",
|
||||
};
|
||||
|
||||
// MDM TODO: Remove mock when backend is merged
|
||||
const mdmAppleBm = {
|
||||
default_team: "Mock backend response Apples",
|
||||
apple_id: "Mock backend response rachel@fleetdm.com",
|
||||
organization_name: "Mock backend response Fleet Device Management",
|
||||
mdm_server_url: "Mock backend response https://fleet.organization.com/mdm",
|
||||
renew_date: "2023-09-30T00:00:00Z",
|
||||
};
|
||||
|
||||
const hosts = {
|
||||
hosts: [
|
||||
{
|
||||
|
|
@ -368,4 +386,6 @@ export default {
|
|||
count,
|
||||
hosts,
|
||||
labels,
|
||||
mdmApple,
|
||||
mdmAppleBm,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -31,6 +31,9 @@ export default {
|
|||
LOGIN: `/${API_VERSION}/fleet/login`,
|
||||
LOGOUT: `/${API_VERSION}/fleet/logout`,
|
||||
MACADMINS: `/${API_VERSION}/fleet/macadmins`,
|
||||
MDM_APPLE: `/${API_VERSION}/fleet/mdm/apple`,
|
||||
MDM_APPLE_BM: `/${API_VERSION}/fleet/mdm/apple_bm`,
|
||||
MDM_APPLE_BM_KEYS: `/${API_VERSION}/fleet/mdm/apple_bm/keys`,
|
||||
MDM_SUMMARY: `/${API_VERSION}/fleet/hosts/summary/mdm`,
|
||||
HOST_MDM: (id: number) => `/${API_VERSION}/fleet/hosts/${id}/mdm`,
|
||||
ME: `/${API_VERSION}/fleet/me`,
|
||||
|
|
|
|||
|
|
@ -13,6 +13,11 @@ export const isPremiumTier = (config: IConfig): boolean => {
|
|||
return config.license.tier === "premium";
|
||||
};
|
||||
|
||||
// MDM TODO: Ensure we grabbed the correct config key when backend is merged
|
||||
export const isMdmEnabled = (config: IConfig): boolean => {
|
||||
return config.mdm_enabled === true;
|
||||
};
|
||||
|
||||
export const isGlobalAdmin = (user: IUser): boolean => {
|
||||
return user.global_role === "admin";
|
||||
};
|
||||
|
|
@ -106,6 +111,7 @@ export default {
|
|||
isSandboxMode,
|
||||
isFreeTier,
|
||||
isPremiumTier,
|
||||
isMdmEnabled,
|
||||
isGlobalAdmin,
|
||||
isGlobalMaintainer,
|
||||
isGlobalObserver,
|
||||
|
|
|
|||
Loading…
Reference in a new issue