mirror of
https://github.com/empayre/fleet.git
synced 2024-11-06 00:45:19 +00:00
Remote wipe: add API endpoint and activity (#17060)
This commit is contained in:
parent
5d20ee85fc
commit
a01241ec2e
1
changes/10488-remote-wipe
Normal file
1
changes/10488-remote-wipe
Normal file
@ -0,0 +1 @@
|
||||
* Added the `POST /api/v1/fleet/hosts/:id/wipe` Fleet Premium API endpoint to support remote wiping a host.
|
@ -360,7 +360,7 @@ func TestGetHosts(t *testing.T) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
ds.GetHostLockWipeStatusFunc = func(ctx context.Context, hostID uint, fleetPlatform string) (*fleet.HostLockWipeStatus, error) {
|
||||
ds.GetHostLockWipeStatusFunc = func(ctx context.Context, host *fleet.Host) (*fleet.HostLockWipeStatus, error) {
|
||||
return &fleet.HostLockWipeStatus{}, nil
|
||||
}
|
||||
|
||||
|
@ -196,7 +196,7 @@ func TestMDMRunCommand(t *testing.T) {
|
||||
ds.GetHostMDMMacOSSetupFunc = func(ctx context.Context, hostID uint) (*fleet.HostMDMMacOSSetup, error) {
|
||||
return nil, nil
|
||||
}
|
||||
ds.GetHostLockWipeStatusFunc = func(ctx context.Context, hostID uint, fleetPlatform string) (*fleet.HostLockWipeStatus, error) {
|
||||
ds.GetHostLockWipeStatusFunc = func(ctx context.Context, host *fleet.Host) (*fleet.HostLockWipeStatus, error) {
|
||||
return &fleet.HostLockWipeStatus{}, nil
|
||||
}
|
||||
ds.ListHostsLiteByUUIDsFunc = func(ctx context.Context, filter fleet.TeamFilter, uuids []string) ([]*fleet.Host, error) {
|
||||
@ -440,11 +440,13 @@ func TestMDMLockCommand(t *testing.T) {
|
||||
ds.GetHostMDMMacOSSetupFunc = func(ctx context.Context, hostID uint) (*fleet.HostMDMMacOSSetup, error) {
|
||||
return nil, nil
|
||||
}
|
||||
ds.GetHostLockWipeStatusFunc = func(ctx context.Context, hostID uint, fleetPlatform string) (*fleet.HostLockWipeStatus, error) {
|
||||
ds.GetHostLockWipeStatusFunc = func(ctx context.Context, host *fleet.Host) (*fleet.HostLockWipeStatus, error) {
|
||||
fleetPlatform := host.FleetPlatform()
|
||||
|
||||
var status fleet.HostLockWipeStatus
|
||||
status.HostFleetPlatform = fleetPlatform
|
||||
|
||||
if _, ok := unlockPending[hostID]; ok {
|
||||
if _, ok := unlockPending[host.ID]; ok {
|
||||
if fleetPlatform == "darwin" {
|
||||
status.UnlockPIN = "1234"
|
||||
status.UnlockRequestedAt = time.Now()
|
||||
@ -454,7 +456,7 @@ func TestMDMLockCommand(t *testing.T) {
|
||||
status.UnlockScript = &fleet.HostScriptResult{}
|
||||
}
|
||||
|
||||
if _, ok := lockPending[hostID]; ok {
|
||||
if _, ok := lockPending[host.ID]; ok {
|
||||
if fleetPlatform == "darwin" {
|
||||
status.LockMDMCommand = &fleet.MDMCommand{}
|
||||
return &status, nil
|
||||
@ -710,10 +712,12 @@ func TestMDMUnlockCommand(t *testing.T) {
|
||||
ds.GetHostMDMMacOSSetupFunc = func(ctx context.Context, hostID uint) (*fleet.HostMDMMacOSSetup, error) {
|
||||
return nil, nil
|
||||
}
|
||||
ds.GetHostLockWipeStatusFunc = func(ctx context.Context, hostID uint, fleetPlatform string) (*fleet.HostLockWipeStatus, error) {
|
||||
ds.GetHostLockWipeStatusFunc = func(ctx context.Context, host *fleet.Host) (*fleet.HostLockWipeStatus, error) {
|
||||
fleetPlatform := host.FleetPlatform()
|
||||
|
||||
var status fleet.HostLockWipeStatus
|
||||
status.HostFleetPlatform = fleetPlatform
|
||||
if _, ok := locked[hostID]; ok {
|
||||
if _, ok := locked[host.ID]; ok {
|
||||
if fleetPlatform == "darwin" {
|
||||
status.LockMDMCommand = &fleet.MDMCommand{}
|
||||
status.LockMDMCommandResult = &fleet.MDMCommandResult{Status: fleet.MDMAppleStatusAcknowledged}
|
||||
@ -723,7 +727,7 @@ func TestMDMUnlockCommand(t *testing.T) {
|
||||
status.LockScript = &fleet.HostScriptResult{ExitCode: ptr.Int64(0)}
|
||||
}
|
||||
|
||||
if _, ok := unlockPending[hostID]; ok {
|
||||
if _, ok := unlockPending[host.ID]; ok {
|
||||
if fleetPlatform == "darwin" {
|
||||
status.UnlockPIN = "1234"
|
||||
status.UnlockRequestedAt = time.Now()
|
||||
@ -733,7 +737,7 @@ func TestMDMUnlockCommand(t *testing.T) {
|
||||
status.UnlockScript = &fleet.HostScriptResult{}
|
||||
}
|
||||
|
||||
if _, ok := lockPending[hostID]; ok {
|
||||
if _, ok := lockPending[host.ID]; ok {
|
||||
if fleetPlatform == "darwin" {
|
||||
status.LockMDMCommand = &fleet.MDMCommand{}
|
||||
return &status, nil
|
||||
|
@ -240,7 +240,7 @@ Fleet records the last 10,000 characters to prevent downtime.
|
||||
}
|
||||
return &fleet.HostScriptResult{}, nil
|
||||
}
|
||||
ds.GetHostLockWipeStatusFunc = func(ctx context.Context, hostID uint, fleetPlatform string) (*fleet.HostLockWipeStatus, error) {
|
||||
ds.GetHostLockWipeStatusFunc = func(ctx context.Context, host *fleet.Host) (*fleet.HostLockWipeStatus, error) {
|
||||
return &fleet.HostLockWipeStatus{}, nil
|
||||
}
|
||||
ds.NewHostScriptExecutionRequestFunc = func(ctx context.Context, req *fleet.HostScriptRequestPayload) (*fleet.HostScriptResult, error) {
|
||||
|
@ -1032,6 +1032,23 @@ This activity contains the following fields:
|
||||
}
|
||||
```
|
||||
|
||||
## wiped_host
|
||||
|
||||
Generated when a user sends a request to wipe a host.
|
||||
|
||||
This activity contains the following fields:
|
||||
- "host_id": ID of the host.
|
||||
- "host_display_name": Display name of the host.
|
||||
|
||||
#### Example
|
||||
|
||||
```json
|
||||
{
|
||||
"host_id": 1,
|
||||
"host_display_name": "Anna's MacBook Pro"
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
<meta name="title" value="Audit logs">
|
||||
<meta name="pageOrderInSection" value="1400">
|
||||
|
46
ee/server/service/embedded_scripts/linux_wipe.sh
Normal file
46
ee/server/service/embedded_scripts/linux_wipe.sh
Normal file
@ -0,0 +1,46 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Function to log out all users and lock their passwords except root
|
||||
logout_users() {
|
||||
for user in $(who | awk '{print $1}' | sort | uniq)
|
||||
do
|
||||
if [ "$user" != "root" ]; then
|
||||
echo "Logging out $user"
|
||||
pkill -KILL -u "$user"
|
||||
passwd -l "$user"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
# Function to wipe non-essential data
|
||||
wipe_non_essential_data() {
|
||||
# Define non-essential paths
|
||||
non_essential_paths="/home/* /tmp /var/tmp /var/log /home/*/.cache /var/cache /home/*/.local/share/Trash"
|
||||
|
||||
for path in $non_essential_paths
|
||||
do
|
||||
if [ -e "$path" ]; then
|
||||
echo "Wiping $path"
|
||||
rm -rf "$path"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
# Function to wipe system files - Warning: This will render the system inoperable
|
||||
wipe_system_files() {
|
||||
# Define essential system paths
|
||||
essential_system_paths="/bin /sbin /usr /lib"
|
||||
|
||||
for path in $essential_system_paths
|
||||
do
|
||||
echo "Wiping $path"
|
||||
rm -rf "$path"
|
||||
done
|
||||
}
|
||||
|
||||
# Start the wiping process
|
||||
logout_users
|
||||
wipe_non_essential_data
|
||||
wipe_system_files
|
||||
|
||||
echo "Wiping process completed."
|
@ -3,6 +3,7 @@ package service
|
||||
import (
|
||||
"context"
|
||||
_ "embed"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
@ -55,21 +56,22 @@ func (svc *Service) LockHost(ctx context.Context, hostID uint) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// TODO(mna): error messages are subtly different in the figma for CLI and
|
||||
// UI, they should be the same as they come from the same place (the API).
|
||||
// I used the CLI messages for the implementation.
|
||||
|
||||
// locking validations are based on the platform of the host
|
||||
switch host.FleetPlatform() {
|
||||
case "darwin":
|
||||
if err := svc.VerifyMDMAppleConfigured(ctx); err != nil {
|
||||
err := fleet.NewInvalidArgumentError("host_id", fleet.AppleMDMNotConfiguredMessage).WithStatus(http.StatusBadRequest)
|
||||
if errors.Is(err, fleet.ErrMDMNotConfigured) {
|
||||
err = fleet.NewInvalidArgumentError("host_id", fleet.AppleMDMNotConfiguredMessage).WithStatus(http.StatusBadRequest)
|
||||
}
|
||||
return ctxerr.Wrap(ctx, err, "check macOS MDM enabled")
|
||||
}
|
||||
|
||||
// on macOS, the lock command requires the host to be MDM-enrolled in Fleet
|
||||
hostMDM, err := svc.ds.GetHostMDM(ctx, host.ID)
|
||||
if err != nil {
|
||||
if fleet.IsNotFound(err) {
|
||||
return ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("host_id", "Can't lock the host because it doesn't have MDM turned on."))
|
||||
}
|
||||
return ctxerr.Wrap(ctx, err, "get host MDM information")
|
||||
}
|
||||
if !hostMDM.IsFleetEnrolled() {
|
||||
@ -79,7 +81,9 @@ func (svc *Service) LockHost(ctx context.Context, hostID uint) error {
|
||||
case "windows", "linux":
|
||||
if host.FleetPlatform() == "windows" {
|
||||
if err := svc.VerifyMDMWindowsConfigured(ctx); err != nil {
|
||||
err := fleet.NewInvalidArgumentError("host_id", fleet.WindowsMDMNotConfiguredMessage).WithStatus(http.StatusBadRequest)
|
||||
if errors.Is(err, fleet.ErrMDMNotConfigured) {
|
||||
err = fleet.NewInvalidArgumentError("host_id", fleet.WindowsMDMNotConfiguredMessage).WithStatus(http.StatusBadRequest)
|
||||
}
|
||||
return ctxerr.Wrap(ctx, err, "check windows MDM enabled")
|
||||
}
|
||||
}
|
||||
@ -94,13 +98,12 @@ func (svc *Service) LockHost(ctx context.Context, hostID uint) error {
|
||||
}
|
||||
|
||||
default:
|
||||
// TODO(mna): should we allow/treat ChromeOS as Linux for this purpose?
|
||||
return ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("host_id", fmt.Sprintf("Unsupported host platform: %s", host.Platform)))
|
||||
}
|
||||
|
||||
// if there's a lock, unlock or wipe action pending, do not accept the lock
|
||||
// request.
|
||||
lockWipe, err := svc.ds.GetHostLockWipeStatus(ctx, host.ID, host.FleetPlatform())
|
||||
lockWipe, err := svc.ds.GetHostLockWipeStatus(ctx, host)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "get host lock/wipe status")
|
||||
}
|
||||
@ -148,7 +151,9 @@ func (svc *Service) UnlockHost(ctx context.Context, hostID uint) (string, error)
|
||||
// be enabled
|
||||
if host.FleetPlatform() == "windows" {
|
||||
if err := svc.VerifyMDMWindowsConfigured(ctx); err != nil {
|
||||
err := fleet.NewInvalidArgumentError("host_id", fleet.WindowsMDMNotConfiguredMessage).WithStatus(http.StatusBadRequest)
|
||||
if errors.Is(err, fleet.ErrMDMNotConfigured) {
|
||||
err = fleet.NewInvalidArgumentError("host_id", fleet.WindowsMDMNotConfiguredMessage).WithStatus(http.StatusBadRequest)
|
||||
}
|
||||
return "", ctxerr.Wrap(ctx, err, "check windows MDM enabled")
|
||||
}
|
||||
}
|
||||
@ -161,11 +166,10 @@ func (svc *Service) UnlockHost(ctx context.Context, hostID uint) (string, error)
|
||||
}
|
||||
|
||||
default:
|
||||
// TODO(mna): should we allow/treat ChromeOS as Linux for this purpose?
|
||||
return "", ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("host_id", fmt.Sprintf("Unsupported host platform: %s", host.Platform)))
|
||||
}
|
||||
|
||||
lockWipe, err := svc.ds.GetHostLockWipeStatus(ctx, host.ID, host.FleetPlatform())
|
||||
lockWipe, err := svc.ds.GetHostLockWipeStatus(ctx, host)
|
||||
if err != nil {
|
||||
return "", ctxerr.Wrap(ctx, err, "get host lock/wipe status")
|
||||
}
|
||||
@ -190,6 +194,95 @@ func (svc *Service) UnlockHost(ctx context.Context, hostID uint) (string, error)
|
||||
return svc.enqueueUnlockHostRequest(ctx, host, lockWipe)
|
||||
}
|
||||
|
||||
func (svc *Service) WipeHost(ctx context.Context, hostID uint) error {
|
||||
// First ensure the user has access to list hosts, then check the specific
|
||||
// host once team_id is loaded.
|
||||
if err := svc.authz.Authorize(ctx, &fleet.Host{}, fleet.ActionList); err != nil {
|
||||
return err
|
||||
}
|
||||
host, err := svc.ds.HostLite(ctx, hostID)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "get host lite")
|
||||
}
|
||||
|
||||
// Authorize again with team loaded now that we have the host's team_id.
|
||||
// Authorize as "execute mdm_command", which is the correct access
|
||||
// requirement and is what happens for macOS platforms.
|
||||
if err := svc.authz.Authorize(ctx, fleet.MDMCommandAuthz{TeamID: host.TeamID}, fleet.ActionWrite); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// wipe validations are based on the platform of the host, Windows and macOS
|
||||
// require MDM to be enabled and the host to be MDM-enrolled in Fleet. Linux
|
||||
// uses scripts, not MDM.
|
||||
var requireMDM bool
|
||||
switch host.FleetPlatform() {
|
||||
case "darwin":
|
||||
if err := svc.VerifyMDMAppleConfigured(ctx); err != nil {
|
||||
if errors.Is(err, fleet.ErrMDMNotConfigured) {
|
||||
err = fleet.NewInvalidArgumentError("host_id", fleet.AppleMDMNotConfiguredMessage).WithStatus(http.StatusBadRequest)
|
||||
}
|
||||
return ctxerr.Wrap(ctx, err, "check macOS MDM enabled")
|
||||
}
|
||||
requireMDM = true
|
||||
|
||||
case "windows":
|
||||
if err := svc.VerifyMDMWindowsConfigured(ctx); err != nil {
|
||||
if errors.Is(err, fleet.ErrMDMNotConfigured) {
|
||||
err = fleet.NewInvalidArgumentError("host_id", fleet.WindowsMDMNotConfiguredMessage).WithStatus(http.StatusBadRequest)
|
||||
}
|
||||
return ctxerr.Wrap(ctx, err, "check windows MDM enabled")
|
||||
}
|
||||
requireMDM = true
|
||||
|
||||
case "linux":
|
||||
// on linux, a script is used to wipe the host so scripts must be enabled
|
||||
appCfg, err := svc.ds.AppConfig(ctx)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "get app config")
|
||||
}
|
||||
if appCfg.ServerSettings.ScriptsDisabled {
|
||||
return ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("host_id", "Can't wipe host because running scripts is disabled in organization settings."))
|
||||
}
|
||||
|
||||
default:
|
||||
return ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("host_id", fmt.Sprintf("Unsupported host platform: %s", host.Platform)))
|
||||
}
|
||||
|
||||
if requireMDM {
|
||||
// the wipe command requires the host to be MDM-enrolled in Fleet
|
||||
hostMDM, err := svc.ds.GetHostMDM(ctx, host.ID)
|
||||
if err != nil {
|
||||
if fleet.IsNotFound(err) {
|
||||
return ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("host_id", "Can't wipe the host because it doesn't have MDM turned on."))
|
||||
}
|
||||
return ctxerr.Wrap(ctx, err, "get host MDM information")
|
||||
}
|
||||
if !hostMDM.IsFleetEnrolled() {
|
||||
return ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("host_id", "Can't wipe the host because it doesn't have MDM turned on."))
|
||||
}
|
||||
}
|
||||
|
||||
// validations based on host's actions status (pending lock, unlock, wipe)
|
||||
lockWipe, err := svc.ds.GetHostLockWipeStatus(ctx, host)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "get host lock/wipe status")
|
||||
}
|
||||
switch {
|
||||
case lockWipe.IsPendingLock():
|
||||
return ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("host_id", "Host has pending lock request. Host cannot be wiped until lock is complete."))
|
||||
case lockWipe.IsPendingUnlock():
|
||||
return ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("host_id", "Host has pending unlock request. Host cannot be wiped until unlock is complete."))
|
||||
case lockWipe.IsPendingWipe():
|
||||
return ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("host_id", "Host has pending wipe request. The host will be wiped when it comes online."))
|
||||
case lockWipe.IsWiped():
|
||||
return ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("host_id", "Host is already wiped.").WithStatus(http.StatusConflict))
|
||||
}
|
||||
|
||||
// all good, go ahead with queuing the wipe request.
|
||||
return svc.enqueueWipeHostRequest(ctx, host, lockWipe)
|
||||
}
|
||||
|
||||
func (svc *Service) enqueueLockHostRequest(ctx context.Context, host *fleet.Host, lockStatus *fleet.HostLockWipeStatus) error {
|
||||
vc, ok := viewer.FromContext(ctx)
|
||||
if !ok {
|
||||
@ -301,6 +394,59 @@ func (svc *Service) enqueueUnlockHostRequest(ctx context.Context, host *fleet.Ho
|
||||
return unlockPIN, nil
|
||||
}
|
||||
|
||||
func (svc *Service) enqueueWipeHostRequest(ctx context.Context, host *fleet.Host, wipeStatus *fleet.HostLockWipeStatus) error {
|
||||
vc, ok := viewer.FromContext(ctx)
|
||||
if !ok {
|
||||
return fleet.ErrNoContext
|
||||
}
|
||||
|
||||
switch wipeStatus.HostFleetPlatform {
|
||||
case "darwin":
|
||||
wipeCommandUUID := uuid.NewString()
|
||||
if err := svc.mdmAppleCommander.EraseDevice(ctx, host, wipeCommandUUID); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "enqueuing wipe request for darwin")
|
||||
}
|
||||
|
||||
case "windows":
|
||||
wipeCmdUUID := uuid.NewString()
|
||||
wipeCmd := &fleet.MDMWindowsCommand{
|
||||
CommandUUID: wipeCmdUUID,
|
||||
RawCommand: []byte(fmt.Sprintf(windowsWipeCommand, wipeCmdUUID)),
|
||||
TargetLocURI: "./Device/Vendor/MSFT/RemoteWipe/doWipeProtected",
|
||||
}
|
||||
if err := svc.ds.WipeHostViaWindowsMDM(ctx, host, wipeCmd); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "enqueuing wipe request for windows")
|
||||
}
|
||||
|
||||
case "linux":
|
||||
// TODO(mna): svc.RunHostScript should be refactored so that we can reuse the
|
||||
// part starting with the validation of the script (just in case), the checks
|
||||
// that we don't enqueue over the limit, etc. for any other important
|
||||
// validation we may add over there and that we bypass here by enqueueing the
|
||||
// script directly in the datastore layer.
|
||||
if err := svc.ds.WipeHostViaScript(ctx, &fleet.HostScriptRequestPayload{
|
||||
HostID: host.ID,
|
||||
ScriptContents: string(linuxWipeScript),
|
||||
UserID: &vc.User.ID,
|
||||
SyncRequest: false,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := svc.ds.NewActivity(
|
||||
ctx,
|
||||
vc.User,
|
||||
fleet.ActivityTypeWipedHost{
|
||||
HostID: host.ID,
|
||||
HostDisplayName: host.DisplayName(),
|
||||
},
|
||||
); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "create activity for wipe host request")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// TODO(mna): ideally we'd embed the scripts from the scripts/mdm/windows/..
|
||||
// and scripts/mdm/linux/.. directories where they currently exist, but this is
|
||||
// not possible (not a Go package) and I don't know if those script locations
|
||||
@ -316,4 +462,21 @@ var (
|
||||
linuxLockScript []byte
|
||||
//go:embed embedded_scripts/linux_unlock.sh
|
||||
linuxUnlockScript []byte
|
||||
//go:embed embedded_scripts/linux_wipe.sh
|
||||
linuxWipeScript []byte
|
||||
|
||||
windowsWipeCommand = `
|
||||
<Exec>
|
||||
<CmdID>%s</CmdID>
|
||||
<Item>
|
||||
<Target>
|
||||
<LocURI>./Device/Vendor/MSFT/RemoteWipe/doWipeProtected</LocURI>
|
||||
</Target>
|
||||
<Meta>
|
||||
<Format xmlns="syncml:metinf">chr</Format>
|
||||
<Type>text/plain</Type>
|
||||
</Meta>
|
||||
<Data></Data>
|
||||
</Item>
|
||||
</Exec>`
|
||||
)
|
||||
|
@ -139,14 +139,7 @@ func (svc *Service) MDMAppleEraseDevice(ctx context.Context, hostID uint) error
|
||||
return err
|
||||
}
|
||||
|
||||
// TODO: save the pin (first return value) in the database
|
||||
// TODO(mna): same here for when we implement the Wipe story, assuming this
|
||||
// implementation (which is for the deprecated /mdm/hosts/:id/wipe endpoint)
|
||||
// should work as the new endpoint, then this should call
|
||||
// svc.enqueueWipeHostRequest so that it behaves like the new endpoint. And
|
||||
// yes, we do need to save the generated PIN so the EraseDevice method
|
||||
// signature must change to return it.
|
||||
err = svc.mdmAppleCommander.EraseDevice(ctx, []string{host.UUID}, uuid.New().String())
|
||||
err = svc.mdmAppleCommander.EraseDevice(ctx, host, uuid.New().String())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -67,7 +67,7 @@ func TestMDMApple(t *testing.T) {
|
||||
{"TestMDMAppleConfigProfileHash", testMDMAppleConfigProfileHash},
|
||||
{"TestResetMDMAppleEnrollment", testResetMDMAppleEnrollment},
|
||||
{"TestMDMAppleDeleteHostDEPAssignments", testMDMAppleDeleteHostDEPAssignments},
|
||||
{"CleanMacOSMDMLock", testCleanMacOSMDMLock},
|
||||
{"LockUnlockWipeMacOS", testLockUnlockWipeMacOS},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
@ -4402,18 +4402,9 @@ func testMDMAppleDeleteHostDEPAssignments(t *testing.T, ds *Datastore) {
|
||||
}
|
||||
}
|
||||
|
||||
func testCleanMacOSMDMLock(t *testing.T, ds *Datastore) {
|
||||
func testLockUnlockWipeMacOS(t *testing.T, ds *Datastore) {
|
||||
ctx := context.Background()
|
||||
|
||||
checkState := func(t *testing.T, status *fleet.HostLockWipeStatus, unlocked, locked, wiped, pendingUnlock, pendingLock, pendingWipe bool) {
|
||||
require.Equal(t, unlocked, status.IsUnlocked())
|
||||
require.Equal(t, locked, status.IsLocked())
|
||||
require.Equal(t, wiped, status.IsWiped())
|
||||
require.Equal(t, pendingLock, status.IsPendingLock())
|
||||
require.Equal(t, pendingUnlock, status.IsPendingUnlock())
|
||||
require.Equal(t, pendingWipe, status.IsPendingWipe())
|
||||
}
|
||||
|
||||
host, err := ds.NewHost(ctx, &fleet.Host{
|
||||
Hostname: "test-host1-name",
|
||||
OsqueryHostID: ptr.String("1337"),
|
||||
@ -4425,11 +4416,11 @@ func testCleanMacOSMDMLock(t *testing.T, ds *Datastore) {
|
||||
require.NoError(t, err)
|
||||
nanoEnroll(t, ds, host, false)
|
||||
|
||||
status, err := ds.GetHostLockWipeStatus(ctx, host.ID, "macos")
|
||||
status, err := ds.GetHostLockWipeStatus(ctx, host)
|
||||
require.NoError(t, err)
|
||||
|
||||
// default state
|
||||
checkState(t, status, true, false, false, false, false, false)
|
||||
checkLockWipeState(t, status, true, false, false, false, false, false)
|
||||
|
||||
appleStore, err := ds.NewMDMAppleMDMStorage(nil, nil)
|
||||
require.NoError(t, err)
|
||||
@ -4443,18 +4434,117 @@ func testCleanMacOSMDMLock(t *testing.T, ds *Datastore) {
|
||||
err = appleStore.EnqueueDeviceLockCommand(ctx, host, cmd, "123456")
|
||||
require.NoError(t, err)
|
||||
|
||||
status, err = ds.GetHostLockWipeStatus(ctx, host.ID, host.FleetPlatform())
|
||||
// it is now pending lock
|
||||
status, err = ds.GetHostLockWipeStatus(ctx, host)
|
||||
require.NoError(t, err)
|
||||
checkState(t, status, true, false, false, false, true, false)
|
||||
checkLockWipeState(t, status, true, false, false, false, true, false)
|
||||
|
||||
// record a command result to simulate locked state
|
||||
err = appleStore.StoreCommandReport(&mdm.Request{
|
||||
EnrollID: &mdm.EnrollID{ID: host.UUID},
|
||||
Context: ctx,
|
||||
}, &mdm.CommandResults{
|
||||
CommandUUID: cmd.CommandUUID,
|
||||
Status: "Acknowledged",
|
||||
RequestType: "DeviceLock",
|
||||
Raw: cmd.Raw,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
err = ds.UpdateHostLockWipeStatusFromAppleMDMResult(ctx, host.UUID, cmd.CommandUUID, "DeviceLock", true)
|
||||
require.NoError(t, err)
|
||||
|
||||
// it is now locked
|
||||
status, err = ds.GetHostLockWipeStatus(ctx, host)
|
||||
require.NoError(t, err)
|
||||
checkLockWipeState(t, status, false, true, false, false, false, false)
|
||||
|
||||
// request an unlock, to make it pending unlock
|
||||
err = ds.UnlockHostManually(ctx, host.ID, time.Now().UTC())
|
||||
require.NoError(t, err)
|
||||
|
||||
// it is now locked pending unlock
|
||||
status, err = ds.GetHostLockWipeStatus(ctx, host)
|
||||
require.NoError(t, err)
|
||||
checkLockWipeState(t, status, false, true, false, true, false, false)
|
||||
|
||||
// execute CleanMacOSMDMLock to simulate successful unlock
|
||||
err = ds.CleanMacOSMDMLock(ctx, host.UUID)
|
||||
require.NoError(t, err)
|
||||
|
||||
status, err = ds.GetHostLockWipeStatus(ctx, host.ID, "macos")
|
||||
// it is back to unlocked state
|
||||
status, err = ds.GetHostLockWipeStatus(ctx, host)
|
||||
require.NoError(t, err)
|
||||
checkState(t, status, true, false, false, false, false, false)
|
||||
checkLockWipeState(t, status, true, false, false, false, false, false)
|
||||
require.Empty(t, status.UnlockPIN)
|
||||
|
||||
// record a request to wipe the host
|
||||
cmd = &mdm.Command{
|
||||
CommandUUID: uuid.NewString(),
|
||||
Raw: []byte("<?xml"),
|
||||
}
|
||||
cmd.Command.RequestType = "EraseDevice"
|
||||
err = appleStore.EnqueueDeviceWipeCommand(ctx, host, cmd)
|
||||
require.NoError(t, err)
|
||||
|
||||
// it is now pending wipe
|
||||
status, err = ds.GetHostLockWipeStatus(ctx, host)
|
||||
require.NoError(t, err)
|
||||
checkLockWipeState(t, status, true, false, false, false, false, true)
|
||||
|
||||
// record a command result failure to simulate failed wipe (back to unlocked)
|
||||
err = appleStore.StoreCommandReport(&mdm.Request{
|
||||
EnrollID: &mdm.EnrollID{ID: host.UUID},
|
||||
Context: ctx,
|
||||
}, &mdm.CommandResults{
|
||||
CommandUUID: cmd.CommandUUID,
|
||||
Status: "Error",
|
||||
RequestType: cmd.Command.RequestType,
|
||||
Raw: cmd.Raw,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
err = ds.UpdateHostLockWipeStatusFromAppleMDMResult(ctx, host.UUID, cmd.CommandUUID, cmd.Command.RequestType, false)
|
||||
require.NoError(t, err)
|
||||
|
||||
// it is back to unlocked
|
||||
status, err = ds.GetHostLockWipeStatus(ctx, host)
|
||||
require.NoError(t, err)
|
||||
checkLockWipeState(t, status, true, false, false, false, false, false)
|
||||
|
||||
// record a new request to wipe the host
|
||||
cmd = &mdm.Command{
|
||||
CommandUUID: uuid.NewString(),
|
||||
Raw: []byte("<?xml"),
|
||||
}
|
||||
cmd.Command.RequestType = "EraseDevice"
|
||||
err = appleStore.EnqueueDeviceWipeCommand(ctx, host, cmd)
|
||||
require.NoError(t, err)
|
||||
|
||||
// it is back to pending wipe
|
||||
status, err = ds.GetHostLockWipeStatus(ctx, host)
|
||||
require.NoError(t, err)
|
||||
checkLockWipeState(t, status, true, false, false, false, false, true)
|
||||
|
||||
// record a command result success to simulate wipe
|
||||
err = appleStore.StoreCommandReport(&mdm.Request{
|
||||
EnrollID: &mdm.EnrollID{ID: host.UUID},
|
||||
Context: ctx,
|
||||
}, &mdm.CommandResults{
|
||||
CommandUUID: cmd.CommandUUID,
|
||||
Status: "Acknowledged",
|
||||
RequestType: cmd.Command.RequestType,
|
||||
Raw: cmd.Raw,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
err = ds.UpdateHostLockWipeStatusFromAppleMDMResult(ctx, host.UUID, cmd.CommandUUID, cmd.Command.RequestType, true)
|
||||
require.NoError(t, err)
|
||||
|
||||
// it is wiped
|
||||
status, err = ds.GetHostLockWipeStatus(ctx, host)
|
||||
require.NoError(t, err)
|
||||
checkLockWipeState(t, status, false, false, true, false, false, false)
|
||||
}
|
||||
|
||||
func TestMDMAppleProfileVerification(t *testing.T) {
|
||||
|
@ -779,8 +779,6 @@ const hostMDMSelect = `,
|
||||
) mdm_host_data
|
||||
`
|
||||
|
||||
// TODO(mna): add integration tests with Get host with locked/wiped/unlocked (+pending) to ensure proper marshaling.
|
||||
|
||||
// hostMDMJoin is the SQL fragment used to join MDM-related tables to the hosts table. It is a
|
||||
// dependency of the hostMDMSelect fragment.
|
||||
const hostMDMJoin = `
|
||||
|
@ -142,28 +142,32 @@ func (ds *Datastore) MDMWindowsInsertCommandForHosts(ctx context.Context, hostUU
|
||||
}
|
||||
|
||||
return ds.withRetryTxx(ctx, func(tx sqlx.ExtContext) error {
|
||||
// first, create the command entry
|
||||
stmt := `
|
||||
INSERT INTO windows_mdm_commands (command_uuid, raw_command, target_loc_uri)
|
||||
VALUES (?, ?, ?)
|
||||
`
|
||||
if _, err := tx.ExecContext(ctx, stmt, cmd.CommandUUID, cmd.RawCommand, cmd.TargetLocURI); err != nil {
|
||||
if isDuplicate(err) {
|
||||
return ctxerr.Wrap(ctx, alreadyExists("MDMWindowsCommand", cmd.CommandUUID))
|
||||
}
|
||||
return ctxerr.Wrap(ctx, err, "inserting MDMWindowsCommand")
|
||||
}
|
||||
|
||||
// create the command execution queue entries, one per host
|
||||
for _, hostUUIDOrDeviceID := range hostUUIDsOrDeviceIDs {
|
||||
if err := ds.mdmWindowsInsertHostCommandDB(ctx, tx, hostUUIDOrDeviceID, cmd.CommandUUID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
return ds.mdmWindowsInsertCommandForHostsDB(ctx, tx, hostUUIDsOrDeviceIDs, cmd)
|
||||
})
|
||||
}
|
||||
|
||||
func (ds *Datastore) mdmWindowsInsertCommandForHostsDB(ctx context.Context, tx sqlx.ExecerContext, hostUUIDsOrDeviceIDs []string, cmd *fleet.MDMWindowsCommand) error {
|
||||
// first, create the command entry
|
||||
stmt := `
|
||||
INSERT INTO windows_mdm_commands (command_uuid, raw_command, target_loc_uri)
|
||||
VALUES (?, ?, ?)
|
||||
`
|
||||
if _, err := tx.ExecContext(ctx, stmt, cmd.CommandUUID, cmd.RawCommand, cmd.TargetLocURI); err != nil {
|
||||
if isDuplicate(err) {
|
||||
return ctxerr.Wrap(ctx, alreadyExists("MDMWindowsCommand", cmd.CommandUUID))
|
||||
}
|
||||
return ctxerr.Wrap(ctx, err, "inserting MDMWindowsCommand")
|
||||
}
|
||||
|
||||
// create the command execution queue entries, one per host
|
||||
for _, hostUUIDOrDeviceID := range hostUUIDsOrDeviceIDs {
|
||||
if err := ds.mdmWindowsInsertHostCommandDB(ctx, tx, hostUUIDOrDeviceID, cmd.CommandUUID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ds *Datastore) mdmWindowsInsertHostCommandDB(ctx context.Context, tx sqlx.ExecerContext, hostUUIDOrDeviceID, commandUUID string) error {
|
||||
stmt := `
|
||||
INSERT INTO windows_mdm_command_queue (enrollment_id, command_uuid)
|
||||
@ -228,20 +232,20 @@ func (ds *Datastore) MDMWindowsSaveResponse(ctx context.Context, deviceID string
|
||||
return ctxerr.New(ctx, "empty raw response")
|
||||
}
|
||||
|
||||
const findCommandsStmt = `SELECT command_uuid, raw_command FROM windows_mdm_commands WHERE command_uuid IN (?)`
|
||||
const (
|
||||
findCommandsStmt = `SELECT command_uuid, raw_command, target_loc_uri FROM windows_mdm_commands WHERE command_uuid IN (?)`
|
||||
saveFullRespStmt = `INSERT INTO windows_mdm_responses (enrollment_id, raw_response) VALUES (?, ?)`
|
||||
dequeueCommandsStmt = `DELETE FROM windows_mdm_command_queue WHERE command_uuid IN (?)`
|
||||
|
||||
const saveFullRespStmt = `INSERT INTO windows_mdm_responses (enrollment_id, raw_response) VALUES (?, ?)`
|
||||
|
||||
const dequeueCommandsStmt = `DELETE FROM windows_mdm_command_queue WHERE command_uuid IN (?)`
|
||||
|
||||
const insertResultsStmt = `
|
||||
insertResultsStmt = `
|
||||
INSERT INTO windows_mdm_command_results
|
||||
(enrollment_id, command_uuid, raw_result, response_id, status_code)
|
||||
VALUES %s
|
||||
ON DUPLICATE KEY UPDATE
|
||||
raw_result = COALESCE(VALUES(raw_result), raw_result),
|
||||
status_code = COALESCE(VALUES(status_code), status_code)
|
||||
`
|
||||
`
|
||||
)
|
||||
|
||||
enrollment, err := ds.MDMWindowsGetEnrolledDeviceWithDeviceID(ctx, deviceID)
|
||||
if err != nil {
|
||||
@ -301,9 +305,15 @@ ON DUPLICATE KEY UPDATE
|
||||
|
||||
// for all the matching UUIDs, try to find any <Status> or
|
||||
// <Result> entries to track them as responses.
|
||||
var args []any
|
||||
var sb strings.Builder
|
||||
var potentialProfilePayloads []*fleet.MDMWindowsProfilePayload
|
||||
var (
|
||||
args []any
|
||||
sb strings.Builder
|
||||
potentialProfilePayloads []*fleet.MDMWindowsProfilePayload
|
||||
|
||||
wipeCmdUUID string
|
||||
wipeCmdStatus string
|
||||
)
|
||||
|
||||
for _, cmd := range matchingCmds {
|
||||
statusCode := ""
|
||||
if status, ok := uuidsToStatus[cmd.CommandUUID]; ok && status.Data != nil {
|
||||
@ -327,6 +337,13 @@ ON DUPLICATE KEY UPDATE
|
||||
}
|
||||
args = append(args, enrollment.ID, cmd.CommandUUID, rawResult, responseID, statusCode)
|
||||
sb.WriteString("(?, ?, ?, ?, ?),")
|
||||
|
||||
// if the command is a Wipe, keep track of it so we can update
|
||||
// host_mdm_actions accordingly.
|
||||
if strings.Contains(cmd.TargetLocURI, "/Device/Vendor/MSFT/RemoteWipe/") {
|
||||
wipeCmdUUID = cmd.CommandUUID
|
||||
wipeCmdStatus = statusCode
|
||||
}
|
||||
}
|
||||
|
||||
if err := updateMDMWindowsHostProfileStatusFromResponseDB(ctx, tx, potentialProfilePayloads); err != nil {
|
||||
@ -339,6 +356,14 @@ ON DUPLICATE KEY UPDATE
|
||||
return ctxerr.Wrap(ctx, err, "inserting command results")
|
||||
}
|
||||
|
||||
// if we received a Wipe command result, update the host's status
|
||||
if wipeCmdUUID != "" {
|
||||
if err := updateHostLockWipeStatusFromResultAndHostUUID(ctx, tx, enrollment.HostUUID,
|
||||
"wipe_ref", wipeCmdUUID, strings.HasPrefix(wipeCmdStatus, "2")); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "updating wipe command result in host_mdm_actions")
|
||||
}
|
||||
}
|
||||
|
||||
// dequeue the commands
|
||||
var matchingUUIDs []string
|
||||
for _, cmd := range matchingCmds {
|
||||
@ -1874,3 +1899,26 @@ host_uuid = ? AND profile_name NOT IN(?) AND NOT (operation_type = '%s' AND COAL
|
||||
}
|
||||
return profiles, nil
|
||||
}
|
||||
|
||||
func (ds *Datastore) WipeHostViaWindowsMDM(ctx context.Context, host *fleet.Host, cmd *fleet.MDMWindowsCommand) error {
|
||||
return ds.withRetryTxx(ctx, func(tx sqlx.ExtContext) error {
|
||||
if err := ds.mdmWindowsInsertCommandForHostsDB(ctx, tx, []string{host.UUID}, cmd); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
stmt := `
|
||||
INSERT INTO host_mdm_actions (
|
||||
host_id,
|
||||
wipe_ref
|
||||
)
|
||||
VALUES (?, ?)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
wipe_ref = VALUES(wipe_ref)`
|
||||
|
||||
if _, err := tx.ExecContext(ctx, stmt, host.ID, cmd.CommandUUID); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "modifying host_mdm_actions for wipe_ref")
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
@ -1266,7 +1266,8 @@ func testMDMWindowsCommandResults(t *testing.T, ds *Datastore) {
|
||||
require.Empty(t, results)
|
||||
}
|
||||
|
||||
func windowsEnroll(t *testing.T, ds fleet.Datastore, h *fleet.Host) {
|
||||
// enrolls the host in Windows MDM and returns the device's enrollment ID.
|
||||
func windowsEnroll(t *testing.T, ds fleet.Datastore, h *fleet.Host) string {
|
||||
ctx := context.Background()
|
||||
d1 := &fleet.MDMWindowsEnrolledDevice{
|
||||
MDMDeviceID: uuid.New().String(),
|
||||
@ -1285,6 +1286,7 @@ func windowsEnroll(t *testing.T, ds fleet.Datastore, h *fleet.Host) {
|
||||
require.NoError(t, err)
|
||||
err = ds.UpdateMDMWindowsEnrollmentsHostUUID(ctx, d1.HostUUID, d1.MDMDeviceID)
|
||||
require.NoError(t, err)
|
||||
return d1.MDMDeviceID
|
||||
}
|
||||
|
||||
func testMDMWindowsProfileManagement(t *testing.T, ds *Datastore) {
|
||||
|
@ -1188,8 +1188,6 @@ func insertOnDuplicateDidUpdate(res sql.Result) bool {
|
||||
// time of the Exec call, and the result simply returns the integers it
|
||||
// already holds:
|
||||
// https://github.com/go-sql-driver/mysql/blob/bcc459a906419e2890a50fc2c99ea6dd927a88f2/result.go
|
||||
//
|
||||
// TODO(mna): would that work on mariadb too?
|
||||
|
||||
lastID, _ := res.LastInsertId()
|
||||
aff, _ := res.RowsAffected()
|
||||
|
@ -92,20 +92,18 @@ func (s *NanoMDMStorage) EnqueueDeviceLockCommand(
|
||||
return err
|
||||
}
|
||||
|
||||
// TODO(roberto): call @mna's transactionable method to update
|
||||
// these tables when it's ready.
|
||||
stmt := `
|
||||
INSERT INTO host_mdm_actions (
|
||||
host_id,
|
||||
lock_ref,
|
||||
unlock_pin
|
||||
)
|
||||
VALUES (?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
wipe_ref = NULL,
|
||||
unlock_ref = NULL,
|
||||
INSERT INTO host_mdm_actions (
|
||||
host_id,
|
||||
lock_ref,
|
||||
unlock_pin
|
||||
)
|
||||
VALUES (?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
wipe_ref = NULL,
|
||||
unlock_ref = NULL,
|
||||
unlock_pin = VALUES(unlock_pin),
|
||||
lock_ref = VALUES(lock_ref)`
|
||||
lock_ref = VALUES(lock_ref)`
|
||||
|
||||
if _, err := tx.ExecContext(ctx, stmt, host.ID, cmd.CommandUUID, pin); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "modifying host_mdm_actions for DeviceLock")
|
||||
@ -115,6 +113,30 @@ func (s *NanoMDMStorage) EnqueueDeviceLockCommand(
|
||||
}, s.logger)
|
||||
}
|
||||
|
||||
// EnqueueDeviceWipeCommand enqueues a EraseDevice command for the given host.
|
||||
func (s *NanoMDMStorage) EnqueueDeviceWipeCommand(ctx context.Context, host *fleet.Host, cmd *mdm.Command) error {
|
||||
return withRetryTxx(ctx, s.db, func(tx sqlx.ExtContext) error {
|
||||
if err := enqueueCommandDB(ctx, tx, []string{host.UUID}, cmd); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
stmt := `
|
||||
INSERT INTO host_mdm_actions (
|
||||
host_id,
|
||||
wipe_ref
|
||||
)
|
||||
VALUES (?, ?)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
wipe_ref = VALUES(wipe_ref)`
|
||||
|
||||
if _, err := tx.ExecContext(ctx, stmt, host.ID, cmd.CommandUUID); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "modifying host_mdm_actions for DeviceWipe")
|
||||
}
|
||||
|
||||
return nil
|
||||
}, s.logger)
|
||||
}
|
||||
|
||||
// NewMDMAppleDEPStorage returns a MySQL nanodep storage that uses the Datastore
|
||||
// underlying MySQL writer *sql.DB.
|
||||
func (ds *Datastore) NewMDMAppleDEPStorage(tok nanodep_client.OAuth1Tokens) (*NanoDEPStorage, error) {
|
||||
|
@ -78,7 +78,7 @@ func testEnqueueDeviceLockCommand(t *testing.T, ds *Datastore) {
|
||||
},
|
||||
}, res)
|
||||
|
||||
status, err := ds.GetHostLockWipeStatus(ctx, host.ID, "darwin")
|
||||
status, err := ds.GetHostLockWipeStatus(ctx, host)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "cmd-uuid", status.LockMDMCommand.CommandUUID)
|
||||
require.Equal(t, "123456", status.UnlockPIN)
|
||||
|
@ -138,7 +138,7 @@ func (ds *Datastore) SetHostScriptExecutionResult(ctx context.Context, result *f
|
||||
return ctxerr.Wrap(ctx, err, "lookup host script corresponding mdm action")
|
||||
}
|
||||
if refCol != "" {
|
||||
err = ds.updateHostLockWipeStatusFromResult(ctx, tx, result.HostID, refCol, result.ExitCode == 0)
|
||||
err = updateHostLockWipeStatusFromResult(ctx, tx, result.HostID, refCol, result.ExitCode == 0)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "update host mdm action based on script result")
|
||||
}
|
||||
@ -557,7 +557,7 @@ ON DUPLICATE KEY UPDATE
|
||||
})
|
||||
}
|
||||
|
||||
func (ds *Datastore) GetHostLockWipeStatus(ctx context.Context, hostID uint, fleetPlatform string) (*fleet.HostLockWipeStatus, error) {
|
||||
func (ds *Datastore) GetHostLockWipeStatus(ctx context.Context, host *fleet.Host) (*fleet.HostLockWipeStatus, error) {
|
||||
const stmt = `
|
||||
SELECT
|
||||
lock_ref,
|
||||
@ -576,11 +576,12 @@ func (ds *Datastore) GetHostLockWipeStatus(ctx context.Context, hostID uint, fle
|
||||
UnlockRef *string `db:"unlock_ref"`
|
||||
UnlockPIN *string `db:"unlock_pin"`
|
||||
}
|
||||
fleetPlatform := host.FleetPlatform()
|
||||
status := &fleet.HostLockWipeStatus{
|
||||
HostFleetPlatform: fleetPlatform,
|
||||
}
|
||||
|
||||
if err := sqlx.GetContext(ctx, ds.reader(ctx), &mdmActions, stmt, hostID); err != nil {
|
||||
if err := sqlx.GetContext(ctx, ds.reader(ctx), &mdmActions, stmt, host.ID); err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
// do not return a Not Found error, return the zero-value status, which
|
||||
// will report the correct states.
|
||||
@ -608,34 +609,22 @@ func (ds *Datastore) GetHostLockWipeStatus(ctx context.Context, hostID uint, fle
|
||||
|
||||
if mdmActions.LockRef != nil {
|
||||
// the lock reference is an MDM command
|
||||
cmd, err := ds.getMDMCommand(ctx, ds.reader(ctx), *mdmActions.LockRef)
|
||||
cmd, cmdRes, err := ds.getHostMDMAppleCommand(ctx, *mdmActions.LockRef, host.UUID)
|
||||
if err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "get lock reference MDM command")
|
||||
return nil, ctxerr.Wrap(ctx, err, "get lock reference")
|
||||
}
|
||||
status.LockMDMCommand = cmd
|
||||
status.LockMDMCommandResult = cmdRes
|
||||
}
|
||||
|
||||
// get the MDM command result, which may be not found (indicating the
|
||||
// command is pending)
|
||||
cmdRes, err := ds.GetMDMAppleCommandResults(ctx, *mdmActions.LockRef)
|
||||
if mdmActions.WipeRef != nil {
|
||||
// the wipe reference is an MDM command
|
||||
cmd, cmdRes, err := ds.getHostMDMAppleCommand(ctx, *mdmActions.WipeRef, host.UUID)
|
||||
if err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "get lock reference MDM command result")
|
||||
}
|
||||
// TODO: each item in the slice returned by
|
||||
// GetMDMAppleCommandResults is a result for a
|
||||
// different host. This only works because we're
|
||||
// enqueuing the command with the given UUID for a
|
||||
// single host, but it's the equivalent of doing
|
||||
// cmdRes[0].
|
||||
//
|
||||
// Ideally, and to be super safe, we should try to find
|
||||
// a command with a matching r.HostUUID, but we don't
|
||||
// have the host UUID available.
|
||||
for _, r := range cmdRes {
|
||||
if r.Status == fleet.MDMAppleStatusAcknowledged || r.Status == fleet.MDMAppleStatusError || r.Status == fleet.MDMAppleStatusCommandFormatError {
|
||||
status.LockMDMCommandResult = r
|
||||
break
|
||||
}
|
||||
return nil, ctxerr.Wrap(ctx, err, "get wipe reference")
|
||||
}
|
||||
status.WipeMDMCommand = cmd
|
||||
status.WipeMDMCommandResult = cmdRes
|
||||
}
|
||||
|
||||
case "windows", "linux":
|
||||
@ -655,10 +644,89 @@ func (ds *Datastore) GetHostLockWipeStatus(ctx context.Context, hostID uint, fle
|
||||
}
|
||||
status.UnlockScript = hsr
|
||||
}
|
||||
|
||||
// wipe is an MDM command on Windows, a script on Linux
|
||||
if mdmActions.WipeRef != nil {
|
||||
if fleetPlatform == "windows" {
|
||||
cmd, cmdRes, err := ds.getHostMDMWindowsCommand(ctx, *mdmActions.WipeRef, host.UUID)
|
||||
if err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "get wipe reference")
|
||||
}
|
||||
status.WipeMDMCommand = cmd
|
||||
status.WipeMDMCommandResult = cmdRes
|
||||
} else {
|
||||
hsr, err := ds.getHostScriptExecutionResultDB(ctx, ds.reader(ctx), *mdmActions.WipeRef)
|
||||
if err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "get wipe reference script result")
|
||||
}
|
||||
status.WipeScript = hsr
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return status, nil
|
||||
}
|
||||
|
||||
func (ds *Datastore) getHostMDMWindowsCommand(ctx context.Context, cmdUUID, hostUUID string) (*fleet.MDMCommand, *fleet.MDMCommandResult, error) {
|
||||
cmd, err := ds.getMDMCommand(ctx, ds.reader(ctx), cmdUUID)
|
||||
if err != nil {
|
||||
return nil, nil, ctxerr.Wrap(ctx, err, "get Windows MDM command")
|
||||
}
|
||||
|
||||
// get the MDM command result, which may be not found (indicating the command
|
||||
// is pending). Note that it doesn't return ErrNoRows if not found, it
|
||||
// returns success and an empty cmdRes slice.
|
||||
cmdResults, err := ds.GetMDMWindowsCommandResults(ctx, cmdUUID)
|
||||
if err != nil {
|
||||
return nil, nil, ctxerr.Wrap(ctx, err, "get Windows MDM command result")
|
||||
}
|
||||
|
||||
// each item in the slice returned by GetMDMWindowsCommandResults is
|
||||
// potentially a result for a different host, we need to find the one for
|
||||
// that specific host.
|
||||
var cmdRes *fleet.MDMCommandResult
|
||||
for _, r := range cmdResults {
|
||||
if r.HostUUID != hostUUID {
|
||||
continue
|
||||
}
|
||||
// all statuses for Windows indicate end of processing of the command
|
||||
// (there is no equivalent of "NotNow" or "Idle" as for Apple).
|
||||
cmdRes = r
|
||||
break
|
||||
}
|
||||
return cmd, cmdRes, nil
|
||||
}
|
||||
|
||||
func (ds *Datastore) getHostMDMAppleCommand(ctx context.Context, cmdUUID, hostUUID string) (*fleet.MDMCommand, *fleet.MDMCommandResult, error) {
|
||||
cmd, err := ds.getMDMCommand(ctx, ds.reader(ctx), cmdUUID)
|
||||
if err != nil {
|
||||
return nil, nil, ctxerr.Wrap(ctx, err, "get Apple MDM command")
|
||||
}
|
||||
|
||||
// get the MDM command result, which may be not found (indicating the command
|
||||
// is pending). Note that it doesn't return ErrNoRows if not found, it
|
||||
// returns success and an empty cmdRes slice.
|
||||
cmdResults, err := ds.GetMDMAppleCommandResults(ctx, cmdUUID)
|
||||
if err != nil {
|
||||
return nil, nil, ctxerr.Wrap(ctx, err, "get Apple MDM command result")
|
||||
}
|
||||
|
||||
// each item in the slice returned by GetMDMAppleCommandResults is
|
||||
// potentially a result for a different host, we need to find the one for
|
||||
// that specific host.
|
||||
var cmdRes *fleet.MDMCommandResult
|
||||
for _, r := range cmdResults {
|
||||
if r.HostUUID != hostUUID {
|
||||
continue
|
||||
}
|
||||
if r.Status == fleet.MDMAppleStatusAcknowledged || r.Status == fleet.MDMAppleStatusError || r.Status == fleet.MDMAppleStatusCommandFormatError {
|
||||
cmdRes = r
|
||||
break
|
||||
}
|
||||
}
|
||||
return cmd, cmdRes, nil
|
||||
}
|
||||
|
||||
// LockHostViaScript will create the script execution request and update
|
||||
// host_mdm_actions in a single transaction.
|
||||
func (ds *Datastore) LockHostViaScript(ctx context.Context, request *fleet.HostScriptRequestPayload) error {
|
||||
@ -740,6 +808,44 @@ func (ds *Datastore) UnlockHostViaScript(ctx context.Context, request *fleet.Hos
|
||||
})
|
||||
}
|
||||
|
||||
// WipeHostViaScript creates the script execution request and updates the
|
||||
// host_mdm_actions table in a single transaction.
|
||||
func (ds *Datastore) WipeHostViaScript(ctx context.Context, request *fleet.HostScriptRequestPayload) error {
|
||||
var res *fleet.HostScriptResult
|
||||
return ds.withRetryTxx(ctx, func(tx sqlx.ExtContext) error {
|
||||
var err error
|
||||
res, err = newHostScriptExecutionRequest(ctx, request, tx)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "wipe host via script create execution")
|
||||
}
|
||||
|
||||
// on duplicate we don't clear any other existing state because at this
|
||||
// point in time, this is just a request to wipe the host that is recorded,
|
||||
// it is pending execution, so if it was locked, it is still locked (so the
|
||||
// lock_ref info must still be there).
|
||||
const stmt = `
|
||||
INSERT INTO host_mdm_actions
|
||||
(
|
||||
host_id,
|
||||
wipe_ref
|
||||
)
|
||||
VALUES (?,?)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
wipe_ref = VALUES(wipe_ref)
|
||||
`
|
||||
|
||||
_, err = tx.ExecContext(ctx, stmt,
|
||||
request.HostID,
|
||||
res.ExecutionID,
|
||||
)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "wipe host via script update mdm actions")
|
||||
}
|
||||
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
func (ds *Datastore) UnlockHostManually(ctx context.Context, hostID uint, ts time.Time) error {
|
||||
const stmt = `
|
||||
INSERT INTO host_mdm_actions
|
||||
@ -758,16 +864,22 @@ func (ds *Datastore) UnlockHostManually(ctx context.Context, hostID uint, ts tim
|
||||
// entering a PIN on the device). The /unlock endpoint can be called multiple
|
||||
// times, so we record the timestamp of the first time it was requested and
|
||||
// from then on, the host is marked as "pending unlock" until the device is
|
||||
// actually unlocked with the PIN.
|
||||
// TODO(mna): to be determined how we then get notified that it has been
|
||||
// unlocked, so that it can transition to unlocked (not pending).
|
||||
// actually unlocked with the PIN. The actual unlocking happens when the
|
||||
// device sends an Idle MDM request.
|
||||
unlockRef := ts.Format(time.DateTime)
|
||||
_, err := ds.writer(ctx).ExecContext(ctx, stmt, hostID, unlockRef)
|
||||
return ctxerr.Wrap(ctx, err, "record manual unlock host request")
|
||||
}
|
||||
|
||||
func (ds *Datastore) updateHostLockWipeStatusFromResult(ctx context.Context, tx sqlx.ExtContext, hostID uint, refCol string, succeeded bool) error {
|
||||
stmt := `UPDATE host_mdm_actions SET %s WHERE host_id = ?`
|
||||
func buildHostLockWipeStatusUpdateStmt(refCol string, succeeded bool, joinPart string) string {
|
||||
var alias string
|
||||
|
||||
stmt := `UPDATE host_mdm_actions `
|
||||
if joinPart != "" {
|
||||
stmt += `hma ` + joinPart
|
||||
alias = "hma."
|
||||
}
|
||||
stmt += ` SET `
|
||||
|
||||
if succeeded {
|
||||
switch refCol {
|
||||
@ -775,23 +887,49 @@ func (ds *Datastore) updateHostLockWipeStatusFromResult(ctx context.Context, tx
|
||||
// Note that this must not clear the unlock_pin, because recording the
|
||||
// lock request does generate the PIN and store it there to be used by an
|
||||
// eventual unlock.
|
||||
stmt = fmt.Sprintf(stmt, "unlock_ref = NULL")
|
||||
stmt += fmt.Sprintf("%sunlock_ref = NULL, %[1]swipe_ref = NULL", alias)
|
||||
case "unlock_ref":
|
||||
// a successful unlock clears itself as well as the lock ref, because
|
||||
// unlock is the default state so we don't need to keep its unlock_ref
|
||||
// around once it's confirmed.
|
||||
stmt = fmt.Sprintf(stmt, "lock_ref = NULL, unlock_ref = NULL, unlock_pin = NULL")
|
||||
stmt += fmt.Sprintf("%slock_ref = NULL, %[1]sunlock_ref = NULL, %[1]sunlock_pin = NULL, %[1]swipe_ref = NULL", alias)
|
||||
case "wipe_ref":
|
||||
// TODO(mna): implement when implementing the wipe story
|
||||
default:
|
||||
return ctxerr.Errorf(ctx, "unknown reference column %q", refCol)
|
||||
stmt += fmt.Sprintf("%slock_ref = NULL, %[1]sunlock_ref = NULL, %[1]sunlock_pin = NULL", alias)
|
||||
}
|
||||
} else {
|
||||
// if the action failed, then we clear the reference to that action itself so
|
||||
// the host stays in the previous state (it doesn't transition to the new
|
||||
// state).
|
||||
stmt = fmt.Sprintf(stmt, refCol+" = NULL")
|
||||
stmt += fmt.Sprintf("%s"+refCol+" = NULL", alias)
|
||||
}
|
||||
return stmt
|
||||
}
|
||||
|
||||
func (ds *Datastore) UpdateHostLockWipeStatusFromAppleMDMResult(ctx context.Context, hostUUID, cmdUUID, requestType string, succeeded bool) error {
|
||||
// a bit of MDM protocol leaking in the mysql layer, but it's either that or
|
||||
// the other way around (MDM protocol would translate to database column)
|
||||
var refCol string
|
||||
switch requestType {
|
||||
case "EraseDevice":
|
||||
refCol = "wipe_ref"
|
||||
case "DeviceLock":
|
||||
refCol = "lock_ref"
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
return updateHostLockWipeStatusFromResultAndHostUUID(ctx, ds.writer(ctx), hostUUID, refCol, cmdUUID, succeeded)
|
||||
}
|
||||
|
||||
func updateHostLockWipeStatusFromResultAndHostUUID(ctx context.Context, tx sqlx.ExtContext, hostUUID, refCol, cmdUUID string, succeeded bool) error {
|
||||
stmt := buildHostLockWipeStatusUpdateStmt(refCol, succeeded, `JOIN hosts h ON hma.host_id = h.id`)
|
||||
stmt += ` WHERE h.uuid = ? AND hma.` + refCol + ` = ?`
|
||||
_, err := tx.ExecContext(ctx, stmt, hostUUID, cmdUUID)
|
||||
return ctxerr.Wrap(ctx, err, "update host lock/wipe status from result via host uuid")
|
||||
}
|
||||
|
||||
func updateHostLockWipeStatusFromResult(ctx context.Context, tx sqlx.ExtContext, hostID uint, refCol string, succeeded bool) error {
|
||||
stmt := buildHostLockWipeStatusUpdateStmt(refCol, succeeded, "")
|
||||
stmt += ` WHERE host_id = ?`
|
||||
_, err := tx.ExecContext(ctx, stmt, hostID)
|
||||
return ctxerr.Wrap(ctx, err, "update host lock/wipe status from result")
|
||||
}
|
||||
|
@ -11,6 +11,7 @@ import (
|
||||
"github.com/fleetdm/fleet/v4/server/fleet"
|
||||
"github.com/fleetdm/fleet/v4/server/ptr"
|
||||
"github.com/fleetdm/fleet/v4/server/test"
|
||||
"github.com/google/uuid"
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@ -29,7 +30,7 @@ func TestScripts(t *testing.T) {
|
||||
{"BatchSetScripts", testBatchSetScripts},
|
||||
{"TestLockHostViaScript", testLockHostViaScript},
|
||||
{"TestUnlockHostViaScript", testUnlockHostViaScript},
|
||||
{"TestLockUnlockViaScripts", testLockUnlockViaScripts},
|
||||
{"TestLockUnlockWipeViaScripts", testLockUnlockWipeViaScripts},
|
||||
{"TestLockUnlockManually", testLockUnlockManually},
|
||||
}
|
||||
for _, c := range cases {
|
||||
@ -735,7 +736,7 @@ func testLockHostViaScript(t *testing.T, ds *Datastore) {
|
||||
require.NoError(t, err)
|
||||
|
||||
// verify that we have created entries in host_mdm_actions and host_script_results
|
||||
status, err := ds.GetHostLockWipeStatus(ctx, windowsHostID, "windows")
|
||||
status, err := ds.GetHostLockWipeStatus(ctx, &fleet.Host{ID: windowsHostID, Platform: "windows", UUID: "uuid"})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "windows", status.HostFleetPlatform)
|
||||
require.NotNil(t, status.LockScript)
|
||||
@ -756,7 +757,7 @@ func testLockHostViaScript(t *testing.T, ds *Datastore) {
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
status, err = ds.GetHostLockWipeStatus(ctx, windowsHostID, "windows")
|
||||
status, err = ds.GetHostLockWipeStatus(ctx, &fleet.Host{ID: windowsHostID, Platform: "windows", UUID: "uuid"})
|
||||
require.NoError(t, err)
|
||||
require.True(t, status.IsLocked())
|
||||
require.False(t, status.IsPendingLock())
|
||||
@ -786,7 +787,7 @@ func testUnlockHostViaScript(t *testing.T, ds *Datastore) {
|
||||
require.NoError(t, err)
|
||||
|
||||
// verify that we have created entries in host_mdm_actions and host_script_results
|
||||
status, err := ds.GetHostLockWipeStatus(ctx, hostID, "windows")
|
||||
status, err := ds.GetHostLockWipeStatus(ctx, &fleet.Host{ID: hostID, Platform: "windows", UUID: "uuid"})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "windows", status.HostFleetPlatform)
|
||||
require.NotNil(t, status.UnlockScript)
|
||||
@ -807,14 +808,14 @@ func testUnlockHostViaScript(t *testing.T, ds *Datastore) {
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
status, err = ds.GetHostLockWipeStatus(ctx, hostID, "windows")
|
||||
status, err = ds.GetHostLockWipeStatus(ctx, &fleet.Host{ID: hostID, Platform: "windows", UUID: "uuid"})
|
||||
require.NoError(t, err)
|
||||
require.True(t, status.IsUnlocked())
|
||||
require.False(t, status.IsPendingUnlock())
|
||||
require.False(t, status.IsLocked())
|
||||
}
|
||||
|
||||
func testLockUnlockViaScripts(t *testing.T, ds *Datastore) {
|
||||
func testLockUnlockWipeViaScripts(t *testing.T, ds *Datastore) {
|
||||
ctx := context.Background()
|
||||
user := test.NewUser(t, ds, "Bob", "bob@example.com", true)
|
||||
|
||||
@ -822,7 +823,7 @@ func testLockUnlockViaScripts(t *testing.T, ds *Datastore) {
|
||||
hostID := uint(i + 1)
|
||||
|
||||
t.Run(platform, func(t *testing.T) {
|
||||
status, err := ds.GetHostLockWipeStatus(ctx, hostID, platform)
|
||||
status, err := ds.GetHostLockWipeStatus(ctx, &fleet.Host{ID: hostID, Platform: platform, UUID: "uuid"})
|
||||
require.NoError(t, err)
|
||||
|
||||
// default state
|
||||
@ -837,7 +838,7 @@ func testLockUnlockViaScripts(t *testing.T, ds *Datastore) {
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
status, err = ds.GetHostLockWipeStatus(ctx, hostID, platform)
|
||||
status, err = ds.GetHostLockWipeStatus(ctx, &fleet.Host{ID: hostID, Platform: platform, UUID: "uuid"})
|
||||
require.NoError(t, err)
|
||||
checkLockWipeState(t, status, true, false, false, false, true, false)
|
||||
|
||||
@ -849,7 +850,7 @@ func testLockUnlockViaScripts(t *testing.T, ds *Datastore) {
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
status, err = ds.GetHostLockWipeStatus(ctx, hostID, platform)
|
||||
status, err = ds.GetHostLockWipeStatus(ctx, &fleet.Host{ID: hostID, Platform: platform, UUID: "uuid"})
|
||||
require.NoError(t, err)
|
||||
checkLockWipeState(t, status, false, true, false, false, false, false)
|
||||
|
||||
@ -862,7 +863,7 @@ func testLockUnlockViaScripts(t *testing.T, ds *Datastore) {
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
status, err = ds.GetHostLockWipeStatus(ctx, hostID, platform)
|
||||
status, err = ds.GetHostLockWipeStatus(ctx, &fleet.Host{ID: hostID, Platform: platform, UUID: "uuid"})
|
||||
require.NoError(t, err)
|
||||
checkLockWipeState(t, status, false, true, false, true, false, false)
|
||||
|
||||
@ -875,7 +876,7 @@ func testLockUnlockViaScripts(t *testing.T, ds *Datastore) {
|
||||
require.NoError(t, err)
|
||||
|
||||
// still locked
|
||||
status, err = ds.GetHostLockWipeStatus(ctx, hostID, platform)
|
||||
status, err = ds.GetHostLockWipeStatus(ctx, &fleet.Host{ID: hostID, Platform: platform, UUID: "uuid"})
|
||||
require.NoError(t, err)
|
||||
checkLockWipeState(t, status, false, true, false, false, false, false)
|
||||
|
||||
@ -888,7 +889,7 @@ func testLockUnlockViaScripts(t *testing.T, ds *Datastore) {
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
status, err = ds.GetHostLockWipeStatus(ctx, hostID, platform)
|
||||
status, err = ds.GetHostLockWipeStatus(ctx, &fleet.Host{ID: hostID, Platform: platform, UUID: "uuid"})
|
||||
require.NoError(t, err)
|
||||
checkLockWipeState(t, status, false, true, false, true, false, false)
|
||||
|
||||
@ -901,7 +902,7 @@ func testLockUnlockViaScripts(t *testing.T, ds *Datastore) {
|
||||
require.NoError(t, err)
|
||||
|
||||
// host is now unlocked
|
||||
status, err = ds.GetHostLockWipeStatus(ctx, hostID, platform)
|
||||
status, err = ds.GetHostLockWipeStatus(ctx, &fleet.Host{ID: hostID, Platform: platform, UUID: "uuid"})
|
||||
require.NoError(t, err)
|
||||
checkLockWipeState(t, status, true, false, false, false, false, false)
|
||||
|
||||
@ -914,7 +915,7 @@ func testLockUnlockViaScripts(t *testing.T, ds *Datastore) {
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
status, err = ds.GetHostLockWipeStatus(ctx, hostID, platform)
|
||||
status, err = ds.GetHostLockWipeStatus(ctx, &fleet.Host{ID: hostID, Platform: platform, UUID: "uuid"})
|
||||
require.NoError(t, err)
|
||||
checkLockWipeState(t, status, true, false, false, false, true, false)
|
||||
|
||||
@ -926,9 +927,93 @@ func testLockUnlockViaScripts(t *testing.T, ds *Datastore) {
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
status, err = ds.GetHostLockWipeStatus(ctx, hostID, platform)
|
||||
status, err = ds.GetHostLockWipeStatus(ctx, &fleet.Host{ID: hostID, Platform: platform, UUID: "uuid"})
|
||||
require.NoError(t, err)
|
||||
checkLockWipeState(t, status, true, false, false, false, false, false)
|
||||
|
||||
switch platform {
|
||||
case "windows":
|
||||
// need a real MDM-enrolled host for MDM commands
|
||||
h, err := ds.NewHost(ctx, &fleet.Host{
|
||||
Hostname: "test-host-windows",
|
||||
OsqueryHostID: ptr.String("osquery-windows"),
|
||||
NodeKey: ptr.String("nodekey-windows"),
|
||||
UUID: "test-uuid-windows",
|
||||
Platform: "windows",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
windowsEnroll(t, ds, h)
|
||||
|
||||
// record a request to wipe the host
|
||||
wipeCmdUUID := uuid.NewString()
|
||||
wipeCmd := &fleet.MDMWindowsCommand{
|
||||
CommandUUID: wipeCmdUUID,
|
||||
RawCommand: []byte(`<Exec></Exec>`),
|
||||
TargetLocURI: "./Device/Vendor/MSFT/RemoteWipe/doWipeProtected",
|
||||
}
|
||||
err = ds.WipeHostViaWindowsMDM(ctx, h, wipeCmd)
|
||||
require.NoError(t, err)
|
||||
|
||||
status, err = ds.GetHostLockWipeStatus(ctx, h)
|
||||
require.NoError(t, err)
|
||||
checkLockWipeState(t, status, true, false, false, false, false, true)
|
||||
|
||||
// TODO: we don't seem to have an easy way to simulate a Windows MDM
|
||||
// protocol response, and there are lots of validations happening so we
|
||||
// can't just send a simple XML. Will test the rest via integration
|
||||
// tests.
|
||||
|
||||
case "linux":
|
||||
// record a request to wipe the host
|
||||
err = ds.WipeHostViaScript(ctx, &fleet.HostScriptRequestPayload{
|
||||
HostID: hostID,
|
||||
ScriptContents: "wipe",
|
||||
UserID: &user.ID,
|
||||
SyncRequest: false,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
status, err = ds.GetHostLockWipeStatus(ctx, &fleet.Host{ID: hostID, Platform: platform, UUID: "uuid"})
|
||||
require.NoError(t, err)
|
||||
checkLockWipeState(t, status, true, false, false, false, false, true)
|
||||
|
||||
// simulate a failed result for the wipe script execution
|
||||
_, err = ds.SetHostScriptExecutionResult(ctx, &fleet.HostScriptResultPayload{
|
||||
HostID: hostID,
|
||||
ExecutionID: status.WipeScript.ExecutionID,
|
||||
ExitCode: 1,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
status, err = ds.GetHostLockWipeStatus(ctx, &fleet.Host{ID: hostID, Platform: platform, UUID: "uuid"})
|
||||
require.NoError(t, err)
|
||||
checkLockWipeState(t, status, true, false, false, false, false, false)
|
||||
|
||||
// record another request to wipe the host
|
||||
err = ds.WipeHostViaScript(ctx, &fleet.HostScriptRequestPayload{
|
||||
HostID: hostID,
|
||||
ScriptContents: "wipe2",
|
||||
UserID: &user.ID,
|
||||
SyncRequest: false,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
status, err = ds.GetHostLockWipeStatus(ctx, &fleet.Host{ID: hostID, Platform: platform, UUID: "uuid"})
|
||||
require.NoError(t, err)
|
||||
checkLockWipeState(t, status, true, false, false, false, false, true)
|
||||
|
||||
// simulate a successful result for the wipe script execution
|
||||
_, err = ds.SetHostScriptExecutionResult(ctx, &fleet.HostScriptResultPayload{
|
||||
HostID: hostID,
|
||||
ExecutionID: status.WipeScript.ExecutionID,
|
||||
ExitCode: 0,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
status, err = ds.GetHostLockWipeStatus(ctx, &fleet.Host{ID: hostID, Platform: platform, UUID: "uuid"})
|
||||
require.NoError(t, err)
|
||||
checkLockWipeState(t, status, false, false, true, false, false, false)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -941,7 +1026,7 @@ func testLockUnlockManually(t *testing.T, ds *Datastore) {
|
||||
err := ds.UnlockHostManually(ctx, 1, twoDaysAgo)
|
||||
require.NoError(t, err)
|
||||
|
||||
status, err := ds.GetHostLockWipeStatus(ctx, 1, "darwin")
|
||||
status, err := ds.GetHostLockWipeStatus(ctx, &fleet.Host{ID: 1, Platform: "darwin", UUID: "uuid"})
|
||||
require.NoError(t, err)
|
||||
require.False(t, status.UnlockRequestedAt.IsZero())
|
||||
require.WithinDuration(t, twoDaysAgo, status.UnlockRequestedAt, 1*time.Second)
|
||||
@ -950,7 +1035,7 @@ func testLockUnlockManually(t *testing.T, ds *Datastore) {
|
||||
// requests
|
||||
err = ds.UnlockHostManually(ctx, 1, today)
|
||||
require.NoError(t, err)
|
||||
status, err = ds.GetHostLockWipeStatus(ctx, 1, "darwin")
|
||||
status, err = ds.GetHostLockWipeStatus(ctx, &fleet.Host{ID: 1, Platform: "darwin", UUID: "uuid"})
|
||||
require.NoError(t, err)
|
||||
require.False(t, status.UnlockRequestedAt.IsZero())
|
||||
require.WithinDuration(t, twoDaysAgo, status.UnlockRequestedAt, 1*time.Second)
|
||||
@ -963,7 +1048,7 @@ func testLockUnlockManually(t *testing.T, ds *Datastore) {
|
||||
})
|
||||
err = ds.UnlockHostManually(ctx, 2, today)
|
||||
require.NoError(t, err)
|
||||
status, err = ds.GetHostLockWipeStatus(ctx, 2, "darwin")
|
||||
status, err = ds.GetHostLockWipeStatus(ctx, &fleet.Host{ID: 2, Platform: "darwin", UUID: "uuid"})
|
||||
require.NoError(t, err)
|
||||
require.False(t, status.UnlockRequestedAt.IsZero())
|
||||
require.WithinDuration(t, today, status.UnlockRequestedAt, 1*time.Second)
|
||||
|
@ -83,6 +83,7 @@ var ActivityDetailsList = []ActivityDetails{
|
||||
|
||||
ActivityTypeLockedHost{},
|
||||
ActivityTypeUnlockedHost{},
|
||||
ActivityTypeWipedHost{},
|
||||
}
|
||||
|
||||
type ActivityDetails interface {
|
||||
@ -1234,6 +1235,20 @@ type ActivityTypeEditedWindowsProfile struct {
|
||||
TeamName *string `json:"team_name"`
|
||||
}
|
||||
|
||||
func (a ActivityTypeEditedWindowsProfile) ActivityName() string {
|
||||
return "edited_windows_profile"
|
||||
}
|
||||
|
||||
func (a ActivityTypeEditedWindowsProfile) Documentation() (activity, details, detailsExample string) {
|
||||
return `Generated when a user edits the Windows profiles of a team (or no team) via the fleetctl CLI.`,
|
||||
`This activity contains the following fields:
|
||||
- "team_id": The ID of the team that the profiles apply to, ` + "`null`" + ` if they apply to devices that are not in a team.
|
||||
- "team_name": The name of the team that the profiles apply to, ` + "`null`" + ` if they apply to devices that are not in a team.`, `{
|
||||
"team_id": 123,
|
||||
"team_name": "Workstations"
|
||||
}`
|
||||
}
|
||||
|
||||
type ActivityTypeLockedHost struct {
|
||||
HostID uint `json:"host_id"`
|
||||
HostDisplayName string `json:"host_display_name"`
|
||||
@ -1283,17 +1298,22 @@ func (a ActivityTypeUnlockedHost) Documentation() (activity, details, detailsExa
|
||||
}`
|
||||
}
|
||||
|
||||
func (a ActivityTypeEditedWindowsProfile) ActivityName() string {
|
||||
return "edited_windows_profile"
|
||||
type ActivityTypeWipedHost struct {
|
||||
HostID uint `json:"host_id"`
|
||||
HostDisplayName string `json:"host_display_name"`
|
||||
}
|
||||
|
||||
func (a ActivityTypeEditedWindowsProfile) Documentation() (activity, details, detailsExample string) {
|
||||
return `Generated when a user edits the Windows profiles of a team (or no team) via the fleetctl CLI.`,
|
||||
func (a ActivityTypeWipedHost) ActivityName() string {
|
||||
return "wiped_host"
|
||||
}
|
||||
|
||||
func (a ActivityTypeWipedHost) Documentation() (activity, details, detailsExample string) {
|
||||
return `Generated when a user sends a request to wipe a host.`,
|
||||
`This activity contains the following fields:
|
||||
- "team_id": The ID of the team that the profiles apply to, ` + "`null`" + ` if they apply to devices that are not in a team.
|
||||
- "team_name": The name of the team that the profiles apply to, ` + "`null`" + ` if they apply to devices that are not in a team.`, `{
|
||||
"team_id": 123,
|
||||
"team_name": "Workstations"
|
||||
- "host_id": ID of the host.
|
||||
- "host_display_name": Display name of the host.`, `{
|
||||
"host_id": 1,
|
||||
"host_display_name": "Anna's MacBook Pro"
|
||||
}`
|
||||
}
|
||||
|
||||
|
@ -19,7 +19,7 @@ type MDMAppleCommandIssuer interface {
|
||||
InstallProfile(ctx context.Context, hostUUIDs []string, profile mobileconfig.Mobileconfig, uuid string) error
|
||||
RemoveProfile(ctx context.Context, hostUUIDs []string, identifier string, uuid string) error
|
||||
DeviceLock(ctx context.Context, host *Host, uuid string) error
|
||||
EraseDevice(ctx context.Context, hostUUIDs []string, uuid string) error
|
||||
EraseDevice(ctx context.Context, host *Host, uuid string) error
|
||||
InstallEnterpriseApplication(ctx context.Context, hostUUIDs []string, uuid string, manifestURL string) error
|
||||
}
|
||||
|
||||
|
@ -1292,7 +1292,7 @@ type Datastore interface {
|
||||
BatchSetScripts(ctx context.Context, tmID *uint, scripts []*Script) error
|
||||
|
||||
// GetHostLockWipeStatus gets the lock/unlock and wipe status for the host.
|
||||
GetHostLockWipeStatus(ctx context.Context, hostID uint, fleetPlatform string) (*HostLockWipeStatus, error)
|
||||
GetHostLockWipeStatus(ctx context.Context, host *Host) (*HostLockWipeStatus, error)
|
||||
|
||||
// LockHostViaScript sends a script to lock a host and updates the
|
||||
// states in host_mdm_actions
|
||||
@ -1310,6 +1310,20 @@ type Datastore interface {
|
||||
// CleanMacOSMDMLock cleans the lock status and pin for a macOS device
|
||||
// after it has been unlocked.
|
||||
CleanMacOSMDMLock(ctx context.Context, hostUUID string) error
|
||||
|
||||
// WipeHostViaScript sends a script to wipe a host and updates the
|
||||
// states in host_mdm_actions.
|
||||
WipeHostViaScript(ctx context.Context, request *HostScriptRequestPayload) error
|
||||
|
||||
// WipeHostViaWindowsMDM sends a Windows MDM command to wipe a host and
|
||||
// updates the states in host_mdm_actions.
|
||||
WipeHostViaWindowsMDM(ctx context.Context, host *Host, cmd *MDMWindowsCommand) error
|
||||
|
||||
// UpdateHostLockWipeStatusFromAppleMDMResult updates the host_mdm_actions
|
||||
// table to reflect the result of the corresponding lock/wipe MDM command for
|
||||
// Apple hosts. It is optimized to update using only the information
|
||||
// available in the Apple MDM protocol.
|
||||
UpdateHostLockWipeStatusFromAppleMDMResult(ctx context.Context, hostUUID, cmdUUID, requestType string, succeeded bool) error
|
||||
}
|
||||
|
||||
// MDMAppleStore wraps nanomdm's storage and adds methods to deal with
|
||||
@ -1317,6 +1331,7 @@ type Datastore interface {
|
||||
type MDMAppleStore interface {
|
||||
storage.AllStorage
|
||||
EnqueueDeviceLockCommand(ctx context.Context, host *Host, cmd *mdm.Command, pin string) error
|
||||
EnqueueDeviceWipeCommand(ctx context.Context, host *Host, cmd *mdm.Command) error
|
||||
}
|
||||
|
||||
// Cloner represents any type that can clone itself. Used for the cached_mysql
|
||||
|
@ -192,7 +192,8 @@ type MDMCommandResult struct {
|
||||
HostUUID string `json:"host_uuid" db:"host_uuid"`
|
||||
// CommandUUID is the unique identifier of the command.
|
||||
CommandUUID string `json:"command_uuid" db:"command_uuid"`
|
||||
// Status is the command status. One of Acknowledged, Error, or NotNow.
|
||||
// Status is the command status. One of Acknowledged, Error, or NotNow for
|
||||
// Apple, or 200, 400, etc for Windows.
|
||||
Status string `json:"status" db:"status"`
|
||||
// UpdatedAt is the last update timestamp of the command result.
|
||||
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
|
||||
|
@ -312,7 +312,12 @@ type HostLockWipeStatus struct {
|
||||
// windows and linux hosts use a script to unlock
|
||||
UnlockScript *HostScriptResult
|
||||
|
||||
// TODO: add wipe status when implementing the Wipe story.
|
||||
// macOS and Windows use MDM commands for Wipe
|
||||
WipeMDMCommand *MDMCommand
|
||||
WipeMDMCommandResult *MDMCommandResult
|
||||
|
||||
// Linux uses a script for Wipe
|
||||
WipeScript *HostScriptResult
|
||||
}
|
||||
|
||||
func (s *HostLockWipeStatus) IsPendingLock() bool {
|
||||
@ -334,8 +339,12 @@ func (s HostLockWipeStatus) IsPendingUnlock() bool {
|
||||
}
|
||||
|
||||
func (s HostLockWipeStatus) IsPendingWipe() bool {
|
||||
// TODO(mna): implement when addressing Wipe story, for now wipe is never pending
|
||||
return false
|
||||
if s.HostFleetPlatform == "linux" {
|
||||
// pending wipe if script execution request is queued but no result yet
|
||||
return s.WipeScript != nil && s.WipeScript.ExitCode == nil
|
||||
}
|
||||
// pending wipe if an MDM command is queued but no result received yet
|
||||
return s.WipeMDMCommand != nil && s.WipeMDMCommandResult == nil
|
||||
}
|
||||
|
||||
func (s HostLockWipeStatus) IsLocked() bool {
|
||||
@ -359,6 +368,20 @@ func (s HostLockWipeStatus) IsUnlocked() bool {
|
||||
}
|
||||
|
||||
func (s HostLockWipeStatus) IsWiped() bool {
|
||||
// TODO(mna): implement when addressing Wipe story, for now never wiped
|
||||
return false
|
||||
switch s.HostFleetPlatform {
|
||||
case "linux":
|
||||
// wiped if script was sent and succeeded
|
||||
return s.WipeScript != nil && s.WipeScript.ExitCode != nil &&
|
||||
*s.WipeScript.ExitCode == 0
|
||||
case "windows":
|
||||
// wiped if an MDM command was sent and succeeded
|
||||
return s.WipeMDMCommand != nil && s.WipeMDMCommandResult != nil &&
|
||||
strings.HasPrefix(s.WipeMDMCommandResult.Status, "2")
|
||||
case "darwin":
|
||||
// wiped if an MDM command was sent and succeeded
|
||||
return s.WipeMDMCommand != nil && s.WipeMDMCommandResult != nil &&
|
||||
s.WipeMDMCommandResult.Status == MDMAppleStatusAcknowledged
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
@ -956,4 +956,5 @@ type Service interface {
|
||||
// Script-based methods (at least for some platforms, MDM-based for others)
|
||||
LockHost(ctx context.Context, hostID uint) error
|
||||
UnlockHost(ctx context.Context, hostID uint) (unlockPIN string, err error)
|
||||
WipeHost(ctx context.Context, hostID uint) error
|
||||
}
|
||||
|
@ -118,7 +118,7 @@ func (svc *MDMAppleCommander) DeviceLock(ctx context.Context, host *fleet.Host,
|
||||
return nil
|
||||
}
|
||||
|
||||
func (svc *MDMAppleCommander) EraseDevice(ctx context.Context, hostUUIDs []string, uuid string) error {
|
||||
func (svc *MDMAppleCommander) EraseDevice(ctx context.Context, host *fleet.Host, uuid string) error {
|
||||
pin := GenerateRandomPin(6)
|
||||
raw := fmt.Sprintf(`<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
@ -132,10 +132,26 @@ func (svc *MDMAppleCommander) EraseDevice(ctx context.Context, hostUUIDs []strin
|
||||
<string>EraseDevice</string>
|
||||
<key>PIN</key>
|
||||
<string>%s</string>
|
||||
<key>ObliterationBehavior</key>
|
||||
<string>Default</string>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>`, uuid, pin)
|
||||
return svc.EnqueueCommand(ctx, hostUUIDs, raw)
|
||||
|
||||
cmd, err := mdm.DecodeCommand([]byte(raw))
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "decoding command")
|
||||
}
|
||||
|
||||
if err := svc.storage.EnqueueDeviceWipeCommand(ctx, host, cmd); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "enqueuing for DeviceWipe")
|
||||
}
|
||||
|
||||
if err := svc.sendNotifications(ctx, []string{host.UUID}); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "sending notifications for DeviceWipe")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (svc *MDMAppleCommander) InstallEnterpriseApplication(ctx context.Context, hostUUIDs []string, uuid string, manifestURL string) error {
|
||||
|
@ -104,7 +104,7 @@ func TestMDMAppleCommander(t *testing.T) {
|
||||
require.True(t, mdmStorage.RetrievePushInfoFuncInvoked)
|
||||
mdmStorage.RetrievePushInfoFuncInvoked = false
|
||||
|
||||
host := &fleet.Host{ID: 1, UUID: "A"}
|
||||
host := &fleet.Host{ID: 1, UUID: "A", Platform: "darwin"}
|
||||
cmdUUID = uuid.New().String()
|
||||
mdmStorage.EnqueueDeviceLockCommandFunc = func(ctx context.Context, gotHost *fleet.Host, cmd *mdm.Command, pin string) error {
|
||||
require.NotNil(t, gotHost)
|
||||
@ -112,6 +112,7 @@ func TestMDMAppleCommander(t *testing.T) {
|
||||
require.Equal(t, host.UUID, gotHost.UUID)
|
||||
require.Equal(t, "DeviceLock", cmd.Command.RequestType)
|
||||
require.Contains(t, string(cmd.Raw), cmdUUID)
|
||||
require.Len(t, pin, 6)
|
||||
return nil
|
||||
}
|
||||
err = cmdr.DeviceLock(ctx, host, cmdUUID)
|
||||
@ -120,6 +121,22 @@ func TestMDMAppleCommander(t *testing.T) {
|
||||
mdmStorage.EnqueueDeviceLockCommandFuncInvoked = false
|
||||
require.True(t, mdmStorage.RetrievePushInfoFuncInvoked)
|
||||
mdmStorage.RetrievePushInfoFuncInvoked = false
|
||||
|
||||
cmdUUID = uuid.New().String()
|
||||
mdmStorage.EnqueueDeviceWipeCommandFunc = func(ctx context.Context, gotHost *fleet.Host, cmd *mdm.Command) error {
|
||||
require.NotNil(t, gotHost)
|
||||
require.Equal(t, host.ID, gotHost.ID)
|
||||
require.Equal(t, host.UUID, gotHost.UUID)
|
||||
require.Equal(t, "EraseDevice", cmd.Command.RequestType)
|
||||
require.Contains(t, string(cmd.Raw), cmdUUID)
|
||||
return nil
|
||||
}
|
||||
err = cmdr.EraseDevice(ctx, host, cmdUUID)
|
||||
require.NoError(t, err)
|
||||
require.True(t, mdmStorage.EnqueueDeviceWipeCommandFuncInvoked)
|
||||
mdmStorage.EnqueueDeviceWipeCommandFuncInvoked = false
|
||||
require.True(t, mdmStorage.RetrievePushInfoFuncInvoked)
|
||||
mdmStorage.RetrievePushInfoFuncInvoked = false
|
||||
}
|
||||
|
||||
func newMockAPNSPushProviderFactory() (*svcmock.APNSPushProviderFactory, *svcmock.APNSPushProvider) {
|
||||
|
@ -56,6 +56,8 @@ type RetrieveTokenUpdateTallyFunc func(ctx context.Context, id string) (int, err
|
||||
|
||||
type EnqueueDeviceLockCommandFunc func(ctx context.Context, host *fleet.Host, cmd *mdm.Command, pin string) error
|
||||
|
||||
type EnqueueDeviceWipeCommandFunc func(ctx context.Context, host *fleet.Host, cmd *mdm.Command) error
|
||||
|
||||
type MDMAppleStore struct {
|
||||
StoreAuthenticateFunc StoreAuthenticateFunc
|
||||
StoreAuthenticateFuncInvoked bool
|
||||
@ -120,6 +122,9 @@ type MDMAppleStore struct {
|
||||
EnqueueDeviceLockCommandFunc EnqueueDeviceLockCommandFunc
|
||||
EnqueueDeviceLockCommandFuncInvoked bool
|
||||
|
||||
EnqueueDeviceWipeCommandFunc EnqueueDeviceWipeCommandFunc
|
||||
EnqueueDeviceWipeCommandFuncInvoked bool
|
||||
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
@ -269,3 +274,10 @@ func (fs *MDMAppleStore) EnqueueDeviceLockCommand(ctx context.Context, host *fle
|
||||
fs.mu.Unlock()
|
||||
return fs.EnqueueDeviceLockCommandFunc(ctx, host, cmd, pin)
|
||||
}
|
||||
|
||||
func (fs *MDMAppleStore) EnqueueDeviceWipeCommand(ctx context.Context, host *fleet.Host, cmd *mdm.Command) error {
|
||||
fs.mu.Lock()
|
||||
fs.EnqueueDeviceWipeCommandFuncInvoked = true
|
||||
fs.mu.Unlock()
|
||||
return fs.EnqueueDeviceWipeCommandFunc(ctx, host, cmd)
|
||||
}
|
||||
|
@ -828,7 +828,7 @@ type GetHostScriptDetailsFunc func(ctx context.Context, hostID uint, teamID *uin
|
||||
|
||||
type BatchSetScriptsFunc func(ctx context.Context, tmID *uint, scripts []*fleet.Script) error
|
||||
|
||||
type GetHostLockWipeStatusFunc func(ctx context.Context, hostID uint, fleetPlatform string) (*fleet.HostLockWipeStatus, error)
|
||||
type GetHostLockWipeStatusFunc func(ctx context.Context, host *fleet.Host) (*fleet.HostLockWipeStatus, error)
|
||||
|
||||
type LockHostViaScriptFunc func(ctx context.Context, request *fleet.HostScriptRequestPayload) error
|
||||
|
||||
@ -838,6 +838,12 @@ type UnlockHostManuallyFunc func(ctx context.Context, hostID uint, ts time.Time)
|
||||
|
||||
type CleanMacOSMDMLockFunc func(ctx context.Context, hostUUID string) error
|
||||
|
||||
type WipeHostViaScriptFunc func(ctx context.Context, request *fleet.HostScriptRequestPayload) error
|
||||
|
||||
type WipeHostViaWindowsMDMFunc func(ctx context.Context, host *fleet.Host, cmd *fleet.MDMWindowsCommand) error
|
||||
|
||||
type UpdateHostLockWipeStatusFromAppleMDMResultFunc func(ctx context.Context, hostUUID string, cmdUUID string, requestType string, succeeded bool) error
|
||||
|
||||
type DataStore struct {
|
||||
HealthCheckFunc HealthCheckFunc
|
||||
HealthCheckFuncInvoked bool
|
||||
@ -2069,6 +2075,15 @@ type DataStore struct {
|
||||
CleanMacOSMDMLockFunc CleanMacOSMDMLockFunc
|
||||
CleanMacOSMDMLockFuncInvoked bool
|
||||
|
||||
WipeHostViaScriptFunc WipeHostViaScriptFunc
|
||||
WipeHostViaScriptFuncInvoked bool
|
||||
|
||||
WipeHostViaWindowsMDMFunc WipeHostViaWindowsMDMFunc
|
||||
WipeHostViaWindowsMDMFuncInvoked bool
|
||||
|
||||
UpdateHostLockWipeStatusFromAppleMDMResultFunc UpdateHostLockWipeStatusFromAppleMDMResultFunc
|
||||
UpdateHostLockWipeStatusFromAppleMDMResultFuncInvoked bool
|
||||
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
@ -4907,11 +4922,11 @@ func (s *DataStore) BatchSetScripts(ctx context.Context, tmID *uint, scripts []*
|
||||
return s.BatchSetScriptsFunc(ctx, tmID, scripts)
|
||||
}
|
||||
|
||||
func (s *DataStore) GetHostLockWipeStatus(ctx context.Context, hostID uint, fleetPlatform string) (*fleet.HostLockWipeStatus, error) {
|
||||
func (s *DataStore) GetHostLockWipeStatus(ctx context.Context, host *fleet.Host) (*fleet.HostLockWipeStatus, error) {
|
||||
s.mu.Lock()
|
||||
s.GetHostLockWipeStatusFuncInvoked = true
|
||||
s.mu.Unlock()
|
||||
return s.GetHostLockWipeStatusFunc(ctx, hostID, fleetPlatform)
|
||||
return s.GetHostLockWipeStatusFunc(ctx, host)
|
||||
}
|
||||
|
||||
func (s *DataStore) LockHostViaScript(ctx context.Context, request *fleet.HostScriptRequestPayload) error {
|
||||
@ -4941,3 +4956,24 @@ func (s *DataStore) CleanMacOSMDMLock(ctx context.Context, hostUUID string) erro
|
||||
s.mu.Unlock()
|
||||
return s.CleanMacOSMDMLockFunc(ctx, hostUUID)
|
||||
}
|
||||
|
||||
func (s *DataStore) WipeHostViaScript(ctx context.Context, request *fleet.HostScriptRequestPayload) error {
|
||||
s.mu.Lock()
|
||||
s.WipeHostViaScriptFuncInvoked = true
|
||||
s.mu.Unlock()
|
||||
return s.WipeHostViaScriptFunc(ctx, request)
|
||||
}
|
||||
|
||||
func (s *DataStore) WipeHostViaWindowsMDM(ctx context.Context, host *fleet.Host, cmd *fleet.MDMWindowsCommand) error {
|
||||
s.mu.Lock()
|
||||
s.WipeHostViaWindowsMDMFuncInvoked = true
|
||||
s.mu.Unlock()
|
||||
return s.WipeHostViaWindowsMDMFunc(ctx, host, cmd)
|
||||
}
|
||||
|
||||
func (s *DataStore) UpdateHostLockWipeStatusFromAppleMDMResult(ctx context.Context, hostUUID string, cmdUUID string, requestType string, succeeded bool) error {
|
||||
s.mu.Lock()
|
||||
s.UpdateHostLockWipeStatusFromAppleMDMResultFuncInvoked = true
|
||||
s.mu.Unlock()
|
||||
return s.UpdateHostLockWipeStatusFromAppleMDMResultFunc(ctx, hostUUID, cmdUUID, requestType, succeeded)
|
||||
}
|
||||
|
@ -2400,6 +2400,13 @@ func (svc *MDMAppleCheckinAndCommandService) CommandAndReportResults(r *mdm.Requ
|
||||
Detail: apple_mdm.FmtErrorChain(cmdResult.ErrorChain),
|
||||
OperationType: fleet.MDMOperationTypeRemove,
|
||||
})
|
||||
case "DeviceLock", "EraseDevice":
|
||||
// call into our datastore to update host_mdm_actions if the status is terminal
|
||||
if cmdResult.Status == fleet.MDMAppleStatusAcknowledged ||
|
||||
cmdResult.Status == fleet.MDMAppleStatusError ||
|
||||
cmdResult.Status == fleet.MDMAppleStatusCommandFormatError {
|
||||
return nil, svc.ds.UpdateHostLockWipeStatusFromAppleMDMResult(r.Context, cmdResult.UDID, cmdResult.CommandUUID, requestType, cmdResult.Status == fleet.MDMAppleStatusAcknowledged)
|
||||
}
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
@ -760,7 +760,7 @@ func TestHostDetailsMDMProfiles(t *testing.T) {
|
||||
ds.GetHostMDMMacOSSetupFunc = func(ctx context.Context, hostID uint) (*fleet.HostMDMMacOSSetup, error) {
|
||||
return nil, nil
|
||||
}
|
||||
ds.GetHostLockWipeStatusFunc = func(ctx context.Context, hostID uint, fleetPlatform string) (*fleet.HostLockWipeStatus, error) {
|
||||
ds.GetHostLockWipeStatusFunc = func(ctx context.Context, host *fleet.Host) (*fleet.HostLockWipeStatus, error) {
|
||||
return &fleet.HostLockWipeStatus{}, nil
|
||||
}
|
||||
|
||||
|
@ -479,6 +479,7 @@ func attachFleetAPIRoutes(r *mux.Router, svc fleet.Service, config config.FleetC
|
||||
ue.GET("/api/_version_/fleet/hosts/{id:[0-9]+}/activities", listHostPastActivitiesEndpoint, listHostPastActivitiesRequest{})
|
||||
ue.POST("/api/_version_/fleet/hosts/{id:[0-9]+}/lock", lockHostEndpoint, lockHostRequest{})
|
||||
ue.POST("/api/_version_/fleet/hosts/{id:[0-9]+}/unlock", unlockHostEndpoint, unlockHostRequest{})
|
||||
ue.POST("/api/_version_/fleet/hosts/{id:[0-9]+}/wipe", wipeHostEndpoint, wipeHostRequest{})
|
||||
|
||||
// Only Fleet MDM specific endpoints should be within the root /mdm/ path.
|
||||
// NOTE: remember to update
|
||||
|
@ -1093,7 +1093,7 @@ func (svc *Service) getHostDetails(ctx context.Context, host *fleet.Host, opts f
|
||||
}
|
||||
host.MDM.MacOSSetup = macOSSetup
|
||||
|
||||
mdmActions, err := svc.ds.GetHostLockWipeStatus(ctx, host.ID, host.FleetPlatform())
|
||||
mdmActions, err := svc.ds.GetHostLockWipeStatus(ctx, host)
|
||||
if err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "get host mdm lock/wipe status")
|
||||
}
|
||||
@ -1104,10 +1104,10 @@ func (svc *Service) getHostDetails(ctx context.Context, host *fleet.Host, opts f
|
||||
host.MDM.PendingAction = ptr.String("")
|
||||
// device status
|
||||
switch {
|
||||
case mdmActions.IsLocked():
|
||||
host.MDM.DeviceStatus = ptr.String("locked")
|
||||
case mdmActions.IsWiped():
|
||||
host.MDM.DeviceStatus = ptr.String("wiped")
|
||||
case mdmActions.IsLocked():
|
||||
host.MDM.DeviceStatus = ptr.String("locked")
|
||||
}
|
||||
|
||||
// pending action, if any
|
||||
|
@ -67,7 +67,7 @@ func TestHostDetails(t *testing.T) {
|
||||
ds.ListHostBatteriesFunc = func(ctx context.Context, hostID uint) ([]*fleet.HostBattery, error) {
|
||||
return dsBats, nil
|
||||
}
|
||||
ds.GetHostLockWipeStatusFunc = func(ctx context.Context, hostID uint, fleetPlatform string) (*fleet.HostLockWipeStatus, error) {
|
||||
ds.GetHostLockWipeStatusFunc = func(ctx context.Context, host *fleet.Host) (*fleet.HostLockWipeStatus, error) {
|
||||
return &fleet.HostLockWipeStatus{}, nil
|
||||
}
|
||||
// Health should be replaced at the service layer with custom values determined by the cycle count. See https://github.com/fleetdm/fleet/issues/6763.
|
||||
@ -108,7 +108,7 @@ func TestHostDetailsMDMAppleDiskEncryption(t *testing.T) {
|
||||
ds.ListHostBatteriesFunc = func(ctx context.Context, hostID uint) ([]*fleet.HostBattery, error) {
|
||||
return nil, nil
|
||||
}
|
||||
ds.GetHostLockWipeStatusFunc = func(ctx context.Context, hostID uint, fleetPlatform string) (*fleet.HostLockWipeStatus, error) {
|
||||
ds.GetHostLockWipeStatusFunc = func(ctx context.Context, host *fleet.Host) (*fleet.HostLockWipeStatus, error) {
|
||||
return &fleet.HostLockWipeStatus{}, nil
|
||||
}
|
||||
|
||||
@ -385,7 +385,7 @@ func TestHostDetailsOSSettings(t *testing.T) {
|
||||
ds.GetHostMDMMacOSSetupFunc = func(ctx context.Context, hid uint) (*fleet.HostMDMMacOSSetup, error) {
|
||||
return nil, nil
|
||||
}
|
||||
ds.GetHostLockWipeStatusFunc = func(ctx context.Context, hostID uint, fleetPlatform string) (*fleet.HostLockWipeStatus, error) {
|
||||
ds.GetHostLockWipeStatusFunc = func(ctx context.Context, host *fleet.Host) (*fleet.HostLockWipeStatus, error) {
|
||||
return &fleet.HostLockWipeStatus{}, nil
|
||||
}
|
||||
|
||||
@ -497,7 +497,7 @@ func TestHostDetailsOSSettingsWindowsOnly(t *testing.T) {
|
||||
ds.GetHostMDMWindowsProfilesFunc = func(ctx context.Context, uuid string) ([]fleet.HostMDMWindowsProfile, error) {
|
||||
return nil, nil
|
||||
}
|
||||
ds.GetHostLockWipeStatusFunc = func(ctx context.Context, hostID uint, fleetPlatform string) (*fleet.HostLockWipeStatus, error) {
|
||||
ds.GetHostLockWipeStatusFunc = func(ctx context.Context, host *fleet.Host) (*fleet.HostLockWipeStatus, error) {
|
||||
return &fleet.HostLockWipeStatus{}, nil
|
||||
}
|
||||
|
||||
@ -600,7 +600,7 @@ func TestHostAuth(t *testing.T) {
|
||||
ds.ListHostUpcomingActivitiesFunc = func(ctx context.Context, hostID uint, opt fleet.ListOptions) ([]*fleet.Activity, *fleet.PaginationMetadata, error) {
|
||||
return nil, nil, nil
|
||||
}
|
||||
ds.GetHostLockWipeStatusFunc = func(ctx context.Context, hostID uint, fleetPlatform string) (*fleet.HostLockWipeStatus, error) {
|
||||
ds.GetHostLockWipeStatusFunc = func(ctx context.Context, host *fleet.Host) (*fleet.HostLockWipeStatus, error) {
|
||||
return &fleet.HostLockWipeStatus{}, nil
|
||||
}
|
||||
|
||||
@ -1383,7 +1383,7 @@ func TestHostMDMProfileDetail(t *testing.T) {
|
||||
ds.GetHostMDMMacOSSetupFunc = func(ctx context.Context, hid uint) (*fleet.HostMDMMacOSSetup, error) {
|
||||
return nil, nil
|
||||
}
|
||||
ds.GetHostLockWipeStatusFunc = func(ctx context.Context, hostID uint, fleetPlatform string) (*fleet.HostLockWipeStatus, error) {
|
||||
ds.GetHostLockWipeStatusFunc = func(ctx context.Context, host *fleet.Host) (*fleet.HostLockWipeStatus, error) {
|
||||
return &fleet.HostLockWipeStatus{}, nil
|
||||
}
|
||||
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
|
||||
@ -1445,15 +1445,20 @@ func TestHostMDMProfileDetail(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestLockUnlockHostAuth(t *testing.T) {
|
||||
func TestLockUnlockWipeHostAuth(t *testing.T) {
|
||||
ds := new(mock.Store)
|
||||
svc, ctx := newTestService(t, ds, nil, nil, &TestServerOpts{License: &fleet.LicenseInfo{Tier: fleet.TierPremium}})
|
||||
|
||||
const (
|
||||
teamHostID = 1
|
||||
globalHostID = 2
|
||||
)
|
||||
|
||||
teamHost := &fleet.Host{TeamID: ptr.Uint(1), Platform: "darwin"}
|
||||
globalHost := &fleet.Host{Platform: "darwin"}
|
||||
|
||||
ds.HostByIdentifierFunc = func(ctx context.Context, identifier string) (*fleet.Host, error) {
|
||||
if identifier == "1" {
|
||||
if identifier == fmt.Sprint(teamHostID) {
|
||||
return teamHost, nil
|
||||
}
|
||||
|
||||
@ -1483,14 +1488,14 @@ func TestLockUnlockHostAuth(t *testing.T) {
|
||||
ds.GetHostMDMMacOSSetupFunc = func(ctx context.Context, hostID uint) (*fleet.HostMDMMacOSSetup, error) {
|
||||
return nil, nil
|
||||
}
|
||||
ds.GetHostLockWipeStatusFunc = func(ctx context.Context, hostID uint, fleetPlatform string) (*fleet.HostLockWipeStatus, error) {
|
||||
ds.GetHostLockWipeStatusFunc = func(ctx context.Context, host *fleet.Host) (*fleet.HostLockWipeStatus, error) {
|
||||
return &fleet.HostLockWipeStatus{}, nil
|
||||
}
|
||||
ds.LockHostViaScriptFunc = func(ctx context.Context, request *fleet.HostScriptRequestPayload) error {
|
||||
return nil
|
||||
}
|
||||
ds.HostLiteFunc = func(ctx context.Context, hostID uint) (*fleet.Host, error) {
|
||||
if hostID == 1 {
|
||||
if hostID == teamHostID {
|
||||
return teamHost, nil
|
||||
}
|
||||
|
||||
@ -1596,25 +1601,30 @@ func TestLockUnlockHostAuth(t *testing.T) {
|
||||
}
|
||||
ctx := viewer.NewContext(ctx, viewer.Viewer{User: tt.user})
|
||||
|
||||
err := svc.LockHost(ctx, 2)
|
||||
err := svc.LockHost(ctx, globalHostID)
|
||||
checkAuthErr(t, tt.shouldFailGlobalWrite, err)
|
||||
err = svc.LockHost(ctx, 1)
|
||||
err = svc.LockHost(ctx, teamHostID)
|
||||
checkAuthErr(t, tt.shouldFailTeamWrite, err)
|
||||
|
||||
// Pretend we locked the host
|
||||
ds.GetHostLockWipeStatusFunc = func(ctx context.Context, hostID uint, fleetPlatform string) (*fleet.HostLockWipeStatus, error) {
|
||||
return &fleet.HostLockWipeStatus{HostFleetPlatform: fleetPlatform, LockMDMCommand: &fleet.MDMCommand{}, LockMDMCommandResult: &fleet.MDMCommandResult{Status: fleet.MDMAppleStatusAcknowledged}}, nil
|
||||
ds.GetHostLockWipeStatusFunc = func(ctx context.Context, host *fleet.Host) (*fleet.HostLockWipeStatus, error) {
|
||||
return &fleet.HostLockWipeStatus{HostFleetPlatform: host.FleetPlatform(), LockMDMCommand: &fleet.MDMCommand{}, LockMDMCommandResult: &fleet.MDMCommandResult{Status: fleet.MDMAppleStatusAcknowledged}}, nil
|
||||
}
|
||||
|
||||
_, err = svc.UnlockHost(ctx, 2)
|
||||
_, err = svc.UnlockHost(ctx, globalHostID)
|
||||
checkAuthErr(t, tt.shouldFailGlobalWrite, err)
|
||||
_, err = svc.UnlockHost(ctx, 1)
|
||||
_, err = svc.UnlockHost(ctx, teamHostID)
|
||||
checkAuthErr(t, tt.shouldFailTeamWrite, err)
|
||||
|
||||
// Reset so we're now pretending host is unlocked
|
||||
ds.GetHostLockWipeStatusFunc = func(ctx context.Context, hostID uint, fleetPlatform string) (*fleet.HostLockWipeStatus, error) {
|
||||
ds.GetHostLockWipeStatusFunc = func(ctx context.Context, host *fleet.Host) (*fleet.HostLockWipeStatus, error) {
|
||||
return &fleet.HostLockWipeStatus{}, nil
|
||||
}
|
||||
|
||||
err = svc.WipeHost(ctx, globalHostID)
|
||||
checkAuthErr(t, tt.shouldFailGlobalWrite, err)
|
||||
err = svc.WipeHost(ctx, teamHostID)
|
||||
checkAuthErr(t, tt.shouldFailTeamWrite, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -5350,9 +5350,10 @@ func (s *integrationTestSuite) TestPremiumEndpointsWithoutLicense() {
|
||||
"team_id", "1",
|
||||
)
|
||||
|
||||
// lock/unlock a host
|
||||
// lock/unlock/wipe a host
|
||||
s.Do("POST", "/api/v1/fleet/hosts/123/lock", nil, http.StatusPaymentRequired)
|
||||
s.Do("POST", "/api/v1/fleet/hosts/123/unlock", nil, http.StatusPaymentRequired)
|
||||
s.Do("POST", "/api/v1/fleet/hosts/123/wipe", nil, http.StatusPaymentRequired)
|
||||
}
|
||||
|
||||
func (s *integrationTestSuite) TestScriptsEndpointsWithoutLicense() {
|
||||
|
@ -6866,7 +6866,7 @@ func (s *integrationEnterpriseTestSuite) TestAllSoftwareTitles() {
|
||||
|
||||
}
|
||||
|
||||
func (s *integrationEnterpriseTestSuite) TestLockUnlockWindowsLinux() {
|
||||
func (s *integrationEnterpriseTestSuite) TestLockUnlockWipeWindowsLinux() {
|
||||
ctx := context.Background()
|
||||
t := s.T()
|
||||
|
||||
@ -6888,19 +6888,22 @@ func (s *integrationEnterpriseTestSuite) TestLockUnlockWindowsLinux() {
|
||||
require.NotNil(t, getHostResp.Host.MDM.PendingAction)
|
||||
require.Equal(t, "", *getHostResp.Host.MDM.PendingAction)
|
||||
|
||||
// try to lock/unlock the Windows host, fails because Windows MDM must be enabled
|
||||
// try to lock/unlock/wipe the Windows host, fails because Windows MDM must be enabled
|
||||
res := s.DoRaw("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/lock", winHost.ID), nil, http.StatusBadRequest)
|
||||
errMsg := extractServerErrorText(res.Body)
|
||||
require.Contains(t, errMsg, "Windows MDM isn't turned on.")
|
||||
res = s.DoRaw("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/unlock", winHost.ID), nil, http.StatusBadRequest)
|
||||
errMsg = extractServerErrorText(res.Body)
|
||||
require.Contains(t, errMsg, "Windows MDM isn't turned on.")
|
||||
res = s.DoRaw("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/wipe", winHost.ID), nil, http.StatusBadRequest)
|
||||
errMsg = extractServerErrorText(res.Body)
|
||||
require.Contains(t, errMsg, "Windows MDM isn't turned on.")
|
||||
|
||||
// try to lock/unlock the Linux host succeeds, no MDM constraints
|
||||
// try to lock/unlock/wipe the Linux host succeeds, no MDM constraints
|
||||
s.Do("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/lock", linuxHost.ID), nil, http.StatusNoContent)
|
||||
|
||||
// simulate a successful script result for the lock command
|
||||
status, err := s.ds.GetHostLockWipeStatus(ctx, linuxHost.ID, linuxHost.FleetPlatform())
|
||||
status, err := s.ds.GetHostLockWipeStatus(ctx, linuxHost)
|
||||
require.NoError(t, err)
|
||||
|
||||
var orbitScriptResp orbitPostScriptResultResponse
|
||||
@ -6922,6 +6925,12 @@ func (s *integrationEnterpriseTestSuite) TestLockUnlockWindowsLinux() {
|
||||
require.Equal(t, "locked", *getHostResp.Host.MDM.DeviceStatus)
|
||||
require.NotNil(t, getHostResp.Host.MDM.PendingAction)
|
||||
require.Equal(t, "unlock", *getHostResp.Host.MDM.PendingAction)
|
||||
|
||||
// attempting to Wipe the linux host fails due to pending unlock, not because
|
||||
// of MDM not enabled
|
||||
res = s.DoRaw("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/wipe", linuxHost.ID), nil, http.StatusUnprocessableEntity)
|
||||
errMsg = extractServerErrorText(res.Body)
|
||||
require.Contains(t, errMsg, "Host cannot be wiped until unlock is complete.")
|
||||
}
|
||||
|
||||
// checks that the specified team/no-team has the Windows OS Updates profile with
|
||||
|
@ -11009,12 +11009,15 @@ func (s *integrationMDMTestSuite) TestManualEnrollmentCommands() {
|
||||
checkInstallFleetdCommandSent(mdmDevice, false)
|
||||
}
|
||||
|
||||
func (s *integrationMDMTestSuite) TestLockUnlockWindowsLinux() {
|
||||
func (s *integrationMDMTestSuite) TestLockUnlockWipeWindowsLinux() {
|
||||
t := s.T()
|
||||
ctx := context.Background()
|
||||
|
||||
// create an MDM-enrolled Windows host
|
||||
winHost, _ := createWindowsHostThenEnrollMDM(s.ds, s.server.URL, t)
|
||||
winHost, winMDMClient := createWindowsHostThenEnrollMDM(s.ds, s.server.URL, t)
|
||||
// set its MDM data so it shows as MDM-enrolled in the backend
|
||||
err := s.ds.SetOrUpdateMDMData(ctx, winHost.ID, false, true, s.server.URL, false, fleet.WellKnownMDMFleet, "")
|
||||
require.NoError(t, err)
|
||||
linuxHost := createOrbitEnrolledHost(t, "linux", "lock_unlock_linux", s.ds)
|
||||
|
||||
for _, host := range []*fleet.Host{winHost, linuxHost} {
|
||||
@ -11047,7 +11050,7 @@ func (s *integrationMDMTestSuite) TestLockUnlockWindowsLinux() {
|
||||
require.Contains(t, errMsg, "Host has pending lock request.")
|
||||
|
||||
// simulate a successful script result for the lock command
|
||||
status, err := s.ds.GetHostLockWipeStatus(ctx, host.ID, host.FleetPlatform())
|
||||
status, err := s.ds.GetHostLockWipeStatus(ctx, host)
|
||||
require.NoError(t, err)
|
||||
|
||||
var orbitScriptResp orbitPostScriptResultResponse
|
||||
@ -11081,7 +11084,7 @@ func (s *integrationMDMTestSuite) TestLockUnlockWindowsLinux() {
|
||||
require.Contains(t, errMsg, "Host has pending unlock request.")
|
||||
|
||||
// simulate a failed script result for the unlock command
|
||||
status, err = s.ds.GetHostLockWipeStatus(ctx, host.ID, host.FleetPlatform())
|
||||
status, err = s.ds.GetHostLockWipeStatus(ctx, host)
|
||||
require.NoError(t, err)
|
||||
|
||||
s.DoJSON("POST", "/api/fleet/orbit/scripts/result",
|
||||
@ -11094,10 +11097,219 @@ func (s *integrationMDMTestSuite) TestLockUnlockWindowsLinux() {
|
||||
require.Equal(t, "locked", *getHostResp.Host.MDM.DeviceStatus)
|
||||
require.NotNil(t, getHostResp.Host.MDM.PendingAction)
|
||||
require.Equal(t, "", *getHostResp.Host.MDM.PendingAction)
|
||||
|
||||
// unlock the host, simulate success
|
||||
s.Do("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/unlock", host.ID), nil, http.StatusNoContent)
|
||||
status, err = s.ds.GetHostLockWipeStatus(ctx, host)
|
||||
require.NoError(t, err)
|
||||
s.DoJSON("POST", "/api/fleet/orbit/scripts/result",
|
||||
json.RawMessage(fmt.Sprintf(`{"orbit_node_key": %q, "execution_id": %q, "exit_code": 0, "output": "ok"}`, *host.OrbitNodeKey, status.UnlockScript.ExecutionID)),
|
||||
http.StatusOK, &orbitScriptResp)
|
||||
|
||||
// refresh the host's status, it is unlocked, no pending action
|
||||
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
|
||||
s.Do("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/wipe", host.ID), nil, http.StatusNoContent)
|
||||
wipeActID := s.lastActivityOfTypeMatches(fleet.ActivityTypeWipedHost{}.ActivityName(), fmt.Sprintf(`{"host_id": %d, "host_display_name": %q}`, host.ID, host.DisplayName()), 0)
|
||||
|
||||
// try to wipe the host again, already have it pending
|
||||
res = s.DoRaw("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/wipe", host.ID), nil, http.StatusUnprocessableEntity)
|
||||
errMsg = extractServerErrorText(res.Body)
|
||||
require.Contains(t, errMsg, "Host has pending wipe request.")
|
||||
// no activity created
|
||||
s.lastActivityOfTypeMatches(fleet.ActivityTypeWipedHost{}.ActivityName(), fmt.Sprintf(`{"host_id": %d, "host_display_name": %q}`, host.ID, host.DisplayName()), wipeActID)
|
||||
|
||||
// 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, "unlocked", *getHostResp.Host.MDM.DeviceStatus)
|
||||
require.NotNil(t, getHostResp.Host.MDM.PendingAction)
|
||||
require.Equal(t, "wipe", *getHostResp.Host.MDM.PendingAction)
|
||||
|
||||
status, err = s.ds.GetHostLockWipeStatus(ctx, host)
|
||||
require.NoError(t, err)
|
||||
if host.FleetPlatform() == "linux" {
|
||||
// simulate a successful wipe for the Linux host's script response
|
||||
s.DoJSON("POST", "/api/fleet/orbit/scripts/result",
|
||||
json.RawMessage(fmt.Sprintf(`{"orbit_node_key": %q, "execution_id": %q, "exit_code": 0, "output": "ok"}`, *host.OrbitNodeKey, status.WipeScript.ExecutionID)),
|
||||
http.StatusOK, &orbitScriptResp)
|
||||
} else {
|
||||
// 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: mdm_types.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, "wiped", *getHostResp.Host.MDM.DeviceStatus)
|
||||
require.NotNil(t, getHostResp.Host.MDM.PendingAction)
|
||||
require.Equal(t, "", *getHostResp.Host.MDM.PendingAction)
|
||||
|
||||
// try to wipe the host again, conflict (already wiped)
|
||||
s.Do("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/wipe", host.ID), nil, http.StatusConflict)
|
||||
// no activity created
|
||||
s.lastActivityOfTypeMatches(fleet.ActivityTypeWipedHost{}.ActivityName(), fmt.Sprintf(`{"host_id": %d, "host_display_name": %q}`, host.ID, host.DisplayName()), wipeActID)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (s *integrationMDMTestSuite) TestLockUnlockWipeMacOS() {
|
||||
t := s.T()
|
||||
host, mdmClient := createHostThenEnrollMDM(s.ds, s.server.URL, t)
|
||||
|
||||
// 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)
|
||||
|
||||
// try to unlock the host (which is already its status)
|
||||
var unlockResp unlockHostResponse
|
||||
s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/unlock", host.ID), nil, http.StatusConflict, &unlockResp)
|
||||
|
||||
// lock the host
|
||||
s.Do("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/lock", host.ID), nil, http.StatusNoContent)
|
||||
|
||||
// refresh the host's status, it is now pending lock
|
||||
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, "lock", *getHostResp.Host.MDM.PendingAction)
|
||||
|
||||
// try locking the host while it is pending lock fails
|
||||
res := s.DoRaw("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/lock", host.ID), nil, http.StatusUnprocessableEntity)
|
||||
errMsg := extractServerErrorText(res.Body)
|
||||
require.Contains(t, errMsg, "Host has pending lock request.")
|
||||
|
||||
// simulate a successful MDM result for the lock command
|
||||
cmd, err := mdmClient.Idle()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, cmd)
|
||||
require.Equal(t, "DeviceLock", cmd.Command.RequestType)
|
||||
cmd, err = mdmClient.Acknowledge(cmd.CommandUUID)
|
||||
require.NoError(t, err)
|
||||
|
||||
// refresh the host's status, it is now locked
|
||||
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, "locked", *getHostResp.Host.MDM.DeviceStatus)
|
||||
require.NotNil(t, getHostResp.Host.MDM.PendingAction)
|
||||
require.Equal(t, "", *getHostResp.Host.MDM.PendingAction)
|
||||
|
||||
// try to lock the host again
|
||||
s.Do("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/lock", host.ID), nil, http.StatusConflict)
|
||||
|
||||
// unlock the host
|
||||
unlockResp = unlockHostResponse{}
|
||||
s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/unlock", host.ID), nil, http.StatusOK, &unlockResp)
|
||||
require.NotNil(t, unlockResp.HostID)
|
||||
require.Equal(t, host.ID, *unlockResp.HostID)
|
||||
require.Len(t, unlockResp.UnlockPIN, 6)
|
||||
unlockPIN := unlockResp.UnlockPIN
|
||||
unlockActID := s.lastActivityOfTypeMatches(fleet.ActivityTypeUnlockedHost{}.ActivityName(),
|
||||
fmt.Sprintf(`{"host_id": %d, "host_display_name": %q, "host_platform": %q}`, host.ID, host.DisplayName(), host.FleetPlatform()), 0)
|
||||
|
||||
// refresh the host's status, it is locked pending unlock
|
||||
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, "locked", *getHostResp.Host.MDM.DeviceStatus)
|
||||
require.NotNil(t, getHostResp.Host.MDM.PendingAction)
|
||||
require.Equal(t, "unlock", *getHostResp.Host.MDM.PendingAction)
|
||||
|
||||
// try unlocking the host again simply returns the PIN again
|
||||
unlockResp = unlockHostResponse{}
|
||||
s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/unlock", host.ID), nil, http.StatusOK, &unlockResp)
|
||||
require.Equal(t, unlockPIN, unlockResp.UnlockPIN)
|
||||
// a new unlock host activity is created every time the unlock PIN is viewed
|
||||
newUnlockActID := s.lastActivityOfTypeMatches(fleet.ActivityTypeUnlockedHost{}.ActivityName(),
|
||||
fmt.Sprintf(`{"host_id": %d, "host_display_name": %q, "host_platform": %q}`, host.ID, host.DisplayName(), host.FleetPlatform()), 0)
|
||||
require.NotEqual(t, unlockActID, newUnlockActID)
|
||||
|
||||
// as soon as the host sends an Idle MDM request, it is maked as unlocked
|
||||
cmd, err = mdmClient.Idle()
|
||||
require.NoError(t, err)
|
||||
require.Nil(t, cmd)
|
||||
|
||||
// refresh the host's status, it is unlocked
|
||||
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
|
||||
s.Do("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/wipe", host.ID), nil, http.StatusNoContent)
|
||||
wipeActID := s.lastActivityOfTypeMatches(fleet.ActivityTypeWipedHost{}.ActivityName(), fmt.Sprintf(`{"host_id": %d, "host_display_name": %q}`, host.ID, host.DisplayName()), 0)
|
||||
|
||||
// try to wipe the host again, already have it pending
|
||||
res = s.DoRaw("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/wipe", host.ID), nil, http.StatusUnprocessableEntity)
|
||||
errMsg = extractServerErrorText(res.Body)
|
||||
require.Contains(t, errMsg, "Host has pending wipe request.")
|
||||
// no activity created
|
||||
s.lastActivityOfTypeMatches(fleet.ActivityTypeWipedHost{}.ActivityName(), fmt.Sprintf(`{"host_id": %d, "host_display_name": %q}`, host.ID, host.DisplayName()), wipeActID)
|
||||
|
||||
// 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, "unlocked", *getHostResp.Host.MDM.DeviceStatus)
|
||||
require.NotNil(t, getHostResp.Host.MDM.PendingAction)
|
||||
require.Equal(t, "wipe", *getHostResp.Host.MDM.PendingAction)
|
||||
|
||||
// simulate a successful MDM result for the wipe command
|
||||
cmd, err = mdmClient.Idle()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, cmd)
|
||||
require.Equal(t, "EraseDevice", cmd.Command.RequestType)
|
||||
cmd, err = mdmClient.Acknowledge(cmd.CommandUUID)
|
||||
require.NoError(t, err)
|
||||
|
||||
// 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, "wiped", *getHostResp.Host.MDM.DeviceStatus)
|
||||
require.NotNil(t, getHostResp.Host.MDM.PendingAction)
|
||||
require.Equal(t, "", *getHostResp.Host.MDM.PendingAction)
|
||||
|
||||
// try to wipe the host again, conflict (already wiped)
|
||||
s.Do("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/wipe", host.ID), nil, http.StatusConflict)
|
||||
// no activity created
|
||||
s.lastActivityOfTypeMatches(fleet.ActivityTypeWipedHost{}.ActivityName(), fmt.Sprintf(`{"host_id": %d, "host_display_name": %q}`, host.ID, host.DisplayName()), wipeActID)
|
||||
}
|
||||
|
||||
func (s *integrationMDMTestSuite) TestZCustomConfigurationWebURL() {
|
||||
t := s.T()
|
||||
|
||||
|
@ -545,24 +545,6 @@ func (svc *Service) enqueueAppleMDMCommand(ctx context.Context, rawXMLCmd []byte
|
||||
return nil, ctxerr.Wrap(ctx, err, "decode plist command")
|
||||
}
|
||||
|
||||
// TODO(mna): as per the story's spec:
|
||||
// Make macOS and Windows MDM, low-level lock command available for free
|
||||
// users. Remove validation where we check for Premium for custom MDM
|
||||
// commands that contain the lock command
|
||||
//
|
||||
// So we'd need to not only remove this validation to allow DeviceLock (and
|
||||
// eventually EraseDevice for the Wipe story), but it needs to behave
|
||||
// similarly to how the /lock endpoint would've:
|
||||
//
|
||||
// see https://fleetdm.slack.com/archives/C03C41L5YEL/p1707169116154199?thread_ts=1707162619.655219&cid=C03C41L5YEL
|
||||
// Regarding Free use of “lock” command as custom command, remove the validation but does that behave the same as if /lock had been used?
|
||||
// @Martin Angers
|
||||
// that’s right.
|
||||
//
|
||||
// So it looks like we'd need to parse the command's XML to get the unlock
|
||||
// PIN, and TBD how to behave if there is no PIN or if it's larger than
|
||||
// supported.
|
||||
|
||||
if appleMDMPremiumCommands[strings.TrimSpace(cmd.Command.RequestType)] {
|
||||
lic, err := svc.License(ctx)
|
||||
if err != nil {
|
||||
@ -621,15 +603,6 @@ func (svc *Service) enqueueMicrosoftMDMCommand(ctx context.Context, rawXMLCmd []
|
||||
return nil, ctxerr.Wrap(ctx, err, "decode SyncML command")
|
||||
}
|
||||
|
||||
// TODO(mna): as per the story's spec:
|
||||
// Make macOS and Windows MDM, low-level lock command available for Free
|
||||
// users. Remove validation where we check for Premium for custom MDM
|
||||
// commands that contain the lock command
|
||||
//
|
||||
// However for Windows, it looks like we only prevent the RemoteWipe command,
|
||||
// nothing for lock, so looks like nothing to do here for now (will need a
|
||||
// change for the wipe command).
|
||||
|
||||
if cmdMsg.IsPremium() {
|
||||
lic, err := svc.License(ctx)
|
||||
if err != nil {
|
||||
|
@ -924,3 +924,34 @@ func (svc *Service) UnlockHost(ctx context.Context, hostID uint) (string, error)
|
||||
|
||||
return "", fleet.ErrMissingLicense
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Wipe host
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
type wipeHostRequest struct {
|
||||
HostID uint `url:"id"`
|
||||
}
|
||||
|
||||
type wipeHostResponse struct {
|
||||
Err error `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
func (r wipeHostResponse) Status() int { return http.StatusNoContent }
|
||||
func (r wipeHostResponse) error() error { return r.Err }
|
||||
|
||||
func wipeHostEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) {
|
||||
req := request.(*wipeHostRequest)
|
||||
if err := svc.WipeHost(ctx, req.HostID); err != nil {
|
||||
return wipeHostResponse{Err: err}, nil
|
||||
}
|
||||
return wipeHostResponse{}, nil
|
||||
}
|
||||
|
||||
func (svc *Service) WipeHost(ctx context.Context, hostID uint) error {
|
||||
// skipauth: No authorization check needed due to implementation returning
|
||||
// only license error.
|
||||
svc.authz.SkipAuthorization(ctx)
|
||||
|
||||
return fleet.ErrMissingLicense
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user