UI: Add automatic EnrollMdm modal (#9455)

# Addresses #9365 

# Implements
MDM enrollment modal that handles both automatic and manual enrollment
instructions:
- Automatic:
<img width="1181" alt="Screenshot 2023-01-20 at 4 33 50 PM"
src="https://user-images.githubusercontent.com/61553566/213829293-6d4a5053-9a3c-4f52-8cf8-a6607dc8df4e.png">
- Manual:

<img width="1158" alt="Screenshot 2023-01-20 at 4 35 04 PM"
src="https://user-images.githubusercontent.com/61553566/213829369-73ae779d-14a8-4aa7-9c6a-b97d046d0dc1.png">

- Also includes (by mistake, but might as well include them now) some
small bash scripts for use in MDM development
# Checklist for submitter

If some of the following don't apply, delete the relevant line.

- [x] Changes file added for user-visible changes in `changes/` 
- [x] Updated testing inventory
- [x] Manual QA for all new/changed functionality

---------

Co-authored-by: Jacob Shandling <jacob@fleetdm.com>
This commit is contained in:
Jacob Shandling 2023-01-30 11:44:33 -08:00 committed by GitHub
parent d3565dc032
commit 60712144f2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 158 additions and 46 deletions

View file

@ -0,0 +1 @@
* Add modal for automatic enrollment of a macOS host to MDM

View file

@ -0,0 +1,48 @@
import React from "react";
import Button from "components/buttons/Button";
import Modal from "components/Modal";
interface IAutoEnrollMdmModalProps {
onCancel: () => void;
}
const baseClass = "auto-enroll-mdm-modal enroll-mdm-modal";
const AutoEnrollMdmModal = ({
onCancel,
}: IAutoEnrollMdmModalProps): JSX.Element => {
return (
<Modal title="Turn on MDM" onExit={onCancel} className={baseClass}>
<div>
<p className={`${baseClass}__description`}>
To turn on MDM, Apple Inc. requires that you install a profile.
</p>
<ol>
<li>
From the Apple menu in the top left corner of your screen, select{" "}
<b>System Settings</b> or <b>System Preferences</b>.
</li>
<li>
In the search bar, type Profiles. Select <b>Profiles</b>, find and
select <b>Enrollment Profile</b>, and select <b>Install</b>.
</li>
<li>
Enter your password, and select <b>Enroll</b>.
</li>
<li>
Close this window and select <b>Refetch</b> on your My device page
to tell your organization that MDM is on.
</li>
</ol>
<div className="modal-cta-wrap">
<Button type="button" onClick={onCancel} variant="brand">
Done
</Button>
</div>
</div>
</Modal>
);
};
export default AutoEnrollMdmModal;

View file

@ -0,0 +1 @@
export { default } from "./AutoEnrollMdmModal";

View file

@ -34,11 +34,12 @@ import AboutCard from "../cards/About";
import SoftwareCard from "../cards/Software";
import PoliciesCard from "../cards/Policies";
import InfoModal from "./InfoModal";
import ManualEnrollMdmModal from "./ManualEnrollMdmModal";
import InfoIcon from "../../../../../assets/images/icon-info-purple-14x14@2x.png";
import FleetIcon from "../../../../../assets/images/fleet-avatar-24x24@2x.png";
import PolicyDetailsModal from "../cards/Policies/HostPoliciesTable/PolicyDetailsModal";
import AutoEnrollMdmModal from "./AutoEnrollMdmModal";
import ManualEnrollMdmModal from "./ManualEnrollMdmModal";
const baseClass = "device-user";
@ -59,7 +60,7 @@ const DeviceUserPage = ({
const [isPremiumTier, setIsPremiumTier] = useState(false);
const [showInfoModal, setShowInfoModal] = useState(false);
const [showMdmModal, setShowMdmModal] = useState(false);
const [showEnrollMdmModal, setShowEnrollMdmModal] = useState(false);
const [refetchStartTime, setRefetchStartTime] = useState<number | null>(null);
const [showRefetchSpinner, setShowRefetchSpinner] = useState(false);
const [hostSoftware, setHostSoftware] = useState<ISoftware[]>([]);
@ -215,9 +216,9 @@ const DeviceUserPage = ({
setShowInfoModal(!showInfoModal);
}, [showInfoModal, setShowInfoModal]);
const toggleTurnOnMdmModal = useCallback(() => {
setShowMdmModal(!showMdmModal);
}, [showMdmModal, setShowMdmModal]);
const toggleEnrollMdmModal = useCallback(() => {
setShowEnrollMdmModal(!showEnrollMdmModal);
}, [showEnrollMdmModal, setShowEnrollMdmModal]);
const togglePolicyDetailsModal = useCallback(
(policy: IHostPolicy) => {
@ -264,10 +265,22 @@ const DeviceUserPage = ({
const statusClassName = classnames("status", `status--${host?.status}`);
const turnOnMdmButton = (
<Button variant="unstyled" onClick={() => setShowMdmModal(true)}>
<Button variant="unstyled" onClick={toggleEnrollMdmModal}>
<b>Turn on MDM</b>
</Button>
);
const renderEnrollMdmModal = () => {
return host?.mdm_enrollment_status === "Pending" ? (
<AutoEnrollMdmModal onCancel={toggleEnrollMdmModal} />
) : (
<ManualEnrollMdmModal
onCancel={toggleEnrollMdmModal}
token={deviceAuthToken}
/>
);
};
const renderDeviceUserPage = () => {
const failingPoliciesCount = host?.issues?.failing_policies_count || 0;
return (
@ -339,12 +352,7 @@ const DeviceUserPage = ({
</Tabs>
</TabsWrapper>
{showInfoModal && <InfoModal onCancel={toggleInfoModal} />}
{showMdmModal && (
<ManualEnrollMdmModal
onCancel={toggleTurnOnMdmModal}
token={deviceAuthToken}
/>
)}
{showEnrollMdmModal && renderEnrollMdmModal()}
</div>
)}
{!!host && showPolicyDetailsModal && (

View file

@ -11,17 +11,17 @@ import Spinner from "components/Spinner";
import mdmAPI from "services/entities/mdm";
export interface IInfoModalProps {
interface IManualEnrollMdmModalProps {
onCancel: () => void;
token: string;
token?: string;
}
const baseClass = "manual-enroll-mdm-modal";
const baseClass = "manual-enroll-mdm-modal enroll-mdm-modal";
const ManualEnrollMdmModal = ({
onCancel,
token,
}: IInfoModalProps): JSX.Element => {
token = "",
}: IManualEnrollMdmModalProps): JSX.Element => {
const { renderFlash } = useContext(NotificationContext);
const [isDownloadingProfile, setIsDownloadingProfile] = useState(false);
@ -60,15 +60,15 @@ const ManualEnrollMdmModal = ({
return false;
};
const renderModalContent = () => {
if (isFetchingMdmProfile) {
return <Spinner />;
}
if (fetchMdmProfileError) {
return <DataError card />;
}
if (isFetchingMdmProfile) {
return <Spinner />;
}
if (fetchMdmProfileError) {
return <DataError card />;
}
return (
return (
<Modal title="Turn on MDM" onExit={onCancel} className={baseClass}>
<div>
<p className={`${baseClass}__description`}>
To turn on MDM, Apple Inc. requires that you download and install a
@ -119,12 +119,6 @@ const ManualEnrollMdmModal = ({
</Button>
</div>
</div>
);
};
return (
<Modal title="Turn on MDM" onExit={onCancel} className={baseClass}>
{renderModalContent()}
</Modal>
);
};

View file

@ -1,19 +1,4 @@
.manual-enroll-mdm-modal {
width: 800px;
&__description {
margin: $pad-large 0;
}
ol {
padding-left: 0;
}
li {
margin-bottom: $pad-large;
list-style: number inside;
}
&__download-button {
margin-top: 12px;
}

View file

@ -2,6 +2,23 @@
// TODO: talk to rachel about removing this
background-color: $ui-off-white;
}
.enroll-mdm-modal {
width: 800px;
&__description {
margin: $pad-large 0;
}
ol {
padding-left: 0;
}
li {
margin-bottom: $pad-large;
list-style: number inside;
}
}
.device-user {
display: flex;
flex-wrap: wrap;

5
tools/dbutils/delete-host Executable file
View file

@ -0,0 +1,5 @@
#!/bin/bash
# experimental doesn't always work right
docker compose exec mysql mysql -uroot -ptoor -Dfleet -e "DELETE FROM host_device_auth WHERE host_id=$1;"

3
tools/dbutils/get-host-tokens Executable file
View file

@ -0,0 +1,3 @@
#!/bin/bash
docker compose exec mysql mysql -uroot -ptoor -Dfleet -e "SELECT host_id, token FROM host_device_auth;"

View file

@ -0,0 +1,7 @@
#!/bin/bash
# experimental doesn't always work right
NEW_ID=$1
NEW_TOKEN=$2
docker compose exec mysql mysql -uroot -ptoor -Dfleet -e "INSERT INTO host_device_auth VALUES ($NEW_ID, $NEW_TOKEN, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP);"

View file

@ -0,0 +1,43 @@
#!/bin/bash
# To toggle MDM, run `source toggle-mdm-dev`
# Requires the env at $FLEET_ENV_PATH to contain logic something like:
# if [[ $USE_MDM == "1" ]]; then
# # for UI dev:
# export FLEET_DEV_MDM_ENABLED=1
# # for MDM server
# export FLEET_MDM_APPLE_ENABLE=1
# export FLEET_MDM_APPLE_SCEP_CHALLENGE=scepchallenge
# MDM_PATH={PATH_TO_YOUR_MDM_RELATED_KEYS_AND_CERTS}
# export FLEET_MDM_APPLE_SCEP_CERT=$MDM_PATH"fleet-mdm-apple-scep.crt"
# export FLEET_MDM_APPLE_SCEP_KEY=$MDM_PATH"fleet-mdm-apple-scep.key"
# export FLEET_MDM_APPLE_BM_SERVER_TOKEN=$MDM_PATH"downloadtoken.p7m"
# export FLEET_MDM_APPLE_BM_CERT=$MDM_PATH"fleet-apple-mdm-bm-public-key.crt"
# export FLEET_MDM_APPLE_BM_KEY=$MDM_PATH"fleet-apple-mdm-bm-private.key"
# #below files are from the shared Fleet 1Password
# export FLEET_MDM_APPLE_APNS_CERT=$MDM_PATH"mdmcert.download.push.pem"
# export FLEET_MDM_APPLE_APNS_KEY=$MDM_PATH"mdmcert.download.push.key"
# else
# unset FLEET_DEV_MDM_ENABLED
# unset FLEET_MDM_APPLE_ENABLE
# unset FLEET_MDM_APPLE_SCEP_CHALLENGE
# unset FLEET_MDM_APPLE_SCEP_CERT
# unset FLEET_MDM_APPLE_SCEP_KEY
# unset FLEET_MDM_APPLE_BM_SERVER_TOKEN
# unset FLEET_MDM_APPLE_BM_CERT
# unset FLEET_MDM_APPLE_BM_KEY
# #below files are from the shared Fleet 1Password
# unset FLEET_MDM_APPLE_APNS_CERT
# unset FLEET_MDM_APPLE_APNS_KEY
# fi
if [[ $USE_MDM == "1" ]]; then
export USE_MDM=0
else
export USE_MDM=1
fi
source $FLEET_ENV_PATH