Fix bug where MDM migration fails when attempting to renew enrollment profiles on macOS Sonoma devices (#19726)

This commit is contained in:
Sarah Gillespie
2024-06-13 14:13:43 -05:00
committed by GitHub
parent a327aacdc4
commit c6042de9c9
8 changed files with 118 additions and 38 deletions
+1
View File
@@ -0,0 +1 @@
- Fixed bug where MDM migration failed when attempting to renew enrollment profiles on macOS Sonoma devices.
@@ -2,16 +2,27 @@ import React from "react";
import Button from "components/buttons/Button";
import Modal from "components/Modal";
import { IDeviceUserResponse } from "interfaces/host";
interface IAutoEnrollMdmModalProps {
host: IDeviceUserResponse["host"];
onCancel: () => void;
}
const baseClass = "auto-enroll-mdm-modal";
const AutoEnrollMdmModal = ({
host: { platform, os_version },
onCancel,
}: IAutoEnrollMdmModalProps): JSX.Element => {
let isMacOsSonomaOrLater = false;
if (platform === "darwin" && os_version.startsWith("macOS ")) {
const [major] = os_version
.replace("macOS ", "")
.split(".")
.map((s) => parseInt(s, 10));
isMacOsSonomaOrLater = major >= 14;
}
return (
<Modal
title="Turn on MDM"
@@ -25,13 +36,21 @@ const AutoEnrollMdmModal = ({
</p>
<ol>
<li>
Open your Macs notification center by selecting the date and time
in the top right corner of your screen.
From the Apple menu in the top left corner of your screen, select{" "}
<b>System Settings</b> or <b>System Preferences</b>.
</li>
<li>
Select the <b>Device Enrollment</b> notification. This will open{" "}
<b>System Settings</b> or <b>System Preferences</b>. Select{" "}
<b>Allow</b>.
{isMacOsSonomaOrLater ? (
<>
In the sidebar menu, select <b>Enroll in Remote Management</b>,
and select <b>Enroll</b>.
</>
) : (
<>
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>.
@@ -311,7 +311,7 @@ const DeviceUserPage = ({
const renderEnrollMdmModal = () => {
return host?.dep_assigned_to_fleet ? (
<AutoEnrollMdmModal onCancel={toggleEnrollMdmModal} />
<AutoEnrollMdmModal host={host} onCancel={toggleEnrollMdmModal} />
) : (
<ManualEnrollMdmModal
onCancel={toggleEnrollMdmModal}
+1 -1
View File
@@ -247,6 +247,6 @@ func parseEnrollmentProfileValue(line []byte, key string) (string, bool) {
// showEnrollmentProfileCmd is declared as a variable so it can be overwritten by tests.
var showEnrollmentProfileCmd = func() ([]byte, error) {
cmd := exec.Command("/usr/bin/profiles", "show", "-type", "enrollment")
cmd := exec.Command("sh", "-c", `launchctl asuser $(id -u $(stat -f "%u" /dev/console)) profiles show -type enrollment`)
return cmd.Output()
}
+2 -1
View File
@@ -3,5 +3,6 @@
package update
func runRenewEnrollmentProfile() error {
return runCmdCollectErr("/usr/bin/profiles", "renew", "--type", "enrollment")
cmd := `launchctl asuser $(id -u $(stat -f "%u" /dev/console)) profiles renew -type enrollment`
return runCmdCollectErr("sh", "-c", cmd)
}
+7 -6
View File
@@ -104,14 +104,15 @@ func (h *renewEnrollmentProfileConfigReceiver) Run(config *fleet.OrbitConfig) er
fn = runRenewEnrollmentProfile
}
if err := fn(); err != nil {
// TODO: Look into whether we should increment lastRun here or implement a
// backoff to avoid unnecessary user notification popups and mitigate rate
// limiting by Apple.
log.Info().Err(err).Msg("calling /usr/bin/profiles to renew enrollment profile failed")
} else {
h.lastRun = time.Now()
log.Info().Msg("successfully called /usr/bin/profiles to renew enrollment profile")
// TODO: Design a better way to backoff `profiles show` so that the device doesn't get rate
// limited by Apple. For now, wait at least 2 minutes before retrying.
h.lastRun = time.Now().Add(-h.Frequency).Add(2 * time.Minute)
return nil
}
h.lastRun = time.Now()
log.Info().Msg("successfully called /usr/bin/profiles to renew enrollment profile")
} else {
log.Debug().Msg("skipped calling /usr/bin/profiles to renew enrollment profile, last run was too recent")
}
+82 -24
View File
@@ -8,6 +8,8 @@ import (
"fmt"
"os"
"os/exec"
"strconv"
"strings"
"sync"
"text/template"
"time"
@@ -48,7 +50,7 @@ const mdmUnenrollmentTotalWaitTime = 90 * time.Second
// between unenrollment checks.
const defaultUnenrollmentRetryInterval = 5 * time.Second
var mdmMigrationTemplate = template.Must(template.New("mdmMigrationTemplate").Parse(`
var mdmMigrationTemplatePreSonoma = template.Must(template.New("mdmMigrationTemplate").Parse(`
## Migrate to Fleet
Select **Start** and look for this notification in your notification center:` +
@@ -56,6 +58,14 @@ Select **Start** and look for this notification in your notification center:` +
"After you start, this window will popup every 15-20 minutes until you finish.",
))
var mdmMigrationTemplate = template.Must(template.New("mdmMigrationTemplate").Parse(`
## Migrate to Fleet
Select **Start** and Remote Management window will appear soon:` +
"\n\n![Image showing MDM migration notification](https://fleetdm.com/images/permanent/mdm-migration-sonoma-1500x938.png)\n\n" +
"After you start, this window will popup every 15-20 minutes until you finish.",
))
var errorTemplate = template.Must(template.New("").Parse(`
### Something's gone wrong.
@@ -291,29 +301,9 @@ func (m *swiftDialogMDMMigrator) waitForUnenrollment() error {
}
func (m *swiftDialogMDMMigrator) renderMigration() error {
var message bytes.Buffer
if err := mdmMigrationTemplate.Execute(
&message,
m.props,
); err != nil {
return fmt.Errorf("execute template: %w", err)
}
flags := []string{
// main button
"--button1text", "Start",
// secondary button
"--button2text", "Later",
"--height", "440",
}
if m.props.OrgInfo.ContactURL != "" {
flags = append(flags,
// info button
"--infobuttontext", "Unsure? Contact IT",
"--infobuttonaction", m.props.OrgInfo.ContactURL,
"--quitoninfo",
)
message, flags, err := m.getMessageAndFlags()
if err != nil {
return fmt.Errorf("getting mdm migrator message: %w", err)
}
exitCodeCh, errCh := m.render(message.String(), flags...)
@@ -419,3 +409,71 @@ func (m *swiftDialogMDMMigrator) ShowInterval() error {
func (m *swiftDialogMDMMigrator) SetProps(props MDMMigratorProps) {
m.props = props
}
func (m *swiftDialogMDMMigrator) getMessageAndFlags() (*bytes.Buffer, []string, error) {
vers, err := m.getMacOSMajorVersion()
if err != nil {
// log error for debugging and continue with default template
log.Error().Err(err).Msg("getting macOS major version failed: using default migration template")
}
tmpl := mdmMigrationTemplate
height := "669"
if vers != 0 && vers < 14 {
height = "440"
tmpl = mdmMigrationTemplatePreSonoma
}
var message bytes.Buffer
if err := tmpl.Execute(
&message,
m.props,
); err != nil {
return nil, nil, fmt.Errorf("executing migrqation template: %w", err)
}
flags := []string{
// main button
"--button1text", "Start",
// secondary button
"--button2text", "Later",
"--height", height,
}
if m.props.OrgInfo.ContactURL != "" {
flags = append(flags,
// info button
"--infobuttontext", "Unsure? Contact IT",
"--infobuttonaction", m.props.OrgInfo.ContactURL,
"--quitoninfo",
)
}
return &message, flags, nil
}
// TODO: make this a variable for testing
func (m *swiftDialogMDMMigrator) getMacOSMajorVersion() (int, error) {
cmd := exec.Command("sw_vers", "-productVersion")
out, err := cmd.Output()
if err != nil {
return 0, fmt.Errorf("getting macOS version: %w", err)
}
parts := strings.SplitN(string(out), ".", 2)
switch len(parts) {
case 0:
// this should never happen
return 0, errors.New("getting macOS version: sw_vers command returned no output")
case 1:
// unexpected, so log for debugging
log.Debug().Msgf("parsing macOS version: expected 2 parts, got 1: %s", out)
default:
// ok
}
major, err := strconv.Atoi(parts[0])
if err != nil {
return 0, fmt.Errorf("parsing macOS major version: %w", err)
}
return major, nil
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB