feat: pre sonoma migration features (#21266)

> Related issue #20311

# Checklist for submitter

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

<!-- Note that API documentation changes are now addressed by the
product design team. -->

- [x] Added/updated tests
- [x] Manual QA for all new/changed functionality
- For Orbit and Fleet Desktop changes:
- [ ] Orbit runs on macOS, Linux and Windows. Check if the orbit
feature/bugfix should only apply to one platform (`runtime.GOOS`).
- [ ] Manual QA must be performed in the three main OSs, macOS, Windows
and Linux.
- [x] Auto-update manual QA, from released version of component to new
version (see [tools/tuf/test](../tools/tuf/test/README.md)).
This commit is contained in:
Jahziel Villasana-Espinoza
2024-08-13 16:14:35 -04:00
committed by GitHub
parent 73658ddeb8
commit d3870f2906
10 changed files with 120 additions and 58 deletions
+1 -1
View File
@@ -70,7 +70,7 @@ jobs:
# - colima is pre-installed in macos-12 runners, but not in macos-13 or
# macos-14 runners
run: |
brew install docker colima
brew install --no-binaries docker colima
colima start --mount $TMPDIR:w
- name: Install Go
+12 -2
View File
@@ -62,7 +62,12 @@ func (svc *Service) TriggerMigrateMDMDevice(ctx context.Context, host *fleet.Hos
return ctxerr.Wrap(ctx, err, "fetching host mdm info")
}
if !fleet.IsEligibleForDEPMigration(host, mdmInfo, connected) && !fleet.IsEligibleForManualMigration(host, mdmInfo, connected) {
manualMigrationEligible, err := fleet.IsEligibleForManualMigration(host, mdmInfo, connected)
if err != nil {
return ctxerr.Wrap(ctx, err, "checking manual migration eligibility")
}
if !fleet.IsEligibleForDEPMigration(host, mdmInfo, connected) && !manualMigrationEligible {
bre.InternalErr = ctxerr.New(ctx, "host not eligible for macOS migration")
}
@@ -139,7 +144,12 @@ func (svc *Service) GetFleetDesktopSummary(ctx context.Context) (fleet.DesktopSu
sum.Notifications.RenewEnrollmentProfile = true
}
if fleet.IsEligibleForDEPMigration(host, mdmInfo, connected) || fleet.IsEligibleForManualMigration(host, mdmInfo, connected) {
manualMigrationEligible, err := fleet.IsEligibleForManualMigration(host, mdmInfo, connected)
if err != nil {
return sum, ctxerr.Wrap(ctx, err, "checking manual migration eligibility")
}
if fleet.IsEligibleForDEPMigration(host, mdmInfo, connected) || manualMigrationEligible {
sum.Notifications.NeedsMDMMigration = true
}
@@ -2,28 +2,16 @@ 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"
@@ -41,18 +29,12 @@ const AutoEnrollMdmModal = ({
<b>System Settings</b>.
</li>
<li>
{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 double-click the <b>[Organization name] enrollment</b>{" "}
profile.
</>
)}
In the search bar, type Profiles. Select <b>Profiles</b>, find and
double-click the{" "}
<b>
[Organization name] <br /> enrollment
</b>{" "}
profile.
</li>
<li>
Enter your password, and select <b>Enroll</b>.
@@ -306,7 +306,7 @@ const DeviceUserPage = ({
const renderEnrollMdmModal = () => {
return host?.dep_assigned_to_fleet ? (
<AutoEnrollMdmModal host={host} onCancel={toggleEnrollMdmModal} />
<AutoEnrollMdmModal onCancel={toggleEnrollMdmModal} />
) : (
<ManualEnrollMdmModal
onCancel={toggleEnrollMdmModal}
+2
View File
@@ -62,6 +62,8 @@ const (
MDMMigrationTypeManual = "manual"
// MDMMigrationTypeADE indicates that the MDM migration is for an ADE enrolled host.
MDMMigrationTypeADE = "ade"
// MDMMigrationTypePreSonoma indicates that the MDM migration is for a host on a macOS version < 14.
MDMMigrationTypePreSonoma = "pre-sonoma"
// MDMMigrationOfflineWatcherInterval is the interval at which the offline watcher checks for
// the presence of the migration file.
MDMMigrationOfflineWatcherInterval = 3 * time.Minute
+39 -25
View File
@@ -61,15 +61,7 @@ var mdmMigrationTemplatePreSonoma = template.Must(template.New("mdmMigrationTemp
Select **Start** and look for this notification in your notification center:` +
"\n\n![Image showing MDM migration notification](https://fleetdm.com/images/permanent/mdm-migration-screenshot-notification-2048x480.png)\n\n" +
"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.",
"After you start, this window will popup every 15 minutes until you finish.",
))
var mdmManualMigrationTemplate = template.Must(template.New("").Parse(`
@@ -94,6 +86,8 @@ var errorTemplate = template.Must(template.New("").Parse(`
Please contact your IT admin [here]({{ .ContactURL }}).
`))
var unenrollPreSonoma = "## Migrate to Fleet\nUnenrolling you from your old MDM. This could take 90 seconds...\n\n![Image showing MDM migration notification](https://fleetdm.com/images/permanent/mdm-migration-pre-sonoma-unenroll-1024x500.png)"
var mdmMigrationTemplateOffline = template.Must(template.New("").Parse(`
## Migrate to Fleet
@@ -280,12 +274,19 @@ func (m *swiftDialogMDMMigrator) render(message string, flags ...string) (chan s
return m.baseDialog.render(flags...)
}
func (m *swiftDialogMDMMigrator) renderLoadingSpinner() (chan swiftDialogExitCode, chan error) {
return m.render("## Migrate to Fleet\nUnenrolling you from your old MDM. This could take 90 seconds...",
func (m *swiftDialogMDMMigrator) renderLoadingSpinner(preSonoma bool) (chan swiftDialogExitCode, chan error) {
body := "## Migrate to Fleet\nUnenrolling you from your old MDM. This could take 90 seconds..."
height := "200"
if preSonoma {
body = unenrollPreSonoma
height = "669"
}
return m.render(body,
"--button1text", "Start",
"--button1disabled",
"--quitkey", "x",
"--height", "220",
"--height", height,
)
}
@@ -377,7 +378,13 @@ func (m *swiftDialogMDMMigrator) renderMigration() error {
log.Debug().Bool("isManualMigration", isManualMigration).Bool("isADEMigration", isADEMigration).Bool("isCurrentlyManuallyEnrolled", isCurrentlyManuallyEnrolled).Str("previousMigrationType", previousMigrationType).Msg("props after assigning")
message, flags, err := m.getMessageAndFlags(isManualMigration)
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")
}
message, flags, err := m.getMessageAndFlags(vers, isManualMigration)
if err != nil {
return fmt.Errorf("getting mdm migrator message: %w", err)
}
@@ -410,7 +417,7 @@ func (m *swiftDialogMDMMigrator) renderMigration() error {
if !m.props.IsUnmanaged {
// show the loading spinner
m.renderLoadingSpinner()
m.renderLoadingSpinner(vers < 14)
// send the API call
if notifyErr := m.handler.NotifyRemote(); notifyErr != nil {
@@ -440,19 +447,32 @@ func (m *swiftDialogMDMMigrator) renderMigration() error {
}
}
if isManualMigration {
switch true {
case vers < 14:
if err := m.mrw.SetMigrationFile(constant.MDMMigrationTypePreSonoma); err != nil {
log.Error().Str("migration_type", constant.MDMMigrationTypeADE).Err(err).Msg("set migration file")
}
log.Info().Msg("showing instructions after pre-sonoma unenrollment")
if err := m.handler.ShowInstructions(); err != nil {
return err
}
case isManualMigration:
if err := m.mrw.SetMigrationFile(constant.MDMMigrationTypeManual); err != nil {
log.Error().Str("migration_type", constant.MDMMigrationTypeManual).Err(err).Msg("set migration file")
}
log.Info().Msg("showing instructions after unenrollment")
log.Info().Msg("showing instructions after manual unenrollment")
if err := m.handler.ShowInstructions(); err != nil {
return err
}
} else {
default:
if err := m.mrw.SetMigrationFile(constant.MDMMigrationTypeADE); err != nil {
log.Error().Str("migration_type", constant.MDMMigrationTypeADE).Err(err).Msg("set migration file")
}
}
// close the spinner
@@ -512,20 +532,14 @@ func (m *swiftDialogMDMMigrator) SetProps(props MDMMigratorProps) {
m.props = props
}
func (m *swiftDialogMDMMigrator) getMessageAndFlags(isManualMigration bool) (*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")
}
func (m *swiftDialogMDMMigrator) getMessageAndFlags(version int, isManualMigration bool) (*bytes.Buffer, []string, error) {
tmpl := mdmADEMigrationTemplate
if isManualMigration {
tmpl = mdmManualMigrationTemplate
}
height := "669"
if vers != 0 && vers < 14 {
if version != 0 && version < 15 {
height = "440"
tmpl = mdmMigrationTemplatePreSonoma
}
+36 -3
View File
@@ -7,6 +7,8 @@ import (
"fmt"
"strings"
"time"
"github.com/Masterminds/semver"
)
type HostStatus string
@@ -1223,14 +1225,45 @@ func IsEligibleForDEPMigration(host *Host, mdmInfo *HostMDM, isConnectedToFleetM
(!isConnectedToFleetMDM || mdmInfo.Name != WellKnownMDMFleet)
}
var macOSADEMigrationOnlyLastVersion = semver.MustParse("14")
// IsEligibleForManualMigration returns true if the host is manually enrolled into a 3rd party MDM
// and is able to migrate to Fleet.
func IsEligibleForManualMigration(host *Host, mdmInfo *HostMDM, isConnectedToFleetMDM bool) bool {
return host.IsOsqueryEnrolled() &&
func IsEligibleForManualMigration(host *Host, mdmInfo *HostMDM, isConnectedToFleetMDM bool) (bool, error) {
goodVersion, err := IsMacOSMajorVersionOK(host)
if err != nil {
return false, fmt.Errorf("checking macOS version for manual migration eligibility: %w", err)
}
return goodVersion &&
host.IsOsqueryEnrolled() &&
!host.IsDEPAssignedToFleet() &&
mdmInfo != nil &&
!mdmInfo.InstalledFromDep &&
!mdmInfo.HasJSONProfileAssigned() &&
mdmInfo.Enrolled &&
(!isConnectedToFleetMDM || mdmInfo.Name != WellKnownMDMFleet)
(!isConnectedToFleetMDM || mdmInfo.Name != WellKnownMDMFleet), nil
}
func IsMacOSMajorVersionOK(host *Host) (bool, error) {
if host == nil {
return false, nil
}
parts := strings.Split(host.OSVersion, " ")
if len(parts) < 2 || parts[0] != "macOS" {
return false, nil
}
version, err := semver.NewVersion(parts[1])
if err != nil {
return false, fmt.Errorf("parsing macOS version \"%s\": %w", parts[1], err)
}
if version.GreaterThan(macOSADEMigrationOnlyLastVersion) {
return true, nil
}
return false, nil
}
+17 -1
View File
@@ -223,6 +223,7 @@ func TestIsEligibleForDEPMigration(t *testing.T) {
enrolledInThirdPartyMDM bool
expected bool
expectedManual bool
hostOS string
}{
{
name: "Eligible for DEP migration",
@@ -268,6 +269,7 @@ func TestIsEligibleForDEPMigration(t *testing.T) {
enrolledInThirdPartyMDM: true,
expected: false,
expectedManual: true,
hostOS: "macOS 14.5",
},
{
name: "Not eligible - DEP assigned and DEP profile failed",
@@ -304,6 +306,17 @@ func TestIsEligibleForDEPMigration(t *testing.T) {
enrolledInThirdPartyMDM: true,
expected: false,
expectedManual: true,
hostOS: "macOS 14.5",
},
{
name: "Manual migration ineligible - enrolled in 3rd party, not DEP, but OS version too low",
osqueryHostID: ptr.String("some-id"),
depAssignedToFleet: ptr.Bool(false),
depProfileResponse: "",
enrolledInThirdPartyMDM: true,
expected: false,
expectedManual: false,
hostOS: "macOS 13.9",
},
}
@@ -312,6 +325,7 @@ func TestIsEligibleForDEPMigration(t *testing.T) {
host := &Host{
OsqueryHostID: tc.osqueryHostID,
DEPAssignedToFleet: tc.depAssignedToFleet,
OSVersion: tc.hostOS,
}
mdmInfo := &HostMDM{
@@ -321,7 +335,9 @@ func TestIsEligibleForDEPMigration(t *testing.T) {
}
require.Equal(t, tc.expected, IsEligibleForDEPMigration(host, mdmInfo, false))
require.Equal(t, tc.expectedManual, IsEligibleForManualMigration(host, mdmInfo, false))
manual, err := IsEligibleForManualMigration(host, mdmInfo, false)
require.NoError(t, err)
require.Equal(t, tc.expectedManual, manual)
})
}
}
+6 -1
View File
@@ -203,8 +203,13 @@ func (svc *Service) GetOrbitConfig(ctx context.Context) (fleet.OrbitConfig, erro
notifs.RenewEnrollmentProfile = true
}
manualMigrationEligible, err := fleet.IsEligibleForManualMigration(host, mdmInfo, isConnectedToFleetMDM)
if err != nil {
return fleet.OrbitConfig{}, ctxerr.Wrap(ctx, err, "checking manual migration eligibility")
}
if appConfig.MDM.MacOSMigration.Enable &&
(fleet.IsEligibleForDEPMigration(host, mdmInfo, isConnectedToFleetMDM) || fleet.IsEligibleForManualMigration(host, mdmInfo, isConnectedToFleetMDM)) {
(fleet.IsEligibleForDEPMigration(host, mdmInfo, isConnectedToFleetMDM) || manualMigrationEligible) {
notifs.NeedsMDMMigration = true
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 352 KiB