Bugfix: clear lock/wipe host actions on re-enrollment as new host row (#33561)

This commit is contained in:
Martin Angers
2025-09-30 16:16:03 -04:00
committed by GitHub
parent 12c42ab6c4
commit 6f800e2d5b
9 changed files with 169 additions and 37 deletions
+1 -1
View File
@@ -1095,7 +1095,7 @@ SELECT
h.hardware_serial,
-- if the status filter is "pending", we want to return "pending" for all hosts
? as status,
-- pending hosts will have "updated_at" set in the db, but since
-- pending hosts will have "updated_at" set in the db, but since
-- we're using it to mean "executed at" we'll return it as empty.
CASE
WHEN ? != 'pending' THEN hsr.updated_at
+40 -15
View File
@@ -126,22 +126,47 @@ func (ds *Datastore) MDMWindowsInsertEnrolledDevice(ctx context.Context, device
return nil
}
// MDMWindowsDeleteEnrolledDevice deletes an MDMWindowsEnrolledDevice entry
// from the database using the device's hardware ID.
func (ds *Datastore) MDMWindowsDeleteEnrolledDevice(ctx context.Context, mdmDeviceHWID string) error {
stmt := "DELETE FROM mdm_windows_enrollments WHERE mdm_hardware_id = ?"
// MDMWindowsDeleteEnrolledDeviceOnReenrollment deletes a Windows device
// enrollment entry from the database using the device's hardware ID as it is
// re-enrolling.
func (ds *Datastore) MDMWindowsDeleteEnrolledDeviceOnReenrollment(ctx context.Context, mdmDeviceHWID string) error {
const (
delStmt = "DELETE FROM mdm_windows_enrollments WHERE mdm_hardware_id = ?"
loadStmt = "SELECT host_uuid FROM mdm_windows_enrollments WHERE mdm_hardware_id = ? LIMIT 1"
delActionsStmt = "DELETE FROM host_mdm_actions WHERE host_id = (SELECT id FROM hosts WHERE uuid = ? LIMIT 1)"
)
res, err := ds.writer(ctx).ExecContext(ctx, stmt, mdmDeviceHWID)
if err != nil {
return ctxerr.Wrap(ctx, err, "delete MDMWindowsEnrolledDevice")
}
return ds.withRetryTxx(ctx, func(tx sqlx.ExtContext) error {
var hostUUID sql.NullString
switch err := sqlx.GetContext(ctx, tx, &hostUUID, loadStmt, mdmDeviceHWID); err {
case nil:
// found the host uuid, clear its lock/wipe status
if hostUUID.Valid {
if _, err := tx.ExecContext(ctx, delActionsStmt, hostUUID.String); err != nil {
return ctxerr.Wrap(ctx, err, "delete host_mdm_actions for host")
}
}
deleted, _ := res.RowsAffected()
if deleted == 1 {
return nil
}
case sql.ErrNoRows:
// nothing to delete, return early
return ctxerr.Wrap(ctx, notFound("MDMWindowsEnrolledDevice"))
return ctxerr.Wrap(ctx, notFound("MDMWindowsEnrolledDevice"))
default:
return ctxerr.Wrap(ctx, err, "load host_uuid for MDMWindowsEnrolledDevice")
}
res, err := tx.ExecContext(ctx, delStmt, mdmDeviceHWID)
if err != nil {
return ctxerr.Wrap(ctx, err, "delete MDMWindowsEnrolledDevice")
}
deleted, _ := res.RowsAffected()
if deleted == 1 {
return nil
}
return ctxerr.Wrap(ctx, notFound("MDMWindowsEnrolledDevice"))
})
}
// MDMWindowsDeleteEnrolledDeviceWithDeviceID deletes a given
@@ -574,7 +599,7 @@ AND ` + whereKeyAvailable + `
AND (
(` + whereEncrypted + ` AND NOT ` + whereHostDisksUpdated + `)
OR (NOT ` + whereEncrypted + ` AND ` + whereHostDisksUpdated + ` AND ` + withinGracePeriod + `)
)
)
AND ` + whereBitLockerPINSet
case fleet.DiskEncryptionActionRequired:
@@ -584,7 +609,7 @@ AND ` + whereBitLockerPINSet
AND NOT ` + whereClientError + `
AND ` + whereKeyAvailable + `
AND (
` + whereEncrypted + `
` + whereEncrypted + `
OR (NOT ` + whereEncrypted + ` AND ` + whereHostDisksUpdated + ` AND ` + withinGracePeriod + `)
)
AND NOT ` + whereBitLockerPINSet
+4 -4
View File
@@ -86,14 +86,14 @@ func testMDMWindowsEnrolledDevice(t *testing.T, ds *Datastore) {
require.Equal(t, enrolledDevice.MDMDeviceID, gotEnrolledDevice.MDMDeviceID)
require.Equal(t, enrolledDevice.MDMHardwareID, gotEnrolledDevice.MDMHardwareID)
err = ds.MDMWindowsDeleteEnrolledDevice(ctx, enrolledDevice.MDMHardwareID)
err = ds.MDMWindowsDeleteEnrolledDeviceOnReenrollment(ctx, enrolledDevice.MDMHardwareID)
require.NoError(t, err)
var nfe fleet.NotFoundError
_, err = ds.MDMWindowsGetEnrolledDeviceWithDeviceID(ctx, enrolledDevice.MDMDeviceID)
require.ErrorAs(t, err, &nfe)
err = ds.MDMWindowsDeleteEnrolledDevice(ctx, enrolledDevice.MDMHardwareID)
err = ds.MDMWindowsDeleteEnrolledDeviceOnReenrollment(ctx, enrolledDevice.MDMHardwareID)
require.ErrorAs(t, err, &nfe)
// Test using device ID instead of hardware ID
@@ -117,7 +117,7 @@ func testMDMWindowsEnrolledDevice(t *testing.T, ds *Datastore) {
_, err = ds.MDMWindowsGetEnrolledDeviceWithDeviceID(ctx, enrolledDevice.MDMDeviceID)
require.ErrorAs(t, err, &nfe)
err = ds.MDMWindowsDeleteEnrolledDevice(ctx, enrolledDevice.MDMHardwareID)
err = ds.MDMWindowsDeleteEnrolledDeviceOnReenrollment(ctx, enrolledDevice.MDMHardwareID)
require.ErrorAs(t, err, &nfe)
}
@@ -2098,7 +2098,7 @@ func testMDMWindowsConfigProfilesWithFleetVars(t *testing.T, ds *Datastore) {
// Query the mdm_configuration_profile_variables table to verify the variables were persisted
var varNames []string
stmt := `
SELECT fv.name
SELECT fv.name
FROM mdm_configuration_profile_variables mcpv
JOIN fleet_variables fv ON mcpv.fleet_variable_id = fv.id
WHERE mcpv.windows_profile_uuid = ?
+7 -7
View File
@@ -504,7 +504,7 @@ func (ds *Datastore) UpdateScriptContents(ctx context.Context, scriptID uint, sc
// Update the script to point to the new content
if newContentID != oldContentID {
updateStmt := `
UPDATE scripts
UPDATE scripts
SET script_content_id = ?
WHERE id = ?
`
@@ -638,8 +638,8 @@ func (ds *Datastore) cleanupScriptContent(ctx context.Context, tx sqlx.ExtContex
UNION ALL
SELECT 1 FROM setup_experience_scripts WHERE script_content_id = ?
UNION ALL
SELECT 1 FROM software_installers WHERE
install_script_content_id = ?
SELECT 1 FROM software_installers WHERE
install_script_content_id = ?
OR uninstall_script_content_id = ?
OR post_install_script_content_id = ?
UNION ALL
@@ -992,9 +992,9 @@ WITH all_latest_activities AS (
canceled = 0
) completed_ranked
WHERE row_num = 1
UNION ALL
-- latest from upcoming_activities
SELECT * FROM (
SELECT
@@ -1035,7 +1035,7 @@ FROM
*,
ROW_NUMBER() OVER (
PARTITION BY script_id
ORDER BY
ORDER BY
CASE WHEN source = 'upcoming' THEN 1 ELSE 2 END, -- Prefer upcoming over completed
created_at DESC,
id DESC
@@ -2392,7 +2392,7 @@ FROM (
ba.created_at AS created_at,
j.not_before AS not_before,
ba.id AS id
FROM batch_activities ba
FROM batch_activities ba
LEFT JOIN batch_activity_host_results bahr
ON ba.execution_id = bahr.batch_execution_id
LEFT JOIN host_script_results hsr
+3 -2
View File
@@ -1654,8 +1654,9 @@ type Datastore interface {
// MDMWindowsInsertEnrolledDevice inserts a new MDMWindowsEnrolledDevice in the database
MDMWindowsInsertEnrolledDevice(ctx context.Context, device *MDMWindowsEnrolledDevice) error
// MDMWindowsDeleteEnrolledDevice deletes a give MDMWindowsEnrolledDevice entry from the database using the HW device id.
MDMWindowsDeleteEnrolledDevice(ctx context.Context, mdmDeviceHWID string) error
// MDMWindowsDeleteEnrolledDeviceOnReenrollment deletes a given windows
// device enrollment entry from the database using the HW device id.
MDMWindowsDeleteEnrolledDeviceOnReenrollment(ctx context.Context, mdmDeviceHWID string) error
// MDMWindowsGetEnrolledDeviceWithDeviceID receives a Windows MDM device id and returns the device information
MDMWindowsGetEnrolledDeviceWithDeviceID(ctx context.Context, mdmDeviceID string) (*MDMWindowsEnrolledDevice, error)
+6 -6
View File
@@ -1105,7 +1105,7 @@ type WSTEPAssociateCertHashFunc func(ctx context.Context, deviceUUID string, has
type MDMWindowsInsertEnrolledDeviceFunc func(ctx context.Context, device *fleet.MDMWindowsEnrolledDevice) error
type MDMWindowsDeleteEnrolledDeviceFunc func(ctx context.Context, mdmDeviceHWID string) error
type MDMWindowsDeleteEnrolledDeviceOnReenrollmentFunc func(ctx context.Context, mdmDeviceHWID string) error
type MDMWindowsGetEnrolledDeviceWithDeviceIDFunc func(ctx context.Context, mdmDeviceID string) (*fleet.MDMWindowsEnrolledDevice, error)
@@ -3165,8 +3165,8 @@ type DataStore struct {
MDMWindowsInsertEnrolledDeviceFunc MDMWindowsInsertEnrolledDeviceFunc
MDMWindowsInsertEnrolledDeviceFuncInvoked bool
MDMWindowsDeleteEnrolledDeviceFunc MDMWindowsDeleteEnrolledDeviceFunc
MDMWindowsDeleteEnrolledDeviceFuncInvoked bool
MDMWindowsDeleteEnrolledDeviceOnReenrollmentFunc MDMWindowsDeleteEnrolledDeviceOnReenrollmentFunc
MDMWindowsDeleteEnrolledDeviceOnReenrollmentFuncInvoked bool
MDMWindowsGetEnrolledDeviceWithDeviceIDFunc MDMWindowsGetEnrolledDeviceWithDeviceIDFunc
MDMWindowsGetEnrolledDeviceWithDeviceIDFuncInvoked bool
@@ -7599,11 +7599,11 @@ func (s *DataStore) MDMWindowsInsertEnrolledDevice(ctx context.Context, device *
return s.MDMWindowsInsertEnrolledDeviceFunc(ctx, device)
}
func (s *DataStore) MDMWindowsDeleteEnrolledDevice(ctx context.Context, mdmDeviceHWID string) error {
func (s *DataStore) MDMWindowsDeleteEnrolledDeviceOnReenrollment(ctx context.Context, mdmDeviceHWID string) error {
s.mu.Lock()
s.MDMWindowsDeleteEnrolledDeviceFuncInvoked = true
s.MDMWindowsDeleteEnrolledDeviceOnReenrollmentFuncInvoked = true
s.mu.Unlock()
return s.MDMWindowsDeleteEnrolledDeviceFunc(ctx, mdmDeviceHWID)
return s.MDMWindowsDeleteEnrolledDeviceOnReenrollmentFunc(ctx, mdmDeviceHWID)
}
func (s *DataStore) MDMWindowsGetEnrolledDeviceWithDeviceID(ctx context.Context, mdmDeviceID string) (*fleet.MDMWindowsEnrolledDevice, error) {
+106 -1
View File
@@ -7398,7 +7398,7 @@ func (s *integrationMDMTestSuite) TestValidRequestSecurityTokenRequestWithDevice
windowsHost := createOrbitEnrolledHost(t, "windows", "h1", s.ds)
// Delete the host from the list of MDM enrolled devices if present
_ = s.ds.MDMWindowsDeleteEnrolledDevice(context.Background(), windowsHost.UUID)
_ = s.ds.MDMWindowsDeleteEnrolledDeviceOnReenrollment(context.Background(), windowsHost.UUID)
// Preparing the RequestSecurityToken Request message
encodedBinToken, err := fleet.GetEncodedBinarySecurityToken(fleet.WindowsMDMProgrammaticEnrollmentType, *windowsHost.OrbitNodeKey)
@@ -18194,3 +18194,108 @@ func (s *integrationMDMTestSuite) TestIOSiPadOSRefetch() {
require.NotNil(s.T(), failedHostMDMTokenInactive)
require.False(s.T(), failedHostMDMTokenInactive.Enrolled)
}
// for https://github.com/fleetdm/fleet/issues/29086
func (s *integrationMDMTestSuite) TestWipeWindowsReenrollAsNewHost() {
t := s.T()
ctx := context.Background()
// create an MDM-enrolled Windows host
host, winMDMClient := createWindowsHostThenEnrollMDM(s.ds, s.server.URL, t)
// update its serial number to empty, to simulate the DreamQuest device (and
// others) where this can happen
host.HardwareSerial = ""
err := s.ds.UpdateHost(ctx, host)
require.NoError(t, err)
// get the host's information
var getHostResp getHostResponse
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d", host.ID), nil, http.StatusOK, &getHostResp)
require.NotNil(t, getHostResp.Host.MDM.DeviceStatus)
require.Equal(t, "unlocked", *getHostResp.Host.MDM.DeviceStatus)
require.NotNil(t, getHostResp.Host.MDM.PendingAction)
require.Equal(t, "", *getHostResp.Host.MDM.PendingAction)
// wipe the host
var wipeResp wipeHostResponse
s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/wipe", host.ID), nil, http.StatusOK, &wipeResp)
require.Equal(t, fleet.PendingActionWipe, wipeResp.PendingAction)
require.Equal(t, fleet.DeviceStatusUnlocked, wipeResp.DeviceStatus)
// refresh the host's status, it is unlocked, pending wipe
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d", host.ID), nil, http.StatusOK, &getHostResp)
require.NotNil(t, getHostResp.Host.MDM.DeviceStatus)
require.Equal(t, string(fleet.DeviceStatusUnlocked), *getHostResp.Host.MDM.DeviceStatus)
require.NotNil(t, getHostResp.Host.MDM.PendingAction)
require.Equal(t, string(fleet.PendingActionWipe), *getHostResp.Host.MDM.PendingAction)
status, err := s.ds.GetHostLockWipeStatus(ctx, host)
require.NoError(t, err)
// simulate a successful wipe from the Windows device's MDM response
cmds, err := winMDMClient.StartManagementSession()
require.NoError(t, err)
// two status + the wipe command we enqueued
require.Len(t, cmds, 3)
wipeCmd := cmds[status.WipeMDMCommand.CommandUUID]
require.NotNil(t, wipeCmd)
require.Equal(t, wipeCmd.Verb, fleet.CmdExec)
require.Len(t, wipeCmd.Cmd.Items, 1)
require.EqualValues(t, "./Device/Vendor/MSFT/RemoteWipe/doWipeProtected", *wipeCmd.Cmd.Items[0].Target)
msgID, err := winMDMClient.GetCurrentMsgID()
require.NoError(t, err)
winMDMClient.AppendResponse(fleet.SyncMLCmd{
XMLName: xml.Name{Local: fleet.CmdStatus},
MsgRef: &msgID,
CmdRef: &status.WipeMDMCommand.CommandUUID,
Cmd: ptr.String("Exec"),
Data: ptr.String("200"),
Items: nil,
CmdID: fleet.CmdID{Value: uuid.NewString()},
})
cmds, err = winMDMClient.SendResponse()
require.NoError(t, err)
// the ack of the message should be the only returned command
require.Len(t, cmds, 1)
// refresh the host's status, it is wiped
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d", host.ID), nil, http.StatusOK, &getHostResp)
require.NotNil(t, getHostResp.Host.MDM.DeviceStatus)
require.Equal(t, string(fleet.DeviceStatusWiped), *getHostResp.Host.MDM.DeviceStatus)
require.NotNil(t, getHostResp.Host.MDM.PendingAction)
require.Equal(t, string(fleet.PendingActionNone), *getHostResp.Host.MDM.PendingAction)
// enroll a new host that will re-enroll as the same host for Windows MDM
// (via the same hardware device ID).
newHost := createOrbitEnrolledHost(t, "windows", uuid.NewString(), s.ds)
require.NotEqual(t, host.ID, newHost.ID)
// now re-enroll in MDM for the new host but with the same hardware device ID
newHostDevice := mdmtest.NewTestMDMClientWindowsProgramatic(s.server.URL, *newHost.OrbitNodeKey)
newHostDevice.HardwareID = winMDMClient.HardwareID
err = newHostDevice.Enroll()
require.NoError(t, err)
err = s.ds.UpdateMDMWindowsEnrollmentsHostUUID(ctx, newHost.UUID, newHostDevice.DeviceID)
require.NoError(t, err)
err = s.ds.SetOrUpdateMDMData(ctx, newHost.ID, false, true, s.server.URL, false, fleet.WellKnownMDMFleet, "", false)
require.NoError(t, err)
// refresh the (old) host's status, it should not 500
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d", host.ID), nil, http.StatusOK, &getHostResp)
require.NotNil(t, getHostResp.Host.MDM.DeviceStatus)
require.Equal(t, string(fleet.DeviceStatusUnlocked), *getHostResp.Host.MDM.DeviceStatus)
require.NotNil(t, getHostResp.Host.MDM.PendingAction)
require.Equal(t, string(fleet.PendingActionNone), *getHostResp.Host.MDM.PendingAction)
// attempting to wipe the old host entry should fail, as it is not reported
// as enrolled in Fleet MDM anymore
s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/wipe", host.ID), nil, http.StatusUnprocessableEntity, &wipeResp)
// attempting to wipe the new host entry works
s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/wipe", newHost.ID), nil, http.StatusOK, &wipeResp)
require.Equal(t, fleet.PendingActionWipe, wipeResp.PendingAction)
require.Equal(t, fleet.DeviceStatusUnlocked, wipeResp.DeviceStatus)
}
+1 -1
View File
@@ -1669,7 +1669,7 @@ func (svc *Service) removeWindowsDeviceIfAlreadyMDMEnrolled(ctx context.Context,
}
// Device is already enrolled, let's remove it
err = svc.ds.MDMWindowsDeleteEnrolledDevice(ctx, reqHWDeviceID)
err = svc.ds.MDMWindowsDeleteEnrolledDeviceOnReenrollment(ctx, reqHWDeviceID)
if err != nil {
if fleet.IsNotFound(err) {
return nil