diff --git a/cmd/fleetctl/fleetctl/mdm.go b/cmd/fleetctl/fleetctl/mdm.go index f3ce51c9c8..7b35994f03 100644 --- a/cmd/fleetctl/fleetctl/mdm.go +++ b/cmd/fleetctl/fleetctl/mdm.go @@ -111,7 +111,7 @@ func mdmRunCommand() *cli.Command { return err } - mdmHostPlatform := fleet.MDMPlatform(host.Platform) + mdmHostPlatform := fleet.ClassicMDMPlatform(host.Platform) if mdmHostPlatform != mdmPlatform && mdmPlatform != "" { return errors.New(`Command can't run on hosts with different platforms. Make sure the hosts specified in the "hosts" flag are either all macOS or all Windows hosts.`) } @@ -238,7 +238,7 @@ func mdmUnlockCommand() *cli.Command { return fmt.Errorf("Failed to unlock host: %w", err) } - if fleet.MDMPlatform(host.Platform) == "darwin" { + if fleet.ClassicMDMPlatform(host.Platform) == "darwin" { fmt.Fprintf(c.App.Writer, ` Use this 6 digit PIN to unlock the host: @@ -366,9 +366,8 @@ func hostMdmActionSetup(c *cli.Context, hostIdent string, actionType string) (cl return nil, nil, err } - // check mdm is on for the host. Android isn't in fleet.MDMPlatform. - // See eng-init story: https://github.com/fleetdm/fleet/issues/46118 - if fleet.MDMSupported(host.Platform) || fleet.IsAndroidPlatform(host.Platform) { + // check mdm is on for the host + if fleet.MDMTurnedOnSupported(host.Platform) { if host.MDM.ConnectedToFleet == nil || !*host.MDM.ConnectedToFleet { return nil, nil, fmt.Errorf("Can't %s the host because it doesn't have MDM turned on.", actionType) } diff --git a/cmd/fleetctl/fleetctl/mdm_test.go b/cmd/fleetctl/fleetctl/mdm_test.go index 0904154ec9..1181f975c5 100644 --- a/cmd/fleetctl/fleetctl/mdm_test.go +++ b/cmd/fleetctl/fleetctl/mdm_test.go @@ -742,6 +742,15 @@ func TestMDMUnlockCommand(t *testing.T) { }, mdmInfo: &fleet.HostMDM{Enrolled: true, Name: fleet.WellKnownMDMFleet}, } + androidNotConnected := testhost{ + host: &fleet.Host{ + ID: 15, + UUID: "android-not-connected", + Platform: "android", + MDM: fleet.MDMHostData{Name: fleet.WellKnownMDMFleet, EnrollmentStatus: new("Off"), ConnectedToFleet: new(false)}, + }, + mdmInfo: &fleet.HostMDM{Enrolled: false, Name: fleet.WellKnownMDMFleet}, + } hostByUUID := make(map[string]testhost) hostsByID := make(map[uint]testhost) @@ -758,6 +767,7 @@ func TestMDMUnlockCommand(t *testing.T) { macEnrolledLP, winEnrolledWP, macEnrolledWP, + androidNotConnected, } { hostByUUID[h.host.UUID] = h hostsByID[h.host.ID] = h @@ -918,6 +928,7 @@ fleetctl get host %s {appCfgAllMDM, "valid macos but pending lock", []string{"--host", macEnrolledLP.host.UUID}, "Host has pending lock request."}, {appCfgAllMDM, "valid windows but pending wipe", []string{"--host", winEnrolledWP.host.UUID}, "Host has pending wipe request."}, {appCfgAllMDM, "valid macos but pending wipe", []string{"--host", macEnrolledWP.host.UUID}, "Host has pending wipe request."}, + {appCfgAllMDM, "valid android but not connected", []string{"--host", androidNotConnected.host.UUID}, `Can't unlock the host because it doesn't have MDM turned on.`}, } runTestCases(t, ds, "unlock", successfulOutput, cases) @@ -1076,6 +1087,15 @@ func TestMDMWipeCommand(t *testing.T) { Platform: "linux", }, } + androidNotConnected := testhost{ + host: &fleet.Host{ + ID: 21, + UUID: "android-not-connected", + Platform: "android", + MDM: fleet.MDMHostData{Name: fleet.WellKnownMDMFleet, EnrollmentStatus: new("Off"), ConnectedToFleet: new(false)}, + }, + mdmInfo: &fleet.HostMDM{Enrolled: false, Name: fleet.WellKnownMDMFleet}, + } linuxHostIDs := []uint{linuxEnrolled.host.ID, linuxEnrolled2.host.ID, linuxEnrolled3.host.ID} @@ -1100,6 +1120,7 @@ func TestMDMWipeCommand(t *testing.T) { macEnrolledWiped, winEnrolledLocked, macEnrolledLocked, + androidNotConnected, } { hostByUUID[h.host.UUID] = h hostsByID[h.host.ID] = h @@ -1290,6 +1311,7 @@ func TestMDMWipeCommand(t *testing.T) { {appCfgAllMDM, "valid macos but host is locked", []string{"--host", macEnrolledLocked.host.UUID}, "Host cannot be wiped until it is unlocked."}, {appCfgAllMDM, "valid macos but host is locked", []string{"--host", macEnrolledLocked.host.UUID}, "Host cannot be wiped until it is unlocked."}, {appCfgScriptsDisabled, "valid linux and scripts are disabled", []string{"--host", linuxEnrolled.host.UUID}, ""}, + {appCfgAllMDM, "valid android but not connected", []string{"--host", androidNotConnected.host.UUID}, `Can't wipe the host because it doesn't have MDM turned on.`}, } successfulOutput := func(ident string) string { @@ -1366,14 +1388,23 @@ func TestMDMClearPasscodeCommand(t *testing.T) { macNotEnrolled := testhost{ host: &fleet.Host{ID: 2, UUID: "mac-not-enrolled-cp", Platform: "darwin"}, } + androidNotConnected := testhost{ + host: &fleet.Host{ + ID: 3, UUID: "android-not-connected-cp", Platform: "android", + MDM: fleet.MDMHostData{Name: fleet.WellKnownMDMFleet, EnrollmentStatus: new("Off"), ConnectedToFleet: new(false)}, + }, + mdmInfo: &fleet.HostMDM{Enrolled: false, Name: fleet.WellKnownMDMFleet}, + } hostByUUID := map[string]testhost{ - macEnrolled.host.UUID: macEnrolled, - macNotEnrolled.host.UUID: macNotEnrolled, + macEnrolled.host.UUID: macEnrolled, + macNotEnrolled.host.UUID: macNotEnrolled, + androidNotConnected.host.UUID: androidNotConnected, } hostsByID := map[uint]testhost{ - macEnrolled.host.ID: macEnrolled, - macNotEnrolled.host.ID: macNotEnrolled, + macEnrolled.host.ID: macEnrolled, + macNotEnrolled.host.ID: macNotEnrolled, + androidNotConnected.host.ID: androidNotConnected, } ds := setupTestServer(t) @@ -1404,6 +1435,7 @@ func TestMDMClearPasscodeCommand(t *testing.T) { {appCfgAllMDM, "empty host", []string{"--host", ""}, `No host targeted. Please provide --host.`}, {appCfgAllMDM, "unknown host", []string{"--host", "doesnotexist"}, fleet.HostNotFoundErrMsg}, {appCfgAllMDM, "darwin not enrolled", []string{"--host", macNotEnrolled.host.UUID}, "Can't clear passcode for the host because it doesn't have MDM turned on."}, + {appCfgAllMDM, "android not connected", []string{"--host", androidNotConnected.host.UUID}, "Can't clear passcode for the host because it doesn't have MDM turned on."}, } for _, c := range cases { ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) { return c.appCfg, nil } diff --git a/server/datastore/mysql/apple_mdm.go b/server/datastore/mysql/apple_mdm.go index 0ce04f543d..8c47444ccb 100644 --- a/server/datastore/mysql/apple_mdm.go +++ b/server/datastore/mysql/apple_mdm.go @@ -2192,7 +2192,7 @@ func (ds *Datastore) MDMTurnOff(ctx context.Context, uuid string) (users []*flee return ctxerr.Wrap(ctx, err, "getting host info from UUID") } - if !fleet.MDMSupported(host.Platform) { + if !fleet.ClassicMDMSupported(host.Platform) { return ctxerr.Errorf(ctx, "unsupported host platform: %q", host.Platform) } @@ -4534,7 +4534,7 @@ func (ds *Datastore) MDMResetEnrollment(ctx context.Context, hostUUID string, sc } host := hosts[0] - if !fleet.MDMSupported(host.Platform) { + if !fleet.ClassicMDMSupported(host.Platform) { return ctxerr.Errorf(ctx, "unsupported host platform: %q", host.Platform) } diff --git a/server/datastore/mysql/mdm.go b/server/datastore/mysql/mdm.go index aa7f88ccd0..9d80633195 100644 --- a/server/datastore/mysql/mdm.go +++ b/server/datastore/mysql/mdm.go @@ -337,7 +337,7 @@ WHERE ` + whereTeam ) } byUUID[h.UUID] = h - switch fleet.MDMPlatform(h.Platform) { + switch fleet.ClassicMDMPlatform(h.Platform) { case "darwin": appleUUIDs = append(appleUUIDs, h.UUID) case "windows": diff --git a/server/fleet/mdm.go b/server/fleet/mdm.go index ce1e515898..9a088eb650 100644 --- a/server/fleet/mdm.go +++ b/server/fleet/mdm.go @@ -1099,25 +1099,43 @@ func (m MDMConfigAsset) Copy() MDMConfigAsset { return clone } -// MDMPlatform returns "darwin" or "windows" as MDM platforms -// derived from a host's platform (hosts.platform field). +// ClassicMDMPlatform returns "darwin" or "windows" as MDM platforms derived +// from a host's platform (a raw hosts.platform value, or the collapsed one +// returned by Host.FleetPlatform), or "" for platforms that don't take part in +// the classic MDM command pipeline. // // Note that "darwin" as MDM platform means Apple (we keep it as "darwin" // to keep backwards compatibility throughout the app). -func MDMPlatform(hostPlatform string) string { +// +// Android is deliberately not part of this list: Android hosts don't take part +// in the classic MDM command pipeline (raw XML/plist commands, the +// nano_commands and mdm_windows_commands listings, the mdmlifecycle hooks and +// the host_mdm turn-off/reset paths MDMTurnOff and MDMResetEnrollment). Android +// has its own commands table and its own unenroll path. To check whether Fleet +// can turn MDM on for a platform at all, use MDMTurnedOnSupported instead. +func ClassicMDMPlatform(hostPlatform string) string { switch hostPlatform { case "darwin", "ios", "ipados": return "darwin" case "windows": return "windows" - // TODO(android): add android to this list? } return "" } -// MDMSupported returns whether MDM is supported for a given host platform. -func MDMSupported(hostPlatform string) bool { - return MDMPlatform(hostPlatform) != "" +// ClassicMDMSupported returns whether the given host platform takes part in the +// classic MDM command pipeline. It returns false for Android, see +// ClassicMDMPlatform for details. +func ClassicMDMSupported(hostPlatform string) bool { + return ClassicMDMPlatform(hostPlatform) != "" +} + +// MDMTurnedOnSupported returns whether Fleet supports any form of MDM +// enrollment for the given host platform, Android included. Use this for the +// checks that only care about MDM being turned on for the host, such as the +// "Can't the host because it doesn't have MDM turned on." pre-checks. +func MDMTurnedOnSupported(hostPlatform string) bool { + return ClassicMDMSupported(hostPlatform) || IsAndroidPlatform(hostPlatform) } // FilterMacOSOnlyProfilesFromIOSIPadOS will filter out profiles that are only for macOS devices diff --git a/server/fleet/mdm_test.go b/server/fleet/mdm_test.go index 51d42c8b4d..e12e465f5f 100644 --- a/server/fleet/mdm_test.go +++ b/server/fleet/mdm_test.go @@ -657,6 +657,37 @@ func TestFleetVarRenewalIDRegexp(t *testing.T) { } } +func TestMDMPlatformSupport(t *testing.T) { + cases := []struct { + hostPlatform string + wantClassicPlatform string + wantTurnedOn bool + }{ + {"darwin", "darwin", true}, + {"ios", "darwin", true}, + {"ipados", "darwin", true}, + {"windows", "windows", true}, + // Android hosts can have MDM turned on, but they don't take part in the + // classic MDM command pipeline. + {"android", "", true}, + // "linux" isn't a hosts.platform value, but it is what + // Host.FleetPlatform collapses the distros to. + {"linux", "", false}, + {"ubuntu", "", false}, + {"rhel", "", false}, + {"chrome", "", false}, + {"", "", false}, + {"unknown", "", false}, + } + for _, tc := range cases { + t.Run(tc.hostPlatform, func(t *testing.T) { + require.Equal(t, tc.wantClassicPlatform, fleet.ClassicMDMPlatform(tc.hostPlatform)) + require.Equal(t, tc.wantClassicPlatform != "", fleet.ClassicMDMSupported(tc.hostPlatform)) + require.Equal(t, tc.wantTurnedOn, fleet.MDMTurnedOnSupported(tc.hostPlatform)) + }) + } +} + func TestFilterMacOSOnlyProfilesFromIOSIPadOS(t *testing.T) { for _, tc := range []struct { profiles []*fleet.MDMAppleProfilePayload diff --git a/server/service/hosts.go b/server/service/hosts.go index 879f03be07..4f2ec21c50 100644 --- a/server/service/hosts.go +++ b/server/service/hosts.go @@ -628,7 +628,7 @@ func (svc *Service) DeleteHosts(ctx context.Context, ids []uint, filter *map[str lifecycleErrs := []error{} serialsWithErrs := []string{} for _, host := range hosts { - if fleet.MDMSupported(host.Platform) { + if fleet.ClassicMDMSupported(host.Platform) { if err := mdmLifecycle.Do(ctx, mdmlifecycle.HostOptions{ Action: mdmlifecycle.HostActionDelete, Host: host, @@ -1153,7 +1153,7 @@ func (svc *Service) DeleteHost(ctx context.Context, id uint) error { return err } - if fleet.MDMSupported(host.Platform) { + if fleet.ClassicMDMSupported(host.Platform) { mdmLifecycle := mdmlifecycle.New(svc.ds, svc.logger, svc.NewActivity) err = mdmLifecycle.Do(ctx, mdmlifecycle.HostOptions{ Action: mdmlifecycle.HostActionDelete, diff --git a/server/service/mdm.go b/server/service/mdm.go index d1dcfc107b..b466a7ffb8 100644 --- a/server/service/mdm.go +++ b/server/service/mdm.go @@ -546,7 +546,7 @@ func (svc *Service) RunMDMCommand(ctx context.Context, rawBase64Cmd string, host for platform := range platforms { commandPlatform = platform } - if !fleet.MDMSupported(commandPlatform) { + if !fleet.ClassicMDMSupported(commandPlatform) { err := fleet.NewInvalidArgumentError("host_uuids", "Invalid platform. You can only run MDM commands on Windows or Apple hosts.") return nil, ctxerr.Wrap(ctx, err, "check host platform") }