From f3d400d48eccb4f67ed92adc6dff9a539955a660 Mon Sep 17 00:00:00 2001 From: Roberto Dip Date: Wed, 3 Jan 2024 15:16:59 -0300 Subject: [PATCH] automatically install fleetd for hosts that turn MDM manually (#15883) for #15057 --- changes/manual-enrollment | 1 + server/datastore/mysql/hosts.go | 3 +- server/datastore/mysql/hosts_test.go | 12 +++++++ server/fleet/hosts.go | 1 + server/service/apple_mdm.go | 25 +++++++++++--- server/service/integration_mdm_test.go | 45 ++++++++++++++++++++++++++ server/worker/apple_mdm.go | 30 ++++++++++++++--- server/worker/apple_mdm_test.go | 30 +++++++++++++++++ 8 files changed, 136 insertions(+), 11 deletions(-) create mode 100644 changes/manual-enrollment diff --git a/changes/manual-enrollment b/changes/manual-enrollment new file mode 100644 index 0000000000..539695db5e --- /dev/null +++ b/changes/manual-enrollment @@ -0,0 +1 @@ +* fleetd is now automatically installed on Apple hosts turning on MDM features manually if it wasn't installed before. diff --git a/server/datastore/mysql/hosts.go b/server/datastore/mysql/hosts.go index 6c78a794ee..42ddc84660 100644 --- a/server/datastore/mysql/hosts.go +++ b/server/datastore/mysql/hosts.go @@ -3626,7 +3626,8 @@ func (ds *Datastore) GetHostMDMCheckinInfo(ctx context.Context, hostUUID string) COALESCE(hm.installed_from_dep, false) as installed_from_dep, hd.display_name, COALESCE(h.team_id, 0) as team_id, - hda.host_id IS NOT NULL AND hda.deleted_at IS NULL as dep_assigned_to_fleet + hda.host_id IS NOT NULL AND hda.deleted_at IS NULL as dep_assigned_to_fleet, + h.node_key IS NOT NULL as osquery_enrolled FROM hosts h LEFT JOIN diff --git a/server/datastore/mysql/hosts_test.go b/server/datastore/mysql/hosts_test.go index 44b15e0e3c..453944ff34 100644 --- a/server/datastore/mysql/hosts_test.go +++ b/server/datastore/mysql/hosts_test.go @@ -6997,18 +6997,30 @@ func testHostsGetHostMDMCheckinInfo(t *testing.T, ds *Datastore) { require.Equal(t, true, info.InstalledFromDEP) require.EqualValues(t, tm.ID, info.TeamID) require.False(t, info.DEPAssignedToFleet) + require.True(t, info.OsqueryEnrolled) err = ds.UpsertMDMAppleHostDEPAssignments(ctx, []fleet.Host{*host}) require.NoError(t, err) info, err = ds.GetHostMDMCheckinInfo(ctx, host.UUID) require.NoError(t, err) require.True(t, info.DEPAssignedToFleet) + require.True(t, info.OsqueryEnrolled) err = ds.DeleteHostDEPAssignments(ctx, []string{host.HardwareSerial}) require.NoError(t, err) info, err = ds.GetHostMDMCheckinInfo(ctx, host.UUID) require.NoError(t, err) require.False(t, info.DEPAssignedToFleet) + require.True(t, info.OsqueryEnrolled) + + // host with an empty node key + ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error { + _, err := q.ExecContext(ctx, `UPDATE hosts SET node_key = NULL WHERE uuid = ?`, host.UUID) + return err + }) + info, err = ds.GetHostMDMCheckinInfo(ctx, host.UUID) + require.NoError(t, err) + require.False(t, info.OsqueryEnrolled) } func testHostsLoadHostByOrbitNodeKey(t *testing.T, ds *Datastore) { diff --git a/server/fleet/hosts.go b/server/fleet/hosts.go index ae29cad362..56a110f204 100644 --- a/server/fleet/hosts.go +++ b/server/fleet/hosts.go @@ -1152,6 +1152,7 @@ type HostMDMCheckinInfo struct { DisplayName string `json:"display_name" db:"display_name"` TeamID uint `json:"team_id" db:"team_id"` DEPAssignedToFleet bool `json:"dep_assigned_to_fleet" db:"dep_assigned_to_fleet"` + OsqueryEnrolled bool `json:"osquery_enrolled" db:"osquery_enrolled"` } type HostDiskEncryptionKey struct { diff --git a/server/service/apple_mdm.go b/server/service/apple_mdm.go index 1b52d129a5..1e716040a8 100644 --- a/server/service/apple_mdm.go +++ b/server/service/apple_mdm.go @@ -2233,15 +2233,15 @@ func (svc *MDMAppleCheckinAndCommandService) TokenUpdate(r *mdm.Request, m *mdm. return err } + var tmID *uint + if info.TeamID != 0 { + tmID = &info.TeamID + } + // TODO: improve this to not enqueue the job if a host that is // assigned in ABM is manually enrolling for some reason. if info.DEPAssignedToFleet || info.InstalledFromDEP { svc.logger.Log("info", "queueing post-enroll task for newly enrolled DEP device", "host_uuid", r.ID) - - var tmID *uint - if info.TeamID != 0 { - tmID = &info.TeamID - } if err := worker.QueueAppleMDMJob( r.Context, svc.ds, @@ -2254,6 +2254,21 @@ func (svc *MDMAppleCheckinAndCommandService) TokenUpdate(r *mdm.Request, m *mdm. return ctxerr.Wrap(r.Context, err, "queue DEP post-enroll task") } } + + // manual MDM enrollments that are not fleet-enrolled yet + if !info.InstalledFromDEP && !info.OsqueryEnrolled { + if err := worker.QueueAppleMDMJob( + r.Context, + svc.ds, + svc.logger, + worker.AppleMDMPostManualEnrollmentTask, + r.ID, + tmID, + r.Params[mobileconfig.FleetEnrollReferenceKey], + ); err != nil { + return ctxerr.Wrap(r.Context, err, "queue manual post-enroll task") + } + } } return nil } diff --git a/server/service/integration_mdm_test.go b/server/service/integration_mdm_test.go index 848905087c..cee78ad388 100644 --- a/server/service/integration_mdm_test.go +++ b/server/service/integration_mdm_test.go @@ -10323,3 +10323,48 @@ func (s *integrationMDMTestSuite) TestWindowsFreshEnrollEmptyQuery() { require.Contains(t, dqResp.Queries, "fleet_detail_query_mdm_config_profiles_windows") require.NotEmpty(t, dqResp.Queries, "fleet_detail_query_mdm_config_profiles_windows") } + +func (s *integrationMDMTestSuite) TestManualEnrollmentCommands() { + t := s.T() + + checkInstallFleetdCommandSent := func(mdmDevice *mdmtest.TestAppleMDMClient, wantCommand bool) { + foundInstallFleetdCommand := false + cmd, err := mdmDevice.Idle() + require.NoError(t, err) + for cmd != nil { + if manifest := cmd.Command.InstallEnterpriseApplication.ManifestURL; manifest != nil { + foundInstallFleetdCommand = true + require.Equal(t, "InstallEnterpriseApplication", cmd.Command.RequestType) + require.Contains(t, *cmd.Command.InstallEnterpriseApplication.ManifestURL, apple_mdm.FleetdPublicManifestURL) + } + cmd, err = mdmDevice.Acknowledge(cmd.CommandUUID) + require.NoError(t, err) + } + require.Equal(t, wantCommand, foundInstallFleetdCommand) + } + + // create a device that's not enrolled into Fleet, it should get a command to + // install fleetd + mdmDevice := mdmtest.NewTestMDMClientAppleDirect(mdmtest.AppleEnrollInfo{ + SCEPChallenge: s.fleetCfg.MDM.AppleSCEPChallenge, + SCEPURL: s.server.URL + apple_mdm.SCEPPath, + MDMURL: s.server.URL + apple_mdm.MDMPath, + }) + err := mdmDevice.Enroll() + require.NoError(t, err) + s.runWorker() + checkInstallFleetdCommandSent(mdmDevice, true) + + // create a device that's enrolled into Fleet before turning on MDM features, + // it shouldn't get the command to install fleetd + desktopToken := uuid.New().String() + host := createOrbitEnrolledHost(t, "darwin", "h1", s.ds) + err = s.ds.SetOrUpdateDeviceAuthToken(context.Background(), host.ID, desktopToken) + require.NoError(t, err) + mdmDevice = mdmtest.NewTestMDMClientAppleDesktopManual(s.server.URL, desktopToken) + mdmDevice.UUID = host.UUID + err = mdmDevice.Enroll() + require.NoError(t, err) + s.runWorker() + checkInstallFleetdCommandSent(mdmDevice, false) +} diff --git a/server/worker/apple_mdm.go b/server/worker/apple_mdm.go index 48ea8e007f..0939d69ab9 100644 --- a/server/worker/apple_mdm.go +++ b/server/worker/apple_mdm.go @@ -23,7 +23,8 @@ type AppleMDMTask string // List of supported tasks. const ( - AppleMDMPostDEPEnrollmentTask AppleMDMTask = "post_dep_enrollment" + AppleMDMPostDEPEnrollmentTask AppleMDMTask = "post_dep_enrollment" + AppleMDMPostManualEnrollmentTask AppleMDMTask = "post_manual_enrollment" ) // AppleMDM is the job processor for the apple_mdm job. @@ -61,14 +62,30 @@ func (a *AppleMDM) Run(ctx context.Context, argsJSON json.RawMessage) error { switch args.Task { case AppleMDMPostDEPEnrollmentTask: - return a.runPostDEPEnrollment(ctx, args) + err := a.runPostDEPEnrollment(ctx, args) + return ctxerr.Wrap(ctx, err, "running post Apple DEP enrollment task") + case AppleMDMPostManualEnrollmentTask: + err := a.runPostManualEnrollment(ctx, args) + return ctxerr.Wrap(ctx, err, "running post Apple manual enrollment task") default: return ctxerr.Errorf(ctx, "unknown task: %v", args.Task) } } +func (a *AppleMDM) runPostManualEnrollment(ctx context.Context, args appleMDMArgs) error { + if err := a.installFleetd(ctx, args.HostUUID); err != nil { + return ctxerr.Wrap(ctx, err, "installing post-enrollment packages") + } + + return nil +} + func (a *AppleMDM) runPostDEPEnrollment(ctx context.Context, args appleMDMArgs) error { - if err := a.installEnrollmentPackages(ctx, args.HostUUID, args.TeamID); err != nil { + if err := a.installFleetd(ctx, args.HostUUID); err != nil { + return ctxerr.Wrap(ctx, err, "installing post-enrollment packages") + } + + if err := a.installBootstrapPackage(ctx, args.HostUUID, args.TeamID); err != nil { return ctxerr.Wrap(ctx, err, "installing post-enrollment packages") } @@ -109,13 +126,16 @@ func (a *AppleMDM) runPostDEPEnrollment(ctx context.Context, args appleMDMArgs) return nil } -func (a *AppleMDM) installEnrollmentPackages(ctx context.Context, hostUUID string, teamID *uint) error { +func (a *AppleMDM) installFleetd(ctx context.Context, hostUUID string) error { cmdUUID := uuid.New().String() if err := a.Commander.InstallEnterpriseApplication(ctx, []string{hostUUID}, cmdUUID, apple_mdm.FleetdPublicManifestURL); err != nil { return err } a.Log.Log("info", "sent command to install fleetd", "host_uuid", hostUUID) + return nil +} +func (a *AppleMDM) installBootstrapPackage(ctx context.Context, hostUUID string, teamID *uint) error { // GetMDMAppleBootstrapPackageMeta expects team id 0 for no team var tmID uint if teamID != nil { @@ -143,7 +163,7 @@ func (a *AppleMDM) installEnrollmentPackages(ctx context.Context, hostUUID strin } manifest := appmanifest.NewFromSha(meta.Sha256, url) - cmdUUID = uuid.New().String() + cmdUUID := uuid.New().String() err = a.Commander.InstallEnterpriseApplicationWithEmbeddedManifest(ctx, []string{hostUUID}, cmdUUID, manifest) if err != nil { return err diff --git a/server/worker/apple_mdm_test.go b/server/worker/apple_mdm_test.go index 4a5edf8dde..50e354d7cd 100644 --- a/server/worker/apple_mdm_test.go +++ b/server/worker/apple_mdm_test.go @@ -388,4 +388,34 @@ func TestAppleMDM(t *testing.T) { require.Empty(t, jobs) require.ElementsMatch(t, []string{"InstallEnterpriseApplication", "AccountConfiguration"}, getEnqueuedCommandTypes(t)) }) + + t.Run("installs fleetd for manual enrollments", func(t *testing.T) { + defer mysql.TruncateTables(t, ds) + + h := createEnrolledHost(t, 1, nil, true) + + mdmWorker := &AppleMDM{ + Datastore: ds, + Log: nopLog, + Commander: apple_mdm.NewMDMAppleCommander(mdmStorage, mockPusher{}), + } + w := NewWorker(ds, nopLog) + w.Register(mdmWorker) + + err := QueueAppleMDMJob(ctx, ds, nopLog, AppleMDMPostManualEnrollmentTask, h.UUID, nil, "") + require.NoError(t, err) + + // run the worker, should succeed + err = w.ProcessJobs(ctx) + require.NoError(t, err) + + // ensure the job's not_before allows it to be returned if it were to run + // again + time.Sleep(time.Second) + + jobs, err := ds.GetQueuedJobs(ctx, 1) + require.NoError(t, err) + require.Empty(t, jobs) + require.ElementsMatch(t, []string{"InstallEnterpriseApplication"}, getEnqueuedCommandTypes(t)) + }) }