Fleet UI: MDM settings page (#8977)

This commit is contained in:
RachelElysia 2022-12-16 17:33:10 -05:00 committed by GitHub
parent 3ff0945bd0
commit 929421f3bd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 505 additions and 0 deletions

View file

@ -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,

View file

@ -212,6 +212,7 @@ export interface IConfig {
};
};
};
mdm_enabled?: boolean;
}
export interface IWebhookSettings {

View file

@ -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;

View file

@ -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
)}
</>
);

View 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 dont 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 theyre 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 doesnt 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 theyre 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 theyre 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;

View 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%;
}
}
}

View file

@ -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;

View file

@ -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;

View 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);
},
};

View 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);
});
},
};

View file

@ -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

View file

@ -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,
};

View file

@ -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`,

View file

@ -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,