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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
+6
-24
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -61,15 +61,7 @@ var mdmMigrationTemplatePreSonoma = template.Must(template.New("mdmMigrationTemp
|
||||
|
||||
Select **Start** and look for this notification in your notification center:` +
|
||||
"\n\n\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\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"
|
||||
|
||||
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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 352 KiB |
Reference in New Issue
Block a user