From 03dd4721b6ff5d64bbf551c341e4466d22a96b9d Mon Sep 17 00:00:00 2001 From: Roberto Dip Date: Thu, 20 Jun 2024 16:35:23 -0300 Subject: [PATCH] proper fix for renew enrollment profile requests (#19909) # Checklist for submitter - [x] Added/updated tests - [x] Manual QA for all new/changed functionality --- server/fleet/hosts.go | 8 -- server/service/integration_mdm_test.go | 148 +++++++++++++++++++++++++ 2 files changed, 148 insertions(+), 8 deletions(-) diff --git a/server/fleet/hosts.go b/server/fleet/hosts.go index 013b8d0f35..e21084b5d6 100644 --- a/server/fleet/hosts.go +++ b/server/fleet/hosts.go @@ -708,14 +708,6 @@ func (h *Host) IsEligibleForDEPMigration(isConnectedToFleetMDM bool) bool { func (h *Host) NeedsDEPEnrollment(isConnectedToFleetMDM bool) bool { return h.MDMInfo != nil && !h.MDMInfo.Enrolled && - // as a special case for migration with user interaction, we - // also check the information stored in host_mdm, and assume - // the host needs migration if it's not Fleet - // - // this is because we can't always rely on nano setting - // `nano_enrollment.active = 1` since sometimes Fleet won't get - // the checkout message from the host. - (!isConnectedToFleetMDM || h.MDMInfo.Name != WellKnownMDMFleet) && h.IsDEPAssignedToFleet() } diff --git a/server/service/integration_mdm_test.go b/server/service/integration_mdm_test.go index 720397a494..b8149a546f 100644 --- a/server/service/integration_mdm_test.go +++ b/server/service/integration_mdm_test.go @@ -8927,3 +8927,151 @@ func (s *integrationMDMTestSuite) uploadABMToken(encryptedToken []byte, expected assert.Contains(t, errMsg, wantErr) } } + +func (s *integrationMDMTestSuite) TestSilentMigrationGotchas() { + t := s.T() + ctx := context.Background() + + host := createOrbitEnrolledHost(t, "darwin", "h1", s.ds) + // set the host as enrolled in a third-party MDM + err := s.ds.SetOrUpdateMDMData(ctx, host.ID, true, true, "https://foo.com", false, fleet.WellKnownMDMSimpleMDM, "") + require.NoError(t, err) + + var hostResp getHostResponse + s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d", host.ID), nil, http.StatusOK, &hostResp) + require.NotNil(t, hostResp.Host) + require.NotNil(t, hostResp.Host.MDM.ConnectedToFleet) + require.False(t, *hostResp.Host.MDM.ConnectedToFleet) + + // simulate that the device is assigned to Fleet in ABM + s.mockDEPResponse(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + switch r.URL.Path { + case "/session": + _, _ = w.Write([]byte(`{"auth_session_token": "xyz"}`)) + case "/profile": + encoder := json.NewEncoder(w) + err := encoder.Encode(godep.ProfileResponse{ProfileUUID: "abc"}) + require.NoError(t, err) + case "/server/devices", "/devices/sync": + encoder := json.NewEncoder(w) + err := encoder.Encode(godep.DeviceResponse{ + Devices: []godep.Device{ + { + SerialNumber: host.HardwareSerial, + Model: "Mac Mini", + OS: "osx", + OpType: "added", + }, + }, + }) + require.NoError(t, err) + + case "/profile/devices": + b, err := io.ReadAll(r.Body) + require.NoError(t, err) + var prof profileAssignmentReq + require.NoError(t, json.Unmarshal(b, &prof)) + var resp godep.ProfileResponse + resp.ProfileUUID = prof.ProfileUUID + resp.Devices = map[string]string{ + prof.Devices[0]: string(fleet.DEPAssignProfileResponseSuccess), + } + encoder := json.NewEncoder(w) + err = encoder.Encode(resp) + require.NoError(t, err) + } + })) + s.runDEPSchedule() + + // enable migrations + acResp := appConfigResponse{} + s.DoJSON("PATCH", "/api/v1/fleet/config", json.RawMessage(`{ + "mdm": { "macos_migration": { "enable": true, "mode": "voluntary", "webhook_url": "https://example.com" } } + }`), http.StatusOK, &acResp) + + // orbit config asks for a migration but not to renew enrollment profile + resp := orbitGetConfigResponse{} + s.DoJSON("POST", "/api/fleet/orbit/config", json.RawMessage(fmt.Sprintf(`{"orbit_node_key": %q}`, *host.OrbitNodeKey)), http.StatusOK, &resp) + require.False(t, resp.Notifications.RenewEnrollmentProfile) + require.True(t, resp.Notifications.NeedsMDMMigration) + + // simulate that's actually enrolled to Fleet under the hood + mdmDevice := mdmtest.NewTestMDMClientAppleDirect(mdmtest.AppleEnrollInfo{ + SCEPChallenge: s.scepChallenge, + SCEPURL: s.server.URL + apple_mdm.SCEPPath, + MDMURL: s.server.URL + apple_mdm.MDMPath, + }, "MacBookPro16,1") + err = mdmDevice.Enroll() + require.NoError(t, err) + + // host response says that's connected to Fleet + hostResp = getHostResponse{} + s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d", host.ID), nil, http.StatusOK, &hostResp) + require.NotNil(t, hostResp.Host) + require.NotNil(t, hostResp.Host.MDM.ConnectedToFleet) + require.False(t, *hostResp.Host.MDM.ConnectedToFleet) + + // orbit config asks for a migration because user migrations are enabled, but no ask to renew the enrollment profile. + resp = orbitGetConfigResponse{} + s.DoJSON("POST", "/api/fleet/orbit/config", json.RawMessage(fmt.Sprintf(`{"orbit_node_key": %q}`, *host.OrbitNodeKey)), http.StatusOK, &resp) + require.False(t, resp.Notifications.RenewEnrollmentProfile) + require.True(t, resp.Notifications.NeedsMDMMigration) + + // set an enroll secret so the fleetd profile is delivered + var applyResp applyEnrollSecretSpecResponse + s.DoJSON("POST", "/api/latest/fleet/spec/enroll_secret", applyEnrollSecretSpecRequest{ + Spec: &fleet.EnrollSecretSpec{ + Secrets: []*fleet.EnrollSecret{{Secret: t.Name()}}, + }, + }, http.StatusOK, &applyResp) + + // trigger the profile cron + s.awaitTriggerProfileSchedule(t) + + installs := [][]byte{} + cmd, err := mdmDevice.Idle() + require.NoError(t, err) + + for cmd != nil { + require.Equal(t, "InstallProfile", cmd.Command.RequestType) + installs = append(installs, cmd.Raw) + cmd, err = mdmDevice.Acknowledge(cmd.CommandUUID) + require.NoError(t, err) + } + + require.Len(t, installs, 2) + + // trigger the scep renewals cron + cert, key, err := generateCertWithAPNsTopic() + require.NoError(t, err) + fleetCfg := config.TestConfig() + config.SetTestMDMConfig(s.T(), &fleetCfg, cert, key, "") + logger := kitlog.NewJSONLogger(os.Stdout) + err = RenewSCEPCertificates(ctx, logger, s.ds, &fleetCfg, s.mdmCommander) + require.NoError(t, err) + + // no new commands were enqueued + cmd, err = mdmDevice.Idle() + require.NoError(t, err) + require.Nil(t, cmd) + + // set the host as completely unenrolled + err = s.ds.SetOrUpdateMDMData(ctx, host.ID, false, false, "", false, "", "") + require.NoError(t, err) + // orbit config asks to renew the enrollment profile, migration is not needed anymore so it's false + resp = orbitGetConfigResponse{} + s.DoJSON("POST", "/api/fleet/orbit/config", json.RawMessage(fmt.Sprintf(`{"orbit_node_key": %q}`, *host.OrbitNodeKey)), http.StatusOK, &resp) + require.True(t, resp.Notifications.RenewEnrollmentProfile) + require.False(t, resp.Notifications.NeedsMDMMigration) + + // with migrations disabled, it still asks to renew the enrollment profile + acResp = appConfigResponse{} + s.DoJSON("PATCH", "/api/v1/fleet/config", json.RawMessage(`{ + "mdm": { "macos_migration": { "enable": false } } + }`), http.StatusOK, &acResp) + resp = orbitGetConfigResponse{} + s.DoJSON("POST", "/api/fleet/orbit/config", json.RawMessage(fmt.Sprintf(`{"orbit_node_key": %q}`, *host.OrbitNodeKey)), http.StatusOK, &resp) + require.True(t, resp.Notifications.RenewEnrollmentProfile) + require.False(t, resp.Notifications.NeedsMDMMigration) +}