From d4237f8a4f47386255813b40fedcffeef3defda7 Mon Sep 17 00:00:00 2001 From: Jordan Montgomery Date: Fri, 27 Feb 2026 17:15:34 -0500 Subject: [PATCH] Remove "do not enqueue setup experience items >24 hours after enrollment" logic for macOS hosts (#40739) --- changes/40725-fix-macos-setup | 1 + server/datastore/mysql/setup_experience.go | 52 ++++---- .../datastore/mysql/setup_experience_test.go | 122 ++++++++++++++++-- 3 files changed, 136 insertions(+), 39 deletions(-) create mode 100644 changes/40725-fix-macos-setup diff --git a/changes/40725-fix-macos-setup b/changes/40725-fix-macos-setup new file mode 100644 index 0000000000..edb3d23dd9 --- /dev/null +++ b/changes/40725-fix-macos-setup @@ -0,0 +1 @@ +* Fixed a bug where macOS systems previous enrolled in fleet wouldn't always go through setup experience after a wipe diff --git a/server/datastore/mysql/setup_experience.go b/server/datastore/mysql/setup_experience.go index 2b15ec46b3..a2e5f71750 100644 --- a/server/datastore/mysql/setup_experience.go +++ b/server/datastore/mysql/setup_experience.go @@ -23,32 +23,34 @@ func (ds *Datastore) ResetSetupExperienceItemsAfterFailure(ctx context.Context, } func (ds *Datastore) enqueueSetupExperienceItems(ctx context.Context, hostPlatformLike string, hostUUID string, teamID uint, resetFailedSetupSteps bool) (bool, error) { - // Find the host with the given UUID and platform. If it's already been enrolled for > the cutoff, - // don't enqueue any items. This handles the edge case where an enrolled host upgrades from an - // Orbit version that didn't support setup experience to one that does. - // See https://github.com/fleetdm/fleet/issues/35717 - stmtHost := ` - SELECT - last_enrolled_at - FROM - hosts - WHERE uuid = ? AND platform = ? - ` - var lastEnrolledAt sql.NullTime - if err := sqlx.GetContext(ctx, ds.reader(ctx), &lastEnrolledAt, stmtHost, hostUUID, hostPlatformLike); err != nil { - if errors.Is(err, sql.ErrNoRows) { - // This shouldn't happen but we don't check for it elsewhere, - // so we'll log a warning and continue. - ds.logger.WarnContext(ctx, "Host not found while enqueueing setup experience items", "host_uuid", hostUUID, "platform_like", hostPlatformLike) - } else { - return false, ctxerr.Wrap(ctx, err, "finding host for enqueueing setup experience items") + if hostPlatformLike != "darwin" && hostPlatformLike != "ios" && hostPlatformLike != "ipados" { + // Find the host with the given UUID and platform. If it's already been enrolled for > the cutoff, + // don't enqueue any items. This handles the edge case where an enrolled host upgrades from an + // Orbit version that didn't support setup experience to one that does. + // See https://github.com/fleetdm/fleet/issues/35717 + stmtHost := ` + SELECT + last_enrolled_at + FROM + hosts + WHERE uuid = ? AND platform = ? + ` + var lastEnrolledAt sql.NullTime + if err := sqlx.GetContext(ctx, ds.reader(ctx), &lastEnrolledAt, stmtHost, hostUUID, hostPlatformLike); err != nil { + if errors.Is(err, sql.ErrNoRows) { + // This shouldn't happen but we don't check for it elsewhere, + // so we'll log a warning and continue. + ds.logger.WarnContext(ctx, "Host not found while enqueueing setup experience items", "host_uuid", hostUUID, "platform_like", hostPlatformLike) + } else { + return false, ctxerr.Wrap(ctx, err, "finding host for enqueueing setup experience items") + } + } + // If the host was enrolled more than 24 hours ago, don't enqueue any items. + // Note: if the last enroll date is our "zero date" (1/1/2000), treat it as if it's never enrolled. + if lastEnrolledAt.Valid && lastEnrolledAt.Time.Before(time.Now().Add(-24*time.Hour)) && lastEnrolledAt.Time.After(time.Date(2000, time.January, 1, 0, 0, 0, 0, time.UTC)) { + ds.logger.DebugContext(ctx, "Host enrolled more than 24 hours ago, skipping enqueueing setup experience items", "host_uuid", hostUUID, "platform_like", hostPlatformLike, "last_enrolled_at", lastEnrolledAt.Time) + return false, nil } - } - // If the host was enrolled more than 24 hours ago, don't enqueue any items. - // Note: if the last enroll date is our "zero date" (1/1/2000), treat it as if it's never enrolled. - if lastEnrolledAt.Valid && lastEnrolledAt.Time.Before(time.Now().Add(-24*time.Hour)) && lastEnrolledAt.Time.After(time.Date(2000, time.January, 1, 0, 0, 0, 0, time.UTC)) { - ds.logger.DebugContext(ctx, "Host enrolled more than 24 hours ago, skipping enqueueing setup experience items", "host_uuid", hostUUID, "platform_like", hostPlatformLike, "last_enrolled_at", lastEnrolledAt.Time) - return false, nil } // NOTE: currently, the Android platform does not use the "enqueue setup experience items" flow as it diff --git a/server/datastore/mysql/setup_experience_test.go b/server/datastore/mysql/setup_experience_test.go index 4712797ea9..7168a423c3 100644 --- a/server/datastore/mysql/setup_experience_test.go +++ b/server/datastore/mysql/setup_experience_test.go @@ -32,6 +32,7 @@ func TestSetupExperience(t *testing.T) { {"TestHostInSetupExperience", testHostInSetupExperience}, {"TestGetSetupExperienceScriptByID", testGetSetupExperienceScriptByID}, {"TestUpdateSetupExperienceScriptWhileEnqueued", testUpdateSetupExperienceScriptWhileEnqueued}, + {"TestEnqueueSetupExperienceItemsWindows", testEnqueueSetupExperienceItemsWindows}, } for _, c := range cases { @@ -230,6 +231,103 @@ func testEnqueueSetupExperienceLinuxScriptPackages(t *testing.T, ds *Datastore) }) } +func testEnqueueSetupExperienceItemsWindows(t *testing.T, ds *Datastore) { + ctx := context.Background() + team1, err := ds.NewTeam(ctx, &fleet.Team{Name: "team1"}) + require.NoError(t, err) + team2, err := ds.NewTeam(ctx, &fleet.Team{Name: "team2"}) + require.NoError(t, err) + + user1 := test.NewUser(t, ds, "Alice", "alice@example.com", true) + + // Create some software installers and add them to setup experience + tfr1, err := fleet.NewTempFileReader(strings.NewReader("hello"), t.TempDir) + require.NoError(t, err) + installerID1, _, err := ds.MatchOrCreateSoftwareInstaller(ctx, &fleet.UploadSoftwareInstallerPayload{ + InstallScript: "hello", + PreInstallQuery: "SELECT 1", + PostInstallScript: "world", + UninstallScript: "goodbye", + InstallerFile: tfr1, + StorageID: "storage1", + Filename: "file1", + Title: "Software1", + Version: "1.0", + Source: "apps", + UserID: user1.ID, + TeamID: &team1.ID, + Platform: "windows", + ValidatedLabels: &fleet.LabelIdentsWithScope{}, + }) + require.NoError(t, err) + + tfr2, err := fleet.NewTempFileReader(strings.NewReader("hello"), t.TempDir) + require.NoError(t, err) + installerID2, _, err := ds.MatchOrCreateSoftwareInstaller(ctx, &fleet.UploadSoftwareInstallerPayload{ + InstallScript: "banana", + PreInstallQuery: "SELECT 3", + PostInstallScript: "apple", + InstallerFile: tfr2, + StorageID: "storage3", + Filename: "file3", + Title: "Software2", + Version: "3.0", + Source: "apps", + SelfService: true, + UserID: user1.ID, + TeamID: &team2.ID, + Platform: "windows", + ValidatedLabels: &fleet.LabelIdentsWithScope{}, + }) + require.NoError(t, err) + + ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error { + _, err := q.ExecContext(ctx, "UPDATE software_installers SET install_during_setup = 1 WHERE id IN (?, ?)", installerID1, installerID2) + return err + }) + + host1UUID := "11111111-1111-1111-1111-111111111111" + host2UUID := "22222222-2222-2222-2222-222222222222" + + // Freshly enrolled host, should get items enqueued + _, err = ds.NewHost(ctx, &fleet.Host{ + Hostname: "windows-test-1", + OsqueryHostID: ptr.String("osquery-windows-1"), + NodeKey: ptr.String("node-key-windows-1"), + UUID: host1UUID, + Platform: "windows", + HardwareSerial: "654321a-1", + }) + require.NoError(t, err) + ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error { + _, err := q.ExecContext(ctx, "UPDATE hosts SET last_enrolled_at = ? WHERE uuid = ?", time.Now().Add(-1*time.Hour), host1UUID) + return err + }) + + // Enroll date > 24 hours ago and is windows. This should not get items enqueued. + _, err = ds.NewHost(ctx, &fleet.Host{ + Hostname: "windows-test-2", + OsqueryHostID: ptr.String("osquery-windows-2"), + NodeKey: ptr.String("node-key-windows-2"), + UUID: host2UUID, + Platform: "windows", + HardwareSerial: "654321b-2", + }) + require.NoError(t, err) + ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error { + _, err := q.ExecContext(ctx, "UPDATE hosts SET last_enrolled_at = ? WHERE uuid = ?", time.Now().Add(-25*time.Hour), host2UUID) + return err + }) + + anythingEnqueued, err := ds.EnqueueSetupExperienceItems(ctx, "windows", host1UUID, team1.ID) + require.NoError(t, err) + require.True(t, anythingEnqueued) + + anythingEnqueued, err = ds.EnqueueSetupExperienceItems(ctx, "windows", host2UUID, team2.ID) + require.NoError(t, err) + require.False(t, anythingEnqueued) +} + func testEnqueueSetupExperienceItems(t *testing.T, ds *Datastore) { ctx := context.Background() test.CreateInsertGlobalVPPToken(t, ds) @@ -352,7 +450,7 @@ func testEnqueueSetupExperienceItems(t *testing.T, ds *Datastore) { // Deliberately not adding a record for the hostTeam2Missing, to verify that // we still enqueue items for it if it doesn't exist in the database. - // Enroll date > 24 hours ago. This should NOT have items enqueued. + // Enroll date > 24 hours ago but is macOS. This should get items enqueued. _, err = ds.NewHost(ctx, &fleet.Host{ Hostname: "macos-test-4", OsqueryHostID: ptr.String("osquery-macos-4"), @@ -422,12 +520,11 @@ func testEnqueueSetupExperienceItems(t *testing.T, ds *Datastore) { anythingEnqueued, err = ds.EnqueueSetupExperienceItems(ctx, "darwin", hostTeam1Old, team1.ID) require.NoError(t, err) - require.False(t, anythingEnqueued) - // This host enrolled > 24 hours ago, so we shouldn't enqueue any items for it. + require.True(t, anythingEnqueued) + // This host enrolled > 24 hours ago, but it's darwin, so we should enqueue items for it. awaitingConfig, err = ds.GetHostAwaitingConfiguration(ctx, hostTeam1Old) - require.Error(t, err) - require.True(t, fleet.IsNotFound(err)) - require.False(t, awaitingConfig) + require.NoError(t, err) + require.True(t, awaitingConfig) seRows := []setupExperienceInsertTestRows{} @@ -435,8 +532,8 @@ func testEnqueueSetupExperienceItems(t *testing.T, ds *Datastore) { return sqlx.SelectContext(ctx, q, &seRows, "SELECT host_uuid, name, status, software_installer_id, setup_experience_script_id, vpp_app_team_id FROM setup_experience_status_results") }) - // four hosts with three items enqueued each. - require.Len(t, seRows, 12) + // five hosts with three items enqueued each. + require.Len(t, seRows, 15) for _, tc := range []setupExperienceInsertTestRows{ { @@ -525,9 +622,9 @@ func testEnqueueSetupExperienceItems(t *testing.T, ds *Datastore) { return sqlx.SelectContext(ctx, q, &seRows, "SELECT host_uuid, name, status, software_installer_id, setup_experience_script_id, vpp_app_team_id FROM setup_experience_status_results") }) - // Only the team 1 host should have items enqueued now. + // Only the team 1 and team 3 hosts should have items enqueued now. // Two hosts with three items each. - require.Len(t, seRows, 6) + require.Len(t, seRows, 9) for _, tc := range []setupExperienceInsertTestRows{ { @@ -562,11 +659,8 @@ func testEnqueueSetupExperienceItems(t *testing.T, ds *Datastore) { } for _, row := range seRows { - if row.HostUUID == hostTeam3 || row.HostUUID == hostTeam2 { + if row.HostUUID == hostTeam2 { team := 2 - if row.HostUUID == hostTeam3 { - team = 3 - } t.Errorf("team %d shouldn't have any any entries", team) } }