Delete iOS host refetch commands on MDM re-enrollment (#30158)

This commit is contained in:
Sarah Gillespie
2025-06-23 10:14:00 -05:00
committed by GitHub
parent 3d8b89bdf0
commit 15b60c1f41
3 changed files with 148 additions and 1 deletions
+2
View File
@@ -0,0 +1,2 @@
- Fixed issue where iOS devices were not refetching at the expected cadence when re-enrolled without first
deleting the host.
+13 -1
View File
@@ -4719,7 +4719,19 @@ func (ds *Datastore) MDMResetEnrollment(ctx context.Context, hostUUID string, sc
return ctxerr.Wrap(ctx, err, "resetting disk encryption key information for host")
}
if host.Platform == "darwin" {
// Do platform-specific cleanup.
switch host.Platform {
case "ios", "ipados":
// Clear refetch commands for iOS and iPadOS hosts.
// FIXME: Do we care about wipe/lock commands? How can we consolidate this with host deletion? See https://github.com/fleetdm/fleet/pull/29283/files#r2098735905
_, err = tx.ExecContext(ctx, `
DELETE FROM host_mdm_commands
WHERE host_id = ? AND instr(command_type, ?)`, host.ID, fleet.RefetchBaseCommandUUIDPrefix)
if err != nil {
return ctxerr.Wrap(ctx, err, "resetting host_mdm_commands for host")
}
case "darwin":
// Deleting the matching entry on this table will cause
// the aggregate report to show this host as 'pending' to
// install the bootstrap package.
@@ -13,6 +13,7 @@ import (
"path/filepath"
"strings"
"testing"
"time"
"github.com/fleetdm/fleet/v4/pkg/fleetdbase"
"github.com/fleetdm/fleet/v4/pkg/mdm/mdmtest"
@@ -951,3 +952,135 @@ func (s *integrationMDMTestSuite) TestLifecycleSCEPCertExpiration() {
})
require.True(t, stillMigrated)
}
func (s *integrationMDMTestSuite) TestRefetchAfterReenrollIOSNoDelete() {
t := s.T()
checkInstallFleetdCommandSent := func(mdmDevice *mdmtest.TestAppleMDMClient, wantCommand bool) {
foundInstallFleetdCommand := false
cmd, err := mdmDevice.Idle()
require.NoError(t, err)
for cmd != nil {
var fullCmd micromdm.CommandPayload
require.NoError(t, plist.Unmarshal(cmd.Raw, &fullCmd))
if manifest := fullCmd.Command.InstallEnterpriseApplication.ManifestURL; manifest != nil {
foundInstallFleetdCommand = true
require.Equal(t, "InstallEnterpriseApplication", cmd.Command.RequestType)
require.Contains(t, *fullCmd.Command.InstallEnterpriseApplication.ManifestURL, fleetdbase.GetPKGManifestURL())
}
cmd, err = mdmDevice.Acknowledge(cmd.CommandUUID)
require.NoError(t, err)
}
require.Equal(t, wantCommand, foundInstallFleetdCommand)
}
triggerRefetchCron := func(hostID uint, expectCmds int) {
mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
_, err := q.ExecContext(context.Background(), `UPDATE hosts SET detail_updated_at = DATE_SUB(NOW(), INTERVAL 2 HOUR) WHERE id = ?`, hostID)
return err
})
trigger := triggerRequest{
Name: string(fleet.CronAppleMDMIPhoneIPadRefetcher),
}
s.Do("POST", "/api/latest/fleet/trigger", trigger, http.StatusOK)
// Wait until MDM commands are set up
done := make(chan struct{})
go func() {
ticker := time.NewTicker(100 * time.Millisecond)
defer ticker.Stop()
for range ticker.C {
commands, err := s.ds.GetHostMDMCommands(context.Background(), hostID)
require.NoError(t, err)
if len(commands) >= expectCmds {
done <- struct{}{}
return
}
}
}()
select {
case <-done:
case <-time.After(10 * time.Second):
t.Error("Timeout: MDM commands not queued up")
}
}
// create a global enroll secret
globalSecret := "global_secret"
var applyResp applyEnrollSecretSpecResponse
s.DoJSON("POST", "/api/latest/fleet/spec/enroll_secret", applyEnrollSecretSpecRequest{
Spec: &fleet.EnrollSecretSpec{
Secrets: []*fleet.EnrollSecret{{Secret: globalSecret}},
},
}, http.StatusOK, &applyResp)
hwModel := "iPad13,16"
mdmDevice := mdmtest.NewTestMDMClientAppleOTA(
s.server.URL,
"global_secret",
hwModel,
)
// enrollTime := time.Now().UTC().Truncate(time.Second)
require.NoError(t, mdmDevice.Enroll())
s.runWorker()
checkInstallFleetdCommandSent(mdmDevice, false)
hostByIdentifierResp := getHostResponse{}
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/identifier/%s", mdmDevice.UUID), nil, http.StatusOK, &hostByIdentifierResp)
require.Equal(t, hwModel, hostByIdentifierResp.Host.HardwareModel)
require.Equal(t, "ipados", hostByIdentifierResp.Host.Platform)
require.False(t, hostByIdentifierResp.Host.RefetchRequested)
hostID := hostByIdentifierResp.Host.ID
triggerRefetchCron(hostID, 3)
hostByIdentifierResp = getHostResponse{}
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/identifier/%s", mdmDevice.UUID), nil, http.StatusOK, &hostByIdentifierResp)
require.Equal(t, hwModel, hostByIdentifierResp.Host.HardwareModel)
require.Equal(t, "ipados", hostByIdentifierResp.Host.Platform)
require.False(t, hostByIdentifierResp.Host.RefetchRequested)
cmd, err := mdmDevice.Idle()
require.NoError(t, err)
for cmd != nil {
switch cmd.Command.RequestType {
case "InstalledApplicationList":
cmd, err = mdmDevice.AcknowledgeInstalledApplicationList(mdmDevice.UUID, cmd.CommandUUID, []fleet.Software{})
require.NoError(t, err)
case "CertificateList":
cmd, err = mdmDevice.AcknowledgeCertificateList(mdmDevice.UUID, cmd.CommandUUID, []*x509.Certificate{})
require.NoError(t, err)
case "DeviceInformation":
cmd, err = mdmDevice.AcknowledgeDeviceInformation(mdmDevice.UUID, cmd.CommandUUID, "Test Name", "iPhone 16")
require.NoError(t, err)
default:
require.Fail(t, "unexpected command", cmd.Command.RequestType)
}
}
commands, err := s.ds.GetHostMDMCommands(context.Background(), hostID)
require.NoError(t, err)
require.Len(t, commands, 0)
triggerRefetchCron(hostID, 3)
commands, err = s.ds.GetHostMDMCommands(context.Background(), hostID)
require.NoError(t, err)
require.Len(t, commands, 3)
cmdTypes := make([]string, 0, len(commands))
for _, cmd := range commands {
cmdTypes = append(cmdTypes, cmd.CommandType)
}
require.ElementsMatch(t, []string{fleet.RefetchDeviceCommandUUIDPrefix, fleet.RefetchAppsCommandUUIDPrefix, fleet.RefetchCertsCommandUUIDPrefix}, cmdTypes)
// re-enroll the device
require.NoError(t, mdmDevice.Enroll())
commands, err = s.ds.GetHostMDMCommands(context.Background(), hostID)
require.NoError(t, err)
require.Len(t, commands, 0)
triggerRefetchCron(hostID, 3)
// TODO: Do we care about manually triggered host refetch (where refetch_requested=true)?
}