Bugfix: clear lock/wipe host actions on re-enrollment as new host row (#33561)
This commit is contained in:
@@ -0,0 +1 @@
|
||||
* Fixed a scenario where a wiped Windows host re-enrolled as a distinct host row in Fleet and the previous host's page could not be loaded successfully.
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 = ?
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user