ensure profiles and commands are delivered when MDM is turned on (#12580)

Related to #12482 and #12453, this cleans up Fleet tables that track
profile and bootstrap package status on re-enrollment.
This commit is contained in:
Roberto Dip 2023-06-30 12:30:49 -03:00 committed by GitHub
parent 4b139245cb
commit 5ddd940cb8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 150 additions and 35 deletions

1
changes/mdm-turn-on Normal file
View File

@ -0,0 +1 @@
* Make sure that all configuration profiles and commands are sent to devices if MDM is turned on, even if the device never turned off MDM.

View File

@ -418,6 +418,7 @@ INNER JOIN
ON
nvq.id = h.uuid
WHERE
nvq.active = 1 AND
%s
`, ds.whereFilterHostsByTeams(tmFilter, "h"))
stmt, params := appendListOptionsWithCursorToSQL(stmt, nil, &listOpts.ListOptions)
@ -2173,7 +2174,7 @@ func (ds *Datastore) InsertMDMAppleBootstrapPackage(ctx context.Context, bp *fle
if isDuplicate(err) {
return ctxerr.Wrap(ctx, alreadyExists("BootstrapPackage", fmt.Sprintf("for team %d", bp.TeamID)))
}
return ctxerr.Wrap(ctx, err, "create bootstrap pacckage")
return ctxerr.Wrap(ctx, err, "create bootstrap package")
}
return nil
@ -2626,14 +2627,31 @@ func (ds *Datastore) GetMDMAppleDefaultSetupAssistant(ctx context.Context, teamI
return asst.ProfileUUID, asst.UploadedAt, nil
}
func (ds *Datastore) ResetMDMAppleNanoEnrollment(ctx context.Context, hostUUID string) error {
// it's okay if we didn't update any rows, `nano_enrollments` entries
// are created on `TokenUpdate`, and this function is called on
// `Authenticate` to make sure we start on a clean state if a host is
// re-enrolling.
_, err := ds.writer(ctx).ExecContext(ctx, `UPDATE nano_enrollments SET token_update_tally = 0 WHERE id = ?`, hostUUID)
if err != nil {
return ctxerr.Wrap(ctx, err, "updating nano_enrollments")
}
return nil
func (ds *Datastore) ResetMDMAppleEnrollment(ctx context.Context, hostUUID string) error {
return ds.withRetryTxx(ctx, func(tx sqlx.ExtContext) error {
// it's okay if we didn't update any rows, `nano_enrollments` entries
// are created on `TokenUpdate`, and this function is called on
// `Authenticate` to make sure we start on a clean state if a host is
// re-enrolling.
_, err := tx.ExecContext(ctx, `UPDATE nano_enrollments SET token_update_tally = 0 WHERE id = ?`, hostUUID)
if err != nil {
return ctxerr.Wrap(ctx, err, "resetting nano_enrollments")
}
// Deleting profiles from this table will cause all profiles to
// be re-delivered on the next cron run.
if err := ds.deleteMDMAppleProfilesForHost(ctx, tx, hostUUID); err != nil {
return ctxerr.Wrap(ctx, err, "resetting profiles status")
}
// Deleting the matching entry on this table will cause
// the aggregate report to show this host as 'pending' to
// install the bootstrap package.
_, err = tx.ExecContext(ctx, `DELETE FROM host_mdm_apple_bootstrap_packages WHERE host_uuid = ?`, hostUUID)
if err != nil {
return ctxerr.Wrap(ctx, err, "resetting host_mdm_apple_bootstrap_packages")
}
return nil
})
}

View File

@ -63,7 +63,7 @@ func TestMDMApple(t *testing.T) {
{"TestSetVerifiedMacOSProfiles", testSetVerifiedMacOSProfiles},
{"TestMDMAppleConfigProfileHash", testMDMAppleConfigProfileHash},
{"TestMatchMDMAppleConfigProfiles", testMatchMDMAppleConfigProfiles},
{"TestResetMDMAppleNanoEnrollment", testResetMDMAppleNanoEnrollment},
{"TestResetMDMAppleEnrollment", testResetMDMAppleEnrollment},
{"TestMDMAppleDeleteHostDEPAssignments", testMDMAppleDeleteHostDEPAssignments},
}
@ -3438,6 +3438,17 @@ func testListMDMAppleCommands(t *testing.T, ds *Datastore) {
TeamID: &tm1.ID,
},
})
// randomly set two commadns as inactive
ExecAdhocSQL(t, ds, func(tx sqlx.ExtContext) error {
_, err := tx.ExecContext(ctx, `UPDATE nano_enrollment_queue SET active = 0 LIMIT 2`)
return err
})
// only two results are listed
res, err = ds.ListMDMAppleCommands(ctx, fleet.TeamFilter{User: test.UserAdmin}, &fleet.MDMAppleCommandListOptions{})
require.NoError(t, err)
require.Len(t, res, 2)
}
func testMDMAppleEULA(t *testing.T, ds *Datastore) {
@ -4430,7 +4441,7 @@ func testMatchMDMAppleConfigProfiles(t *testing.T, ds *Datastore) {
}
}
func testResetMDMAppleNanoEnrollment(t *testing.T, ds *Datastore) {
func testResetMDMAppleEnrollment(t *testing.T, ds *Datastore) {
ctx := context.Background()
host, err := ds.NewHost(ctx, &fleet.Host{
Hostname: "test-host1-name",
@ -4444,22 +4455,72 @@ func testResetMDMAppleNanoEnrollment(t *testing.T, ds *Datastore) {
// try with a host that doesn't have a matching entry
// in nano_enrollments
err = ds.ResetMDMAppleNanoEnrollment(ctx, host.UUID)
err = ds.ResetMDMAppleEnrollment(ctx, host.UUID)
require.NoError(t, err)
// add a matching entry
// add a matching entry in the nano table
nanoEnroll(t, ds, host, false)
enrollment, err := ds.GetNanoMDMEnrollment(ctx, host.UUID)
require.NoError(t, err)
require.Equal(t, enrollment.TokenUpdateTally, 1)
err = ds.ResetMDMAppleNanoEnrollment(ctx, host.UUID)
// add configuration profiles
cp, err := ds.NewMDMAppleConfigProfile(ctx, *generateCP("name0", "identifier0", 0))
require.NoError(t, err)
upsertHostCPs([]*fleet.Host{host}, []*fleet.MDMAppleConfigProfile{cp}, fleet.MDMAppleOperationTypeInstall, &fleet.MDMAppleDeliveryVerified, ctx, ds, t)
gotProfs, err := ds.GetHostMDMProfiles(ctx, host.UUID)
require.NoError(t, err)
require.Len(t, gotProfs, 1)
// add a record of the bootstrap package being installed
_, err = ds.writer(ctx).Exec(`
INSERT INTO nano_commands (command_uuid, request_type, command)
VALUES ('command-uuid', 'foo', '<?xml')
`)
require.NoError(t, err)
_, err = ds.writer(ctx).Exec(`
INSERT INTO nano_command_results (id, command_uuid, status, result)
VALUES (?, 'command-uuid', 'Acknowledged', '<?xml')
`, host.UUID)
require.NoError(t, err)
err = ds.InsertMDMAppleBootstrapPackage(ctx, &fleet.MDMAppleBootstrapPackage{
TeamID: uint(0),
Name: t.Name(),
Sha256: sha256.New().Sum(nil),
Bytes: []byte("content"),
Token: uuid.New().String(),
})
require.NoError(t, err)
err = ds.RecordHostBootstrapPackage(ctx, "command-uuid", host.UUID)
require.NoError(t, err)
err = ds.SetOrUpdateMDMData(context.Background(), host.ID, false, true, "foo.mdm.example.com", true, "")
require.NoError(t, err)
sum, err := ds.GetMDMAppleBootstrapPackageSummary(ctx, uint(0))
require.NoError(t, err)
require.Zero(t, sum.Failed)
require.Zero(t, sum.Pending)
require.EqualValues(t, 1, sum.Installed)
// reset the enrollment
err = ds.ResetMDMAppleEnrollment(ctx, host.UUID)
require.NoError(t, err)
enrollment, err = ds.GetNanoMDMEnrollment(ctx, host.UUID)
require.NoError(t, err)
require.Zero(t, enrollment.TokenUpdateTally)
gotProfs, err = ds.GetHostMDMProfiles(ctx, host.UUID)
require.NoError(t, err)
require.Empty(t, gotProfs)
sum, err = ds.GetMDMAppleBootstrapPackageSummary(ctx, uint(0))
require.NoError(t, err)
require.Zero(t, sum.Failed)
require.Zero(t, sum.Installed)
require.EqualValues(t, 1, sum.Pending)
}
func testMDMAppleDeleteHostDEPAssignments(t *testing.T, ds *Datastore) {

View File

@ -345,7 +345,8 @@ var hostRefs = []string{
// the host.uuid is not always named the same, so the map key is the table name
// and the map value is the column name to match to the host.uuid.
var additionalHostRefsByUUID = map[string]string{
"host_mdm_apple_profiles": "host_uuid",
"host_mdm_apple_profiles": "host_uuid",
"host_mdm_apple_bootstrap_packages": "host_uuid",
}
func (ds *Datastore) DeleteHost(ctx context.Context, hid uint) error {

View File

@ -2,6 +2,7 @@ package mysql
import (
"context"
"crypto/sha256"
"database/sql"
"encoding/json"
"errors"
@ -5773,6 +5774,22 @@ func testHostsDeleteHosts(t *testing.T, ds *Datastore) {
_, err = ds.writer(context.Background()).Exec(`INSERT INTO host_dep_assignments (host_id) VALUES (?)`, host.ID)
require.NoError(t, err)
_, err = ds.writer(context.Background()).Exec(`
INSERT INTO nano_commands (command_uuid, request_type, command)
VALUES ('command-uuid', 'foo', '<?xml')
`)
require.NoError(t, err)
err = ds.InsertMDMAppleBootstrapPackage(context.Background(), &fleet.MDMAppleBootstrapPackage{
TeamID: uint(0),
Name: t.Name(),
Sha256: sha256.New().Sum(nil),
Bytes: []byte("content"),
Token: uuid.New().String(),
})
require.NoError(t, err)
err = ds.RecordHostBootstrapPackage(context.Background(), "command-uuid", host.UUID)
require.NoError(t, err)
// Check there's an entry for the host in all the associated tables.
for _, hostRef := range hostRefs {
var ok bool

View File

@ -852,10 +852,9 @@ type Datastore interface {
// not already enrolled in Fleet.
IngestMDMAppleDeviceFromCheckin(ctx context.Context, mdmHost MDMAppleHostDetails) error
// ResetMDMAppleNanoEnrollment resets the
// `nano_enrollments.token_update_tally` if a matching row for the host
// exists.
ResetMDMAppleNanoEnrollment(ctx context.Context, hostUUID string) error
// ResetMDMAppleEnrollment resets all tables with enrollment-related
// information if a matching row for the host exists.
ResetMDMAppleEnrollment(ctx context.Context, hostUUID string) error
// ListMDMAppleDEPSerialsInTeam returns a list of serial numbers of hosts
// that are enrolled or pending enrollment in Fleet's MDM via DEP for the

View File

@ -574,7 +574,7 @@ type IngestMDMAppleDevicesFromDEPSyncFunc func(ctx context.Context, devices []go
type IngestMDMAppleDeviceFromCheckinFunc func(ctx context.Context, mdmHost fleet.MDMAppleHostDetails) error
type ResetMDMAppleNanoEnrollmentFunc func(ctx context.Context, hostUUID string) error
type ResetMDMAppleEnrollmentFunc func(ctx context.Context, hostUUID string) error
type ListMDMAppleDEPSerialsInTeamFunc func(ctx context.Context, teamID *uint) ([]string, error)
@ -1489,8 +1489,8 @@ type DataStore struct {
IngestMDMAppleDeviceFromCheckinFunc IngestMDMAppleDeviceFromCheckinFunc
IngestMDMAppleDeviceFromCheckinFuncInvoked bool
ResetMDMAppleNanoEnrollmentFunc ResetMDMAppleNanoEnrollmentFunc
ResetMDMAppleNanoEnrollmentFuncInvoked bool
ResetMDMAppleEnrollmentFunc ResetMDMAppleEnrollmentFunc
ResetMDMAppleEnrollmentFuncInvoked bool
ListMDMAppleDEPSerialsInTeamFunc ListMDMAppleDEPSerialsInTeamFunc
ListMDMAppleDEPSerialsInTeamFuncInvoked bool
@ -3558,11 +3558,11 @@ func (s *DataStore) IngestMDMAppleDeviceFromCheckin(ctx context.Context, mdmHost
return s.IngestMDMAppleDeviceFromCheckinFunc(ctx, mdmHost)
}
func (s *DataStore) ResetMDMAppleNanoEnrollment(ctx context.Context, hostUUID string) error {
func (s *DataStore) ResetMDMAppleEnrollment(ctx context.Context, hostUUID string) error {
s.mu.Lock()
s.ResetMDMAppleNanoEnrollmentFuncInvoked = true
s.ResetMDMAppleEnrollmentFuncInvoked = true
s.mu.Unlock()
return s.ResetMDMAppleNanoEnrollmentFunc(ctx, hostUUID)
return s.ResetMDMAppleEnrollmentFunc(ctx, hostUUID)
}
func (s *DataStore) ListMDMAppleDEPSerialsInTeam(ctx context.Context, teamID *uint) ([]string, error) {

View File

@ -2200,7 +2200,7 @@ func (svc *MDMAppleCheckinAndCommandService) Authenticate(r *mdm.Request, m *mdm
if err := svc.ds.IngestMDMAppleDeviceFromCheckin(r.Context, host); err != nil {
return ctxerr.Wrap(r.Context, err, "ingesting device in Authenticate message")
}
if err := svc.ds.ResetMDMAppleNanoEnrollment(r.Context, host.UDID); err != nil {
if err := svc.ds.ResetMDMAppleEnrollment(r.Context, host.UDID); err != nil {
return ctxerr.Wrap(r.Context, err, "resetting nano enrollment info in Authenticate message")
}
info, err := svc.ds.GetHostMDMCheckinInfo(r.Context, m.Enrollment.UDID)

View File

@ -975,7 +975,7 @@ func TestMDMAuthenticate(t *testing.T) {
return nil
}
ds.ResetMDMAppleNanoEnrollmentFunc = func(ctx context.Context, hostUUID string) error {
ds.ResetMDMAppleEnrollmentFunc = func(ctx context.Context, hostUUID string) error {
require.Equal(t, uuid, hostUUID)
return nil
}
@ -994,7 +994,7 @@ func TestMDMAuthenticate(t *testing.T) {
require.True(t, ds.IngestMDMAppleDeviceFromCheckinFuncInvoked)
require.True(t, ds.GetHostMDMCheckinInfoFuncInvoked)
require.True(t, ds.NewActivityFuncInvoked)
require.True(t, ds.ResetMDMAppleNanoEnrollmentFuncInvoked)
require.True(t, ds.ResetMDMAppleEnrollmentFuncInvoked)
}
func TestMDMTokenUpdate(t *testing.T) {

View File

@ -790,11 +790,21 @@ func (s *integrationMDMTestSuite) TestDEPProfileAssignment() {
<-ch
}
// add global profiles
globalProfile := mobileconfigForTest("N1", "I1")
s.Do("POST", "/api/v1/fleet/mdm/apple/profiles/batch", batchSetMDMAppleProfilesRequest{Profiles: [][]byte{globalProfile}}, http.StatusNoContent)
checkPostEnrollmentCommands := func(mdmDevice *mdmtest.TestMDMClient, shouldReceive bool) {
// run the worker to process the DEP enroll request
s.runWorker()
// run the worker to assign configuration profiles
ch := make(chan bool)
s.onProfileScheduleDone = func() { close(ch) }
_, err := s.profileSchedule.Trigger()
require.NoError(t, err)
<-ch
var fleetdCmd *micromdm.CommandPayload
var fleetdCmd, installProfileCmd *micromdm.CommandPayload
cmd, err := mdmDevice.Idle()
require.NoError(t, err)
for cmd != nil {
@ -802,16 +812,24 @@ func (s *integrationMDMTestSuite) TestDEPProfileAssignment() {
cmd.Command.InstallEnterpriseApplication.ManifestURL != nil &&
strings.Contains(*cmd.Command.InstallEnterpriseApplication.ManifestURL, apple_mdm.FleetdPublicManifestURL) {
fleetdCmd = cmd
} else if cmd.Command.RequestType == "InstallProfile" {
installProfileCmd = cmd
}
cmd, err = mdmDevice.Acknowledge(cmd.CommandUUID)
require.NoError(t, err)
}
if shouldReceive {
require.NotNil(t, fleetdCmd)
require.NotNil(t, fleetdCmd.Command)
// received request to install fleetd
require.NotNil(t, fleetdCmd, "host didn't get a command to install fleetd")
require.NotNil(t, fleetdCmd.Command, "host didn't get a command to install fleetd")
// received request to install the global configuration profile
require.NotNil(t, installProfileCmd, "host didn't get a command to install profiles")
require.NotNil(t, installProfileCmd.Command, "host didn't get a command to install profiles")
} else {
require.Nil(t, fleetdCmd)
require.Nil(t, fleetdCmd, "host got a command to install fleetd")
require.Nil(t, installProfileCmd, "host got a command to install profiles")
}
}
@ -887,7 +905,7 @@ func (s *integrationMDMTestSuite) TestDEPProfileAssignment() {
err := mdmDevice.Enroll()
require.NoError(t, err)
// make sure the host gets a request to install fleetd
// make sure the host gets post enrollment requests
checkPostEnrollmentCommands(mdmDevice, true)
// only one shows up as pending