mirror of
https://github.com/fleetdm/fleet
synced 2026-05-17 05:58:40 +00:00
Add mdm wipe host feature (#17272)
relates to #9951 This adds the mdm host wipe feature to fleet. This includes: 1. adding wipe functionality in the fleet web UI 2. adding wipe functionality in the fleetctl CLI 3. adding API endpoints to wipe a host 4. Implementing wipe functionality on the fleet server.
This commit is contained in:
commit
730f8850ff
84 changed files with 2650 additions and 725 deletions
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.
|
||||
1
changes/issue-10489-ui-for-wiping-host
Normal file
1
changes/issue-10489-ui-for-wiping-host
Normal file
|
|
@ -0,0 +1 @@
|
|||
- add UI for wiping a host with fleet mdm.
|
||||
1
changes/issue-10494-add-wipe-cli
Normal file
1
changes/issue-10494-add-wipe-cli
Normal file
|
|
@ -0,0 +1 @@
|
|||
- add wipe command to fleetctl
|
||||
BIN
cmd/fleetctl/debug.test3290534544
Executable file
BIN
cmd/fleetctl/debug.test3290534544
Executable file
Binary file not shown.
BIN
cmd/fleetctl/debug.test698732484
Executable file
BIN
cmd/fleetctl/debug.test698732484
Executable file
Binary file not shown.
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ func mdmCommand() *cli.Command {
|
|||
mdmRunCommand(),
|
||||
mdmLockCommand(),
|
||||
mdmUnlockCommand(),
|
||||
mdmWipeCommand(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
@ -179,38 +180,11 @@ func mdmLockCommand() *cli.Command {
|
|||
Action: func(c *cli.Context) error {
|
||||
hostIdent := c.String("host")
|
||||
|
||||
if len(hostIdent) == 0 {
|
||||
return errors.New("No host targeted. Please provide --host.")
|
||||
}
|
||||
|
||||
client, err := clientFromCLI(c)
|
||||
client, host, err := hostMdmActionSetup(c, hostIdent, "lock")
|
||||
if err != nil {
|
||||
return fmt.Errorf("create client: %w", err)
|
||||
}
|
||||
|
||||
host, err := client.HostByIdentifier(hostIdent)
|
||||
if err != nil {
|
||||
var nfe service.NotFoundErr
|
||||
if errors.As(err, &nfe) {
|
||||
return errors.New("The host doesn't exist. Please provide a valid host identifier.")
|
||||
}
|
||||
|
||||
var sce kithttp.StatusCoder
|
||||
if errors.As(err, &sce) {
|
||||
if sce.StatusCode() == http.StatusForbidden {
|
||||
return errors.New("Permission denied. You don't have permission to lock this host.")
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
if host.Platform == "windows" || host.Platform == "darwin" {
|
||||
if host.MDM.EnrollmentStatus == nil || !strings.HasPrefix(*host.MDM.EnrollmentStatus, "On") ||
|
||||
host.MDM.Name != fleet.WellKnownMDMFleet {
|
||||
return errors.New(`Can't lock the host because it doesn't have MDM turned on.`)
|
||||
}
|
||||
}
|
||||
|
||||
if err := client.MDMLockHost(host.ID); err != nil {
|
||||
return fmt.Errorf("Failed to lock host: %w", err)
|
||||
}
|
||||
|
|
@ -245,38 +219,11 @@ func mdmUnlockCommand() *cli.Command {
|
|||
Action: func(c *cli.Context) error {
|
||||
hostIdent := c.String("host")
|
||||
|
||||
if len(hostIdent) == 0 {
|
||||
return errors.New("No host targeted. Please provide --host.")
|
||||
}
|
||||
|
||||
client, err := clientFromCLI(c)
|
||||
client, host, err := hostMdmActionSetup(c, hostIdent, "unlock")
|
||||
if err != nil {
|
||||
return fmt.Errorf("create client: %w", err)
|
||||
}
|
||||
|
||||
host, err := client.HostByIdentifier(hostIdent)
|
||||
if err != nil {
|
||||
var nfe service.NotFoundErr
|
||||
if errors.As(err, &nfe) {
|
||||
return errors.New("The host doesn't exist. Please provide a valid host identifier.")
|
||||
}
|
||||
|
||||
var sce kithttp.StatusCoder
|
||||
if errors.As(err, &sce) {
|
||||
if sce.StatusCode() == http.StatusForbidden {
|
||||
return errors.New("Permission denied. You don't have permission to unlock this host.")
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
if host.Platform == "windows" || host.Platform == "darwin" {
|
||||
if host.MDM.EnrollmentStatus == nil || !strings.HasPrefix(*host.MDM.EnrollmentStatus, "On") ||
|
||||
host.MDM.Name != fleet.WellKnownMDMFleet {
|
||||
return errors.New(`Can't unlock the host because it doesn't have MDM turned on.`)
|
||||
}
|
||||
}
|
||||
|
||||
pin, err := client.MDMUnlockHost(host.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to unlock host: %w", err)
|
||||
|
|
@ -306,3 +253,88 @@ fleetctl get host %s
|
|||
},
|
||||
}
|
||||
}
|
||||
|
||||
// create a mdm command to wipe the device
|
||||
func mdmWipeCommand() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "wipe",
|
||||
Usage: "Wipe a host to erase all content on a workstation.",
|
||||
Flags: []cli.Flag{contextFlag(), debugFlag(), &cli.StringFlag{
|
||||
Name: "host",
|
||||
Usage: "The host, specified by identifier, that you want to wipe.",
|
||||
Required: true,
|
||||
}},
|
||||
Action: func(c *cli.Context) error {
|
||||
hostIdent := c.String("host")
|
||||
|
||||
client, host, err := hostMdmActionSetup(c, hostIdent, "wipe")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
config, err := client.GetAppConfig()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// linux hosts need scripts to be enabled in the org settings to wipe.
|
||||
if host.Platform == "linux" && config.ServerSettings.ScriptsDisabled {
|
||||
return errors.New("Can't wipe host because running scripts is disabled in organization settings.")
|
||||
}
|
||||
|
||||
if err := client.MDMWipeHost(host.ID); err != nil {
|
||||
return fmt.Errorf("Failed to wipe host: %w", err)
|
||||
}
|
||||
|
||||
fmt.Fprintf(c.App.Writer, `
|
||||
The host will wipe when it comes online.
|
||||
|
||||
Copy and run this command to see results:
|
||||
|
||||
fleetctl get host %s`, hostIdent)
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Does some common setup for the host mdm actions such as validating the host,
|
||||
// creating the client, getting the desired host, checking permissions, and
|
||||
// ensuring MDM is turned on for the host.
|
||||
func hostMdmActionSetup(c *cli.Context, hostIdent string, actionType string) (client *service.Client, host *service.HostDetailResponse, err error) {
|
||||
if len(hostIdent) == 0 {
|
||||
return nil, nil, errors.New("No host targeted. Please provide --host.")
|
||||
}
|
||||
|
||||
client, err = clientFromCLI(c)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("create client: %w", err)
|
||||
}
|
||||
|
||||
host, err = client.HostByIdentifier(hostIdent)
|
||||
if err != nil {
|
||||
var nfe service.NotFoundErr
|
||||
if errors.As(err, &nfe) {
|
||||
fmt.Println(hostIdent)
|
||||
return nil, nil, errors.New("The host doesn't exist. Please provide a valid host identifier.")
|
||||
}
|
||||
|
||||
var sce kithttp.StatusCoder
|
||||
if errors.As(err, &sce) {
|
||||
if sce.StatusCode() == http.StatusForbidden {
|
||||
return nil, nil, fmt.Errorf("Permission denied. You don't have permission to %s this host.", actionType)
|
||||
}
|
||||
}
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// check mdm is on for the host
|
||||
if host.Platform == "windows" || host.Platform == "darwin" {
|
||||
if host.MDM.EnrollmentStatus == nil || !strings.HasPrefix(*host.MDM.EnrollmentStatus, "On") ||
|
||||
host.MDM.Name != fleet.WellKnownMDMFleet {
|
||||
return nil, nil, fmt.Errorf("Can't %s the host because it doesn't have MDM turned on.", actionType)
|
||||
}
|
||||
}
|
||||
|
||||
return client, host, 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) {
|
||||
|
|
@ -370,6 +370,21 @@ func TestMDMLockCommand(t *testing.T) {
|
|||
MDMInfo: &fleet.HostMDM{Enrolled: true, Name: fleet.WellKnownMDMFleet},
|
||||
MDM: fleet.MDMHostData{Name: fleet.WellKnownMDMFleet, EnrollmentStatus: ptr.String("On (manual)")},
|
||||
}
|
||||
winEnrolledWP := &fleet.Host{
|
||||
ID: 12,
|
||||
UUID: "win-enrolled-wp",
|
||||
Platform: "windows",
|
||||
MDMInfo: &fleet.HostMDM{Enrolled: true, Name: fleet.WellKnownMDMFleet},
|
||||
MDM: fleet.MDMHostData{Name: fleet.WellKnownMDMFleet, EnrollmentStatus: ptr.String("On (manual)")},
|
||||
}
|
||||
macEnrolledWP := &fleet.Host{
|
||||
ID: 13,
|
||||
UUID: "mac-enrolled-wp",
|
||||
Platform: "darwin",
|
||||
MDMInfo: &fleet.HostMDM{Enrolled: true, Name: fleet.WellKnownMDMFleet},
|
||||
MDM: fleet.MDMHostData{Name: fleet.WellKnownMDMFleet, EnrollmentStatus: ptr.String("On (manual)")},
|
||||
}
|
||||
|
||||
hostByUUID := make(map[string]*fleet.Host)
|
||||
hostsByID := make(map[uint]*fleet.Host)
|
||||
for _, h := range []*fleet.Host{
|
||||
|
|
@ -384,6 +399,8 @@ func TestMDMLockCommand(t *testing.T) {
|
|||
macEnrolledUP,
|
||||
winEnrolledLP,
|
||||
macEnrolledLP,
|
||||
winEnrolledWP,
|
||||
macEnrolledWP,
|
||||
} {
|
||||
hostByUUID[h.UUID] = h
|
||||
hostsByID[h.ID] = h
|
||||
|
|
@ -393,58 +410,28 @@ func TestMDMLockCommand(t *testing.T) {
|
|||
winEnrolledUP.ID: winEnrolledUP,
|
||||
macEnrolledUP.ID: macEnrolledUP,
|
||||
}
|
||||
|
||||
lockPending := map[uint]*fleet.Host{
|
||||
winEnrolledLP.ID: winEnrolledLP,
|
||||
macEnrolledLP.ID: macEnrolledLP,
|
||||
}
|
||||
|
||||
enqueuer := new(mock.MDMAppleStore)
|
||||
license := &fleet.LicenseInfo{Tier: fleet.TierPremium, Expiration: time.Now().Add(24 * time.Hour)}
|
||||
wipePending := map[uint]*fleet.Host{
|
||||
winEnrolledWP.ID: winEnrolledWP,
|
||||
macEnrolledWP.ID: macEnrolledWP,
|
||||
}
|
||||
|
||||
_, ds := runServerWithMockedDS(t, &service.TestServerOpts{
|
||||
MDMStorage: enqueuer,
|
||||
MDMPusher: mockPusher{},
|
||||
License: license,
|
||||
NoCacheDatastore: true,
|
||||
})
|
||||
ds := setupTestServer(t)
|
||||
setupDSMocks(ds, hostByUUID, hostsByID)
|
||||
|
||||
// custom ds mocks for these tests
|
||||
ds.GetHostLockWipeStatusFunc = func(ctx context.Context, host *fleet.Host) (*fleet.HostLockWipeStatus, error) {
|
||||
fleetPlatform := host.FleetPlatform()
|
||||
|
||||
// Mock datastore funcs
|
||||
ds.HostByIdentifierFunc = func(ctx context.Context, identifier string) (*fleet.Host, error) {
|
||||
h, ok := hostByUUID[identifier]
|
||||
if !ok {
|
||||
return nil, ¬FoundError{}
|
||||
}
|
||||
return h, nil
|
||||
}
|
||||
ds.LoadHostSoftwareFunc = func(ctx context.Context, host *fleet.Host, includeCVEScores bool) error {
|
||||
return nil
|
||||
}
|
||||
ds.ListPacksForHostFunc = func(ctx context.Context, hid uint) (packs []*fleet.Pack, err error) {
|
||||
return nil, nil
|
||||
}
|
||||
ds.ListHostBatteriesFunc = func(ctx context.Context, id uint) ([]*fleet.HostBattery, error) {
|
||||
return nil, nil
|
||||
}
|
||||
ds.ListPoliciesForHostFunc = func(ctx context.Context, host *fleet.Host) ([]*fleet.HostPolicy, error) {
|
||||
return nil, nil
|
||||
}
|
||||
ds.ListLabelsForHostFunc = func(ctx context.Context, hid uint) ([]*fleet.Label, error) {
|
||||
return nil, nil
|
||||
}
|
||||
ds.GetHostMDMAppleProfilesFunc = func(ctx context.Context, hostUUID string) ([]fleet.HostMDMAppleProfile, error) {
|
||||
return nil, nil
|
||||
}
|
||||
ds.GetHostMDMWindowsProfilesFunc = func(ctx context.Context, hostUUID string) ([]fleet.HostMDMWindowsProfile, error) {
|
||||
return nil, nil
|
||||
}
|
||||
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) {
|
||||
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 +441,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
|
||||
|
|
@ -463,38 +450,24 @@ func TestMDMLockCommand(t *testing.T) {
|
|||
status.LockScript = &fleet.HostScriptResult{}
|
||||
}
|
||||
|
||||
if _, ok := wipePending[host.ID]; ok {
|
||||
if fleetPlatform == "linux" {
|
||||
status.WipeScript = &fleet.HostScriptResult{ExitCode: nil}
|
||||
return &status, nil
|
||||
}
|
||||
|
||||
status.WipeMDMCommand = &fleet.MDMCommand{}
|
||||
status.WipeMDMCommandResult = nil
|
||||
return &status, nil
|
||||
}
|
||||
|
||||
return &status, nil
|
||||
}
|
||||
ds.LockHostViaScriptFunc = func(ctx context.Context, request *fleet.HostScriptRequestPayload) error {
|
||||
return nil
|
||||
}
|
||||
ds.HostLiteFunc = func(ctx context.Context, hostID uint) (*fleet.Host, error) {
|
||||
h, ok := hostsByID[hostID]
|
||||
if !ok {
|
||||
return nil, ¬FoundError{}
|
||||
}
|
||||
|
||||
return h, nil
|
||||
}
|
||||
ds.GetMDMWindowsBitLockerStatusFunc = func(ctx context.Context, host *fleet.Host) (*fleet.HostMDMDiskEncryption, error) {
|
||||
return nil, nil
|
||||
}
|
||||
ds.GetHostMDMFunc = func(ctx context.Context, hostID uint) (*fleet.HostMDM, error) {
|
||||
h, ok := hostsByID[hostID]
|
||||
if !ok {
|
||||
return nil, ¬FoundError{}
|
||||
}
|
||||
|
||||
return h.MDMInfo, nil
|
||||
}
|
||||
ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error {
|
||||
ds.LockHostViaScriptFunc = func(ctx context.Context, request *fleet.HostScriptRequestPayload, platform string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
appCfgAllMDM := &fleet.AppConfig{MDM: fleet.MDM{EnabledAndConfigured: true, WindowsEnabledAndConfigured: true}}
|
||||
appCfgWinMDM := &fleet.AppConfig{MDM: fleet.MDM{WindowsEnabledAndConfigured: true}}
|
||||
appCfgMacMDM := &fleet.AppConfig{MDM: fleet.MDM{EnabledAndConfigured: true}}
|
||||
appCfgNoMDM := &fleet.AppConfig{MDM: fleet.MDM{}}
|
||||
appCfgAllMDM, appCfgWinMDM, appCfgMacMDM, appCfgNoMDM := setupAppConigs()
|
||||
|
||||
successfulOutput := func(ident string) string {
|
||||
return fmt.Sprintf(`
|
||||
|
|
@ -535,25 +508,11 @@ fleetctl mdm unlock --host=%s
|
|||
{appCfgAllMDM, "valid macos but pending unlock", []string{"--host", macEnrolledUP.UUID}, "Host has pending unlock request."},
|
||||
{appCfgAllMDM, "valid windows but pending lock", []string{"--host", winEnrolledLP.UUID}, "Host has pending lock request."},
|
||||
{appCfgAllMDM, "valid macos but pending lock", []string{"--host", macEnrolledLP.UUID}, "Host has pending lock request."},
|
||||
// TODO: add test for wipe once implemented
|
||||
{appCfgAllMDM, "valid windows but pending wipe", []string{"--host", winEnrolledWP.UUID}, "Host has pending wipe request."},
|
||||
{appCfgAllMDM, "valid macos but pending wipe", []string{"--host", macEnrolledWP.UUID}, "Host has pending wipe request."},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
|
||||
return c.appCfg, nil
|
||||
}
|
||||
enqueuer.EnqueueDeviceLockCommandFunc = func(ctx context.Context, host *fleet.Host, cmd *mdm.Command, pin string) error {
|
||||
return nil
|
||||
}
|
||||
buf, err := runAppNoChecks(append([]string{"mdm", "lock"}, c.flags...))
|
||||
if c.wantErr != "" {
|
||||
require.Error(t, err, c.desc)
|
||||
require.ErrorContains(t, err, c.wantErr, c.desc)
|
||||
} else {
|
||||
require.NoError(t, err, c.desc)
|
||||
require.Equal(t, buf.String(), successfulOutput(c.flags[1]), c.desc)
|
||||
}
|
||||
}
|
||||
runTestCases(t, ds, "lock", successfulOutput, cases)
|
||||
}
|
||||
|
||||
func TestMDMUnlockCommand(t *testing.T) {
|
||||
|
|
@ -614,7 +573,6 @@ func TestMDMUnlockCommand(t *testing.T) {
|
|||
MDMInfo: &fleet.HostMDM{Enrolled: true, Name: fleet.WellKnownMDMFleet},
|
||||
MDM: fleet.MDMHostData{Name: fleet.WellKnownMDMFleet, EnrollmentStatus: ptr.String("On (manual)")},
|
||||
}
|
||||
|
||||
winEnrolledLP := &fleet.Host{
|
||||
ID: 10,
|
||||
UUID: "win-enrolled-lp",
|
||||
|
|
@ -629,6 +587,20 @@ func TestMDMUnlockCommand(t *testing.T) {
|
|||
MDMInfo: &fleet.HostMDM{Enrolled: true, Name: fleet.WellKnownMDMFleet},
|
||||
MDM: fleet.MDMHostData{Name: fleet.WellKnownMDMFleet, EnrollmentStatus: ptr.String("On (manual)")},
|
||||
}
|
||||
winEnrolledWP := &fleet.Host{
|
||||
ID: 12,
|
||||
UUID: "win-enrolled-wp",
|
||||
Platform: "windows",
|
||||
MDMInfo: &fleet.HostMDM{Enrolled: true, Name: fleet.WellKnownMDMFleet},
|
||||
MDM: fleet.MDMHostData{Name: fleet.WellKnownMDMFleet, EnrollmentStatus: ptr.String("On (manual)")},
|
||||
}
|
||||
macEnrolledWP := &fleet.Host{
|
||||
ID: 13,
|
||||
UUID: "mac-enrolled-wp",
|
||||
Platform: "darwin",
|
||||
MDMInfo: &fleet.HostMDM{Enrolled: true, Name: fleet.WellKnownMDMFleet},
|
||||
MDM: fleet.MDMHostData{Name: fleet.WellKnownMDMFleet, EnrollmentStatus: ptr.String("On (manual)")},
|
||||
}
|
||||
|
||||
hostByUUID := make(map[string]*fleet.Host)
|
||||
hostsByID := make(map[uint]*fleet.Host)
|
||||
|
|
@ -644,6 +616,8 @@ func TestMDMUnlockCommand(t *testing.T) {
|
|||
macEnrolledUP,
|
||||
winEnrolledLP,
|
||||
macEnrolledLP,
|
||||
winEnrolledWP,
|
||||
macEnrolledWP,
|
||||
} {
|
||||
hostByUUID[h.UUID] = h
|
||||
hostsByID[h.ID] = h
|
||||
|
|
@ -664,56 +638,21 @@ func TestMDMUnlockCommand(t *testing.T) {
|
|||
macEnrolledLP.ID: macEnrolledLP,
|
||||
}
|
||||
|
||||
enqueuer := new(mock.MDMAppleStore)
|
||||
license := &fleet.LicenseInfo{Tier: fleet.TierPremium, Expiration: time.Now().Add(24 * time.Hour)}
|
||||
|
||||
enqueuer.EnqueueDeviceLockCommandFunc = func(ctx context.Context, host *fleet.Host, cmd *mdm.Command, pin string) error {
|
||||
return nil
|
||||
wipePending := map[uint]*fleet.Host{
|
||||
winEnrolledWP.ID: winEnrolledWP,
|
||||
macEnrolledWP.ID: macEnrolledWP,
|
||||
}
|
||||
|
||||
_, ds := runServerWithMockedDS(t, &service.TestServerOpts{
|
||||
MDMStorage: enqueuer,
|
||||
MDMPusher: mockPusher{},
|
||||
License: license,
|
||||
NoCacheDatastore: true,
|
||||
})
|
||||
ds := setupTestServer(t)
|
||||
setupDSMocks(ds, hostByUUID, hostsByID)
|
||||
|
||||
// custom mocks for these test
|
||||
ds.GetHostLockWipeStatusFunc = func(ctx context.Context, host *fleet.Host) (*fleet.HostLockWipeStatus, error) {
|
||||
fleetPlatform := host.FleetPlatform()
|
||||
|
||||
// Mock datastore funcs
|
||||
ds.HostByIdentifierFunc = func(ctx context.Context, identifier string) (*fleet.Host, error) {
|
||||
h, ok := hostByUUID[identifier]
|
||||
if !ok {
|
||||
return nil, ¬FoundError{}
|
||||
}
|
||||
return h, nil
|
||||
}
|
||||
ds.LoadHostSoftwareFunc = func(ctx context.Context, host *fleet.Host, includeCVEScores bool) error {
|
||||
return nil
|
||||
}
|
||||
ds.ListPacksForHostFunc = func(ctx context.Context, hid uint) (packs []*fleet.Pack, err error) {
|
||||
return nil, nil
|
||||
}
|
||||
ds.ListHostBatteriesFunc = func(ctx context.Context, id uint) ([]*fleet.HostBattery, error) {
|
||||
return nil, nil
|
||||
}
|
||||
ds.ListPoliciesForHostFunc = func(ctx context.Context, host *fleet.Host) ([]*fleet.HostPolicy, error) {
|
||||
return nil, nil
|
||||
}
|
||||
ds.ListLabelsForHostFunc = func(ctx context.Context, hid uint) ([]*fleet.Label, error) {
|
||||
return nil, nil
|
||||
}
|
||||
ds.GetHostMDMAppleProfilesFunc = func(ctx context.Context, hostUUID string) ([]fleet.HostMDMAppleProfile, error) {
|
||||
return nil, nil
|
||||
}
|
||||
ds.GetHostMDMWindowsProfilesFunc = func(ctx context.Context, hostUUID string) ([]fleet.HostMDMWindowsProfile, error) {
|
||||
return nil, nil
|
||||
}
|
||||
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) {
|
||||
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 +662,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 +672,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
|
||||
|
|
@ -742,41 +681,27 @@ func TestMDMUnlockCommand(t *testing.T) {
|
|||
status.LockScript = &fleet.HostScriptResult{}
|
||||
}
|
||||
|
||||
if _, ok := wipePending[host.ID]; ok {
|
||||
if fleetPlatform == "linux" {
|
||||
status.WipeScript = &fleet.HostScriptResult{ExitCode: nil}
|
||||
return &status, nil
|
||||
}
|
||||
|
||||
status.WipeMDMCommand = &fleet.MDMCommand{}
|
||||
status.WipeMDMCommandResult = nil
|
||||
return &status, nil
|
||||
}
|
||||
|
||||
return &status, nil
|
||||
}
|
||||
ds.UnlockHostViaScriptFunc = func(ctx context.Context, request *fleet.HostScriptRequestPayload) error {
|
||||
ds.UnlockHostViaScriptFunc = func(ctx context.Context, request *fleet.HostScriptRequestPayload, platform string) error {
|
||||
return nil
|
||||
}
|
||||
ds.UnlockHostManuallyFunc = func(ctx context.Context, hostID uint, ts time.Time) error {
|
||||
return nil
|
||||
}
|
||||
ds.HostLiteFunc = func(ctx context.Context, hostID uint) (*fleet.Host, error) {
|
||||
h, ok := hostsByID[hostID]
|
||||
if !ok {
|
||||
return nil, ¬FoundError{}
|
||||
}
|
||||
|
||||
return h, nil
|
||||
}
|
||||
ds.GetMDMWindowsBitLockerStatusFunc = func(ctx context.Context, host *fleet.Host) (*fleet.HostMDMDiskEncryption, error) {
|
||||
return nil, nil
|
||||
}
|
||||
ds.GetHostMDMFunc = func(ctx context.Context, hostID uint) (*fleet.HostMDM, error) {
|
||||
h, ok := hostsByID[hostID]
|
||||
if !ok {
|
||||
return nil, ¬FoundError{}
|
||||
}
|
||||
|
||||
return h.MDMInfo, nil
|
||||
}
|
||||
ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error {
|
||||
ds.UnlockHostManuallyFunc = func(ctx context.Context, hostID uint, platform string, ts time.Time) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
appCfgAllMDM := &fleet.AppConfig{MDM: fleet.MDM{EnabledAndConfigured: true, WindowsEnabledAndConfigured: true}}
|
||||
appCfgWinMDM := &fleet.AppConfig{MDM: fleet.MDM{WindowsEnabledAndConfigured: true}}
|
||||
appCfgMacMDM := &fleet.AppConfig{MDM: fleet.MDM{EnabledAndConfigured: true}}
|
||||
appCfgNoMDM := &fleet.AppConfig{MDM: fleet.MDM{}}
|
||||
appCfgAllMDM, appCfgWinMDM, appCfgMacMDM, appCfgNoMDM := setupAppConigs()
|
||||
|
||||
successfulOutput := func(ident string) string {
|
||||
h := hostByUUID[ident]
|
||||
|
|
@ -801,7 +726,7 @@ fleetctl get host %s
|
|||
}{
|
||||
{appCfgAllMDM, "no flags", nil, `Required flag "host" not set`},
|
||||
{appCfgAllMDM, "host flag empty", []string{"--host", ""}, `No host targeted. Please provide --host.`},
|
||||
{appCfgAllMDM, "lock non-existent host", []string{"--host", "notfound"}, `The host doesn't exist. Please provide a valid host identifier.`},
|
||||
{appCfgAllMDM, "unlock non-existent host", []string{"--host", "notfound"}, `The host doesn't exist. Please provide a valid host identifier.`},
|
||||
{appCfgMacMDM, "valid windows but only macos mdm", []string{"--host", winEnrolled.UUID}, `Windows MDM isn't turned on.`},
|
||||
{appCfgAllMDM, "valid windows", []string{"--host", winEnrolled.UUID}, ""},
|
||||
{appCfgAllMDM, "valid macos", []string{"--host", macEnrolled.UUID}, ""},
|
||||
|
|
@ -817,22 +742,310 @@ fleetctl get host %s
|
|||
{appCfgAllMDM, "valid macos but pending unlock", []string{"--host", macEnrolledUP.UUID}, ""},
|
||||
{appCfgAllMDM, "valid windows but pending lock", []string{"--host", winEnrolledLP.UUID}, "Host has pending lock request."},
|
||||
{appCfgAllMDM, "valid macos but pending lock", []string{"--host", macEnrolledLP.UUID}, "Host has pending lock request."},
|
||||
// TODO: add test for wipe once implemented
|
||||
{appCfgAllMDM, "valid windows but pending wipe", []string{"--host", winEnrolledWP.UUID}, "Host has pending wipe request."},
|
||||
{appCfgAllMDM, "valid macos but pending wipe", []string{"--host", macEnrolledWP.UUID}, "Host has pending wipe request."},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
|
||||
return c.appCfg, nil
|
||||
}
|
||||
buf, err := runAppNoChecks(append([]string{"mdm", "unlock"}, c.flags...))
|
||||
if c.wantErr != "" {
|
||||
require.Error(t, err, c.desc)
|
||||
require.ErrorContains(t, err, c.wantErr, c.desc)
|
||||
} else {
|
||||
require.NoError(t, err, c.desc)
|
||||
require.Contains(t, buf.String(), successfulOutput(c.flags[1]), c.desc)
|
||||
}
|
||||
runTestCases(t, ds, "unlock", successfulOutput, cases)
|
||||
}
|
||||
|
||||
func TestMDMWipeCommand(t *testing.T) {
|
||||
macEnrolled := &fleet.Host{
|
||||
ID: 1,
|
||||
UUID: "mac-enrolled",
|
||||
Platform: "darwin",
|
||||
MDMInfo: &fleet.HostMDM{Enrolled: true, Name: fleet.WellKnownMDMFleet},
|
||||
MDM: fleet.MDMHostData{Name: fleet.WellKnownMDMFleet, EnrollmentStatus: ptr.String("On (manual)")},
|
||||
}
|
||||
winEnrolled := &fleet.Host{
|
||||
ID: 2,
|
||||
UUID: "win-enrolled",
|
||||
Platform: "windows",
|
||||
MDMInfo: &fleet.HostMDM{Enrolled: true, Name: fleet.WellKnownMDMFleet},
|
||||
MDM: fleet.MDMHostData{Name: fleet.WellKnownMDMFleet, EnrollmentStatus: ptr.String("On (manual)")},
|
||||
}
|
||||
linuxEnrolled := &fleet.Host{
|
||||
ID: 3,
|
||||
UUID: "linux-enrolled",
|
||||
Platform: "linux",
|
||||
}
|
||||
winNotEnrolled := &fleet.Host{
|
||||
ID: 4,
|
||||
UUID: "win-not-enrolled",
|
||||
Platform: "windows",
|
||||
}
|
||||
macNotEnrolled := &fleet.Host{
|
||||
ID: 5,
|
||||
UUID: "mac-not-enrolled",
|
||||
Platform: "darwin",
|
||||
}
|
||||
macPending := &fleet.Host{
|
||||
ID: 6,
|
||||
UUID: "mac-pending",
|
||||
Platform: "darwin",
|
||||
MDMInfo: &fleet.HostMDM{Enrolled: false, Name: fleet.WellKnownMDMFleet},
|
||||
MDM: fleet.MDMHostData{Name: fleet.WellKnownMDMFleet, EnrollmentStatus: ptr.String("Pending")},
|
||||
}
|
||||
winPending := &fleet.Host{
|
||||
ID: 7,
|
||||
UUID: "win-pending",
|
||||
Platform: "windows",
|
||||
MDMInfo: &fleet.HostMDM{Enrolled: false, Name: fleet.WellKnownMDMFleet},
|
||||
MDM: fleet.MDMHostData{Name: fleet.WellKnownMDMFleet, EnrollmentStatus: ptr.String("Pending")},
|
||||
}
|
||||
winEnrolledUP := &fleet.Host{
|
||||
ID: 8,
|
||||
UUID: "win-enrolled-up",
|
||||
Platform: "windows",
|
||||
MDMInfo: &fleet.HostMDM{Enrolled: true, Name: fleet.WellKnownMDMFleet},
|
||||
MDM: fleet.MDMHostData{Name: fleet.WellKnownMDMFleet, EnrollmentStatus: ptr.String("On (manual)")},
|
||||
}
|
||||
macEnrolledUP := &fleet.Host{
|
||||
ID: 9,
|
||||
UUID: "mac-enrolled-up",
|
||||
Platform: "darwin",
|
||||
MDMInfo: &fleet.HostMDM{Enrolled: true, Name: fleet.WellKnownMDMFleet},
|
||||
MDM: fleet.MDMHostData{Name: fleet.WellKnownMDMFleet, EnrollmentStatus: ptr.String("On (manual)")},
|
||||
}
|
||||
winEnrolledLP := &fleet.Host{
|
||||
ID: 10,
|
||||
UUID: "win-enrolled-lp",
|
||||
Platform: "windows",
|
||||
MDMInfo: &fleet.HostMDM{Enrolled: true, Name: fleet.WellKnownMDMFleet},
|
||||
MDM: fleet.MDMHostData{Name: fleet.WellKnownMDMFleet, EnrollmentStatus: ptr.String("On (manual)")},
|
||||
}
|
||||
macEnrolledLP := &fleet.Host{
|
||||
ID: 11,
|
||||
UUID: "mac-enrolled-lp",
|
||||
Platform: "darwin",
|
||||
MDMInfo: &fleet.HostMDM{Enrolled: true, Name: fleet.WellKnownMDMFleet},
|
||||
MDM: fleet.MDMHostData{Name: fleet.WellKnownMDMFleet, EnrollmentStatus: ptr.String("On (manual)")},
|
||||
}
|
||||
winEnrolledWP := &fleet.Host{
|
||||
ID: 12,
|
||||
UUID: "win-enrolled-wp",
|
||||
Platform: "windows",
|
||||
MDMInfo: &fleet.HostMDM{Enrolled: true, Name: fleet.WellKnownMDMFleet},
|
||||
MDM: fleet.MDMHostData{Name: fleet.WellKnownMDMFleet, EnrollmentStatus: ptr.String("On (manual)")},
|
||||
}
|
||||
macEnrolledWP := &fleet.Host{
|
||||
ID: 13,
|
||||
UUID: "mac-enrolled-wp",
|
||||
Platform: "darwin",
|
||||
MDMInfo: &fleet.HostMDM{Enrolled: true, Name: fleet.WellKnownMDMFleet},
|
||||
MDM: fleet.MDMHostData{Name: fleet.WellKnownMDMFleet, EnrollmentStatus: ptr.String("On (manual)")},
|
||||
}
|
||||
winEnrolledWiped := &fleet.Host{
|
||||
ID: 14,
|
||||
UUID: "win-enrolled-wiped",
|
||||
Platform: "windows",
|
||||
MDMInfo: &fleet.HostMDM{Enrolled: true, Name: fleet.WellKnownMDMFleet},
|
||||
MDM: fleet.MDMHostData{Name: fleet.WellKnownMDMFleet, EnrollmentStatus: ptr.String("On (manual)")},
|
||||
}
|
||||
macEnrolledWiped := &fleet.Host{
|
||||
ID: 15,
|
||||
UUID: "mac-enrolled-wiped",
|
||||
Platform: "darwin",
|
||||
MDMInfo: &fleet.HostMDM{Enrolled: true, Name: fleet.WellKnownMDMFleet},
|
||||
MDM: fleet.MDMHostData{Name: fleet.WellKnownMDMFleet, EnrollmentStatus: ptr.String("On (manual)")},
|
||||
}
|
||||
winEnrolledLocked := &fleet.Host{
|
||||
ID: 16,
|
||||
UUID: "win-enrolled-locked",
|
||||
Platform: "windows",
|
||||
MDMInfo: &fleet.HostMDM{Enrolled: true, Name: fleet.WellKnownMDMFleet},
|
||||
MDM: fleet.MDMHostData{Name: fleet.WellKnownMDMFleet, EnrollmentStatus: ptr.String("On (manual")},
|
||||
}
|
||||
macEnrolledLocked := &fleet.Host{
|
||||
ID: 17,
|
||||
UUID: "mac-enrolled-locked",
|
||||
Platform: "darwin",
|
||||
MDMInfo: &fleet.HostMDM{Enrolled: true, Name: fleet.WellKnownMDMFleet},
|
||||
MDM: fleet.MDMHostData{Name: fleet.WellKnownMDMFleet, EnrollmentStatus: ptr.String("On (manual")},
|
||||
}
|
||||
|
||||
hostByUUID := make(map[string]*fleet.Host)
|
||||
hostsByID := make(map[uint]*fleet.Host)
|
||||
for _, h := range []*fleet.Host{
|
||||
winEnrolled,
|
||||
macEnrolled,
|
||||
linuxEnrolled,
|
||||
macNotEnrolled,
|
||||
winNotEnrolled,
|
||||
macPending,
|
||||
winPending,
|
||||
winEnrolledUP,
|
||||
macEnrolledUP,
|
||||
winEnrolledLP,
|
||||
macEnrolledLP,
|
||||
winEnrolledWP,
|
||||
macEnrolledWP,
|
||||
winEnrolledWiped,
|
||||
macEnrolledWiped,
|
||||
winEnrolledLocked,
|
||||
macEnrolledLocked,
|
||||
} {
|
||||
hostByUUID[h.UUID] = h
|
||||
hostsByID[h.ID] = h
|
||||
}
|
||||
|
||||
locked := map[uint]*fleet.Host{
|
||||
winEnrolledLocked.ID: winEnrolledLocked,
|
||||
macEnrolledLocked.ID: macEnrolledLocked,
|
||||
}
|
||||
|
||||
unlockPending := map[uint]*fleet.Host{
|
||||
winEnrolledUP.ID: winEnrolledUP,
|
||||
macEnrolledUP.ID: macEnrolledUP,
|
||||
}
|
||||
|
||||
lockPending := map[uint]*fleet.Host{
|
||||
winEnrolledLP.ID: winEnrolledLP,
|
||||
macEnrolledLP.ID: macEnrolledLP,
|
||||
}
|
||||
|
||||
wipePending := map[uint]*fleet.Host{
|
||||
winEnrolledWP.ID: winEnrolledWP,
|
||||
macEnrolledWP.ID: macEnrolledWP,
|
||||
}
|
||||
|
||||
wiped := map[uint]*fleet.Host{
|
||||
winEnrolledWiped.ID: winEnrolledWiped,
|
||||
macEnrolledWiped.ID: macEnrolledWiped,
|
||||
}
|
||||
|
||||
ds := setupTestServer(t)
|
||||
setupDSMocks(ds, hostByUUID, hostsByID)
|
||||
|
||||
// TODO: custom ds mocks for these tests
|
||||
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[host.ID]; ok {
|
||||
if fleetPlatform == "darwin" {
|
||||
status.LockMDMCommand = &fleet.MDMCommand{}
|
||||
status.LockMDMCommandResult = &fleet.MDMCommandResult{Status: fleet.MDMAppleStatusAcknowledged}
|
||||
return &status, nil
|
||||
}
|
||||
|
||||
status.LockScript = &fleet.HostScriptResult{ExitCode: ptr.Int64(0)}
|
||||
}
|
||||
|
||||
if _, ok := unlockPending[host.ID]; ok {
|
||||
if fleetPlatform == "darwin" {
|
||||
status.UnlockPIN = "1234"
|
||||
status.UnlockRequestedAt = time.Now()
|
||||
return &status, nil
|
||||
}
|
||||
|
||||
status.UnlockScript = &fleet.HostScriptResult{}
|
||||
}
|
||||
|
||||
if _, ok := lockPending[host.ID]; ok {
|
||||
if fleetPlatform == "darwin" {
|
||||
status.LockMDMCommand = &fleet.MDMCommand{}
|
||||
return &status, nil
|
||||
}
|
||||
|
||||
status.LockScript = &fleet.HostScriptResult{}
|
||||
}
|
||||
|
||||
if _, ok := wipePending[host.ID]; ok {
|
||||
if fleetPlatform == "linux" {
|
||||
status.WipeScript = &fleet.HostScriptResult{ExitCode: nil}
|
||||
return &status, nil
|
||||
}
|
||||
|
||||
status.WipeMDMCommand = &fleet.MDMCommand{}
|
||||
status.WipeMDMCommandResult = nil
|
||||
return &status, nil
|
||||
}
|
||||
|
||||
if _, ok := wiped[host.ID]; ok {
|
||||
if fleetPlatform == "linux" {
|
||||
status.WipeScript = &fleet.HostScriptResult{ExitCode: ptr.Int64(0)}
|
||||
}
|
||||
|
||||
if fleetPlatform == "darwin" {
|
||||
status.WipeMDMCommand = &fleet.MDMCommand{}
|
||||
status.WipeMDMCommandResult = &fleet.MDMCommandResult{
|
||||
Status: fleet.MDMAppleStatusAcknowledged,
|
||||
}
|
||||
}
|
||||
|
||||
if fleetPlatform == "windows" {
|
||||
status.WipeMDMCommand = &fleet.MDMCommand{}
|
||||
status.WipeMDMCommandResult = &fleet.MDMCommandResult{
|
||||
Status: "200",
|
||||
}
|
||||
}
|
||||
|
||||
return &status, nil
|
||||
}
|
||||
|
||||
return &status, nil
|
||||
}
|
||||
ds.UnlockHostViaScriptFunc = func(ctx context.Context, request *fleet.HostScriptRequestPayload, hostFleetPlatform string) error {
|
||||
return nil
|
||||
}
|
||||
ds.UnlockHostManuallyFunc = func(ctx context.Context, hostID uint, hostFleetPlatform string, ts time.Time) error {
|
||||
return nil
|
||||
}
|
||||
ds.WipeHostViaWindowsMDMFunc = func(ctx context.Context, host *fleet.Host, cmd *fleet.MDMWindowsCommand) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
ds.WipeHostViaScriptFunc = func(ctx context.Context, request *fleet.HostScriptRequestPayload, hostFleetPlatform string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
appCfgAllMDM, appCfgWinMDM, appCfgMacMDM, appCfgNoMDM := setupAppConigs()
|
||||
appCfgScriptsDisabled := &fleet.AppConfig{ServerSettings: fleet.ServerSettings{ScriptsDisabled: true}}
|
||||
|
||||
cases := []struct {
|
||||
appCfg *fleet.AppConfig
|
||||
desc string
|
||||
flags []string
|
||||
wantErr string
|
||||
}{
|
||||
{appCfgAllMDM, "no flags", nil, `Required flag "host" not set`},
|
||||
{appCfgAllMDM, "host flag empty", []string{"--host", ""}, `No host targeted. Please provide --host.`},
|
||||
{appCfgAllMDM, "wipe non-existent host", []string{"--host", "notfound"}, `The host doesn't exist. Please provide a valid host identifier.`},
|
||||
{appCfgMacMDM, "valid windows but only macos mdm", []string{"--host", winEnrolled.UUID}, `Windows MDM isn't turned on.`},
|
||||
{appCfgAllMDM, "valid windows", []string{"--host", winEnrolled.UUID}, ""},
|
||||
{appCfgAllMDM, "valid macos", []string{"--host", macEnrolled.UUID}, ""},
|
||||
{appCfgNoMDM, "valid linux", []string{"--host", linuxEnrolled.UUID}, ""},
|
||||
{appCfgNoMDM, "valid windows but no mdm", []string{"--host", winEnrolled.UUID}, `Windows MDM isn't turned on.`},
|
||||
{appCfgMacMDM, "valid macos but not enrolled", []string{"--host", macNotEnrolled.UUID}, `Can't wipe the host because it doesn't have MDM turned on.`},
|
||||
{appCfgWinMDM, "valid windows but not enrolled", []string{"--host", winNotEnrolled.UUID}, `Can't wipe the host because it doesn't have MDM turned on.`},
|
||||
{appCfgWinMDM, "valid windows but pending mdm enroll", []string{"--host", winPending.UUID}, `Can't wipe the host because it doesn't have MDM turned on.`},
|
||||
{appCfgMacMDM, "valid macos but pending mdm enroll", []string{"--host", macPending.UUID}, `Can't wipe the host because it doesn't have MDM turned on.`},
|
||||
{appCfgAllMDM, "valid windows but pending unlock", []string{"--host", winEnrolledUP.UUID}, "Host has pending unlock request."},
|
||||
{appCfgAllMDM, "valid macos but pending unlock", []string{"--host", macEnrolledUP.UUID}, "Host has pending unlock request."},
|
||||
{appCfgAllMDM, "valid windows but pending lock", []string{"--host", winEnrolledLP.UUID}, "Host has pending lock request."},
|
||||
{appCfgAllMDM, "valid macos but pending lock", []string{"--host", macEnrolledLP.UUID}, "Host has pending lock request."},
|
||||
{appCfgAllMDM, "valid windows but pending wipe", []string{"--host", winEnrolledWP.UUID}, "Host has pending wipe request."},
|
||||
{appCfgAllMDM, "valid macos but pending wipe", []string{"--host", macEnrolledWP.UUID}, "Host has pending wipe request."},
|
||||
{appCfgAllMDM, "valid windows but host wiped", []string{"--host", winEnrolledWiped.UUID}, "Host is already wiped."},
|
||||
{appCfgAllMDM, "valid macos but host wiped", []string{"--host", macEnrolledWiped.UUID}, "Host is already wiped."},
|
||||
{appCfgAllMDM, "valid windows but host is locked", []string{"--host", winEnrolledLocked.UUID}, "Host cannot be wiped until it is unlocked."},
|
||||
{appCfgAllMDM, "valid macos but host is locked", []string{"--host", macEnrolledLocked.UUID}, "Host cannot be wiped until it is unlocked."},
|
||||
{appCfgAllMDM, "valid macos but host is locked", []string{"--host", macEnrolledLocked.UUID}, "Host cannot be wiped until it is unlocked."},
|
||||
{appCfgScriptsDisabled, "valid linux but script are disabled", []string{"--host", linuxEnrolled.UUID}, "Can't wipe host because running scripts is disabled in organization settings."},
|
||||
}
|
||||
|
||||
successfulOutput := func(ident string) string {
|
||||
return fmt.Sprintf(`
|
||||
The host will wipe when it comes online.
|
||||
|
||||
Copy and run this command to see results:
|
||||
|
||||
fleetctl get host %s`, ident)
|
||||
}
|
||||
|
||||
runTestCases(t, ds, "wipe", successfulOutput, cases)
|
||||
}
|
||||
|
||||
func writeTmpAppleMDMCmd(t *testing.T, commandName string) string {
|
||||
|
|
@ -882,3 +1095,116 @@ func writeTmpMobileconfig(t *testing.T, name string) string {
|
|||
require.NoError(t, err)
|
||||
return tmpFile.Name()
|
||||
}
|
||||
|
||||
// sets up the test server with the mock datastore and returns the mock datastore
|
||||
func setupTestServer(t *testing.T) *mock.Store {
|
||||
enqueuer := new(mock.MDMAppleStore)
|
||||
license := &fleet.LicenseInfo{Tier: fleet.TierPremium, Expiration: time.Now().Add(24 * time.Hour)}
|
||||
|
||||
enqueuer.EnqueueDeviceLockCommandFunc = func(ctx context.Context, host *fleet.Host, cmd *mdm.Command, pin string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
enqueuer.EnqueueDeviceWipeCommandFunc = func(ctx context.Context, host *fleet.Host, cmd *mdm.Command) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
_, ds := runServerWithMockedDS(t, &service.TestServerOpts{
|
||||
MDMStorage: enqueuer,
|
||||
MDMPusher: mockPusher{},
|
||||
License: license,
|
||||
NoCacheDatastore: true,
|
||||
})
|
||||
|
||||
return ds
|
||||
}
|
||||
|
||||
// sets up common data store mocks that are needed for the tests.
|
||||
func setupDSMocks(ds *mock.Store, hostByUUID map[string]*fleet.Host, hostsByID map[uint]*fleet.Host) {
|
||||
ds.HostByIdentifierFunc = func(ctx context.Context, identifier string) (*fleet.Host, error) {
|
||||
h, ok := hostByUUID[identifier]
|
||||
if !ok {
|
||||
return nil, ¬FoundError{}
|
||||
}
|
||||
return h, nil
|
||||
}
|
||||
ds.LoadHostSoftwareFunc = func(ctx context.Context, host *fleet.Host, includeCVEScores bool) error {
|
||||
return nil
|
||||
}
|
||||
ds.ListPacksForHostFunc = func(ctx context.Context, hid uint) (packs []*fleet.Pack, err error) {
|
||||
return nil, nil
|
||||
}
|
||||
ds.ListHostBatteriesFunc = func(ctx context.Context, id uint) ([]*fleet.HostBattery, error) {
|
||||
return nil, nil
|
||||
}
|
||||
ds.ListPoliciesForHostFunc = func(ctx context.Context, host *fleet.Host) ([]*fleet.HostPolicy, error) {
|
||||
return nil, nil
|
||||
}
|
||||
ds.ListLabelsForHostFunc = func(ctx context.Context, hid uint) ([]*fleet.Label, error) {
|
||||
return nil, nil
|
||||
}
|
||||
ds.GetHostMDMAppleProfilesFunc = func(ctx context.Context, hostUUID string) ([]fleet.HostMDMAppleProfile, error) {
|
||||
return nil, nil
|
||||
}
|
||||
ds.GetHostMDMWindowsProfilesFunc = func(ctx context.Context, hostUUID string) ([]fleet.HostMDMWindowsProfile, error) {
|
||||
return nil, nil
|
||||
}
|
||||
ds.GetHostMDMMacOSSetupFunc = func(ctx context.Context, hostID uint) (*fleet.HostMDMMacOSSetup, error) {
|
||||
return nil, nil
|
||||
}
|
||||
ds.HostLiteFunc = func(ctx context.Context, hostID uint) (*fleet.Host, error) {
|
||||
h, ok := hostsByID[hostID]
|
||||
if !ok {
|
||||
return nil, ¬FoundError{}
|
||||
}
|
||||
|
||||
return h, nil
|
||||
}
|
||||
ds.GetMDMWindowsBitLockerStatusFunc = func(ctx context.Context, host *fleet.Host) (*fleet.HostMDMDiskEncryption, error) {
|
||||
return nil, nil
|
||||
}
|
||||
ds.GetHostMDMFunc = func(ctx context.Context, hostID uint) (*fleet.HostMDM, error) {
|
||||
h, ok := hostsByID[hostID]
|
||||
if !ok {
|
||||
return nil, ¬FoundError{}
|
||||
}
|
||||
|
||||
return h.MDMInfo, nil
|
||||
}
|
||||
ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// sets up the various app configs for the tests. These app configs reflect the various
|
||||
// states of the MDM configuration.
|
||||
func setupAppConigs() (*fleet.AppConfig, *fleet.AppConfig, *fleet.AppConfig, *fleet.AppConfig) {
|
||||
appCfgAllMDM := &fleet.AppConfig{MDM: fleet.MDM{EnabledAndConfigured: true, WindowsEnabledAndConfigured: true}}
|
||||
appCfgWinMDM := &fleet.AppConfig{MDM: fleet.MDM{WindowsEnabledAndConfigured: true}}
|
||||
appCfgMacMDM := &fleet.AppConfig{MDM: fleet.MDM{EnabledAndConfigured: true}}
|
||||
appCfgNoMDM := &fleet.AppConfig{MDM: fleet.MDM{}}
|
||||
|
||||
return appCfgAllMDM, appCfgWinMDM, appCfgMacMDM, appCfgNoMDM
|
||||
}
|
||||
|
||||
func runTestCases(t *testing.T, ds *mock.Store, actionType string, successfulOutput func(ident string) string, cases []struct {
|
||||
appCfg *fleet.AppConfig
|
||||
desc string
|
||||
flags []string
|
||||
wantErr string
|
||||
},
|
||||
) {
|
||||
for _, c := range cases {
|
||||
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
|
||||
return c.appCfg, nil
|
||||
}
|
||||
buf, err := runAppNoChecks(append([]string{"mdm", actionType}, c.flags...))
|
||||
if c.wantErr != "" {
|
||||
require.Error(t, err, c.desc)
|
||||
require.ErrorContains(t, err, c.wantErr, c.desc)
|
||||
} else {
|
||||
require.NoError(t, err, c.desc)
|
||||
require.Contains(t, buf.String(), successfulOutput(c.flags[1]), c.desc)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
|
|
@ -111,6 +114,8 @@ func (svc *Service) LockHost(ctx context.Context, hostID uint) error {
|
|||
return ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("host_id", "Host has pending unlock request. Host cannot be locked again until unlock is complete."))
|
||||
case lockWipe.IsPendingWipe():
|
||||
return ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("host_id", "Host has pending wipe request. Cannot process lock requests once host is wiped."))
|
||||
case lockWipe.IsWiped():
|
||||
return ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("host_id", "Host is wiped. Cannot process lock requests once host is wiped."))
|
||||
case lockWipe.IsLocked():
|
||||
return ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("host_id", "Host is already locked.").WithStatus(http.StatusConflict))
|
||||
}
|
||||
|
|
@ -148,7 +153,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 +168,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")
|
||||
}
|
||||
|
|
@ -182,6 +188,8 @@ func (svc *Service) UnlockHost(ctx context.Context, hostID uint) (string, error)
|
|||
return "", ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("host_id", "Host has pending unlock request. The host will unlock when it comes online."))
|
||||
case lockWipe.IsPendingWipe():
|
||||
return "", ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("host_id", "Host has pending wipe request. Cannot process unlock requests once host is wiped."))
|
||||
case lockWipe.IsWiped():
|
||||
return "", ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("host_id", "Host is wiped. Cannot process unlock requests once host is wiped."))
|
||||
case lockWipe.IsUnlocked():
|
||||
return "", ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("host_id", "Host is already unlocked.").WithStatus(http.StatusConflict))
|
||||
}
|
||||
|
|
@ -190,6 +198,97 @@ 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.IsLocked():
|
||||
return ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("host_id", "Host is locked. Host cannot be wiped until it is unlocked."))
|
||||
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 {
|
||||
|
|
@ -232,7 +331,7 @@ func (svc *Service) enqueueLockHostRequest(ctx context.Context, host *fleet.Host
|
|||
ScriptContents: string(script),
|
||||
UserID: &vc.User.ID,
|
||||
SyncRequest: false,
|
||||
}); err != nil {
|
||||
}, host.FleetPlatform()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
|
@ -260,7 +359,7 @@ func (svc *Service) enqueueUnlockHostRequest(ctx context.Context, host *fleet.Ho
|
|||
if lockStatus.HostFleetPlatform == "darwin" {
|
||||
// record the unlock request if it was not already recorded
|
||||
if lockStatus.UnlockRequestedAt.IsZero() {
|
||||
if err := svc.ds.UnlockHostManually(ctx, host.ID, time.Now().UTC()); err != nil {
|
||||
if err := svc.ds.UnlockHostManually(ctx, host.ID, host.FleetPlatform(), time.Now().UTC()); err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
|
@ -281,7 +380,7 @@ func (svc *Service) enqueueUnlockHostRequest(ctx context.Context, host *fleet.Ho
|
|||
ScriptContents: string(script),
|
||||
UserID: &vc.User.ID,
|
||||
SyncRequest: false,
|
||||
}); err != nil {
|
||||
}, host.FleetPlatform()); err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
|
@ -301,6 +400,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,
|
||||
}, host.FleetPlatform()); 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 +468,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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -106,7 +106,8 @@ type InitialStateType = {
|
|||
isSandboxMode?: boolean;
|
||||
isFreeTier?: boolean;
|
||||
isPremiumTier?: boolean;
|
||||
isMdmEnabledAndConfigured?: boolean;
|
||||
isMacMdmEnabledAndConfigured?: boolean;
|
||||
isWindowsMdmEnabledAndConfigured?: boolean;
|
||||
isGlobalAdmin?: boolean;
|
||||
isGlobalMaintainer?: boolean;
|
||||
isGlobalObserver?: boolean;
|
||||
|
|
@ -156,7 +157,8 @@ export const initialState = {
|
|||
isSandboxMode: false,
|
||||
isFreeTier: undefined,
|
||||
isPremiumTier: undefined,
|
||||
isMdmEnabledAndConfigured: undefined,
|
||||
isMacMdmEnabledAndConfigured: undefined,
|
||||
isWindowsMdmEnabledAndConfigured: undefined,
|
||||
isGlobalAdmin: undefined,
|
||||
isGlobalMaintainer: undefined,
|
||||
isGlobalObserver: undefined,
|
||||
|
|
@ -212,7 +214,12 @@ const setPermissions = (
|
|||
isSandboxMode: permissions.isSandboxMode(config),
|
||||
isFreeTier: permissions.isFreeTier(config),
|
||||
isPremiumTier: permissions.isPremiumTier(config),
|
||||
isMdmEnabledAndConfigured: permissions.isMdmEnabledAndConfigured(config),
|
||||
isMacMdmEnabledAndConfigured: permissions.isMacMdmEnabledAndConfigured(
|
||||
config
|
||||
),
|
||||
isWindowsMdmEnabledAndConfigured: permissions.isWindowsMdmEnabledAndConfigured(
|
||||
config
|
||||
),
|
||||
isGlobalAdmin: permissions.isGlobalAdmin(user),
|
||||
isGlobalMaintainer: permissions.isGlobalMaintainer(user),
|
||||
isGlobalObserver: permissions.isGlobalObserver(user),
|
||||
|
|
@ -365,7 +372,8 @@ const AppProvider = ({ children }: Props): JSX.Element => {
|
|||
isSandboxMode: state.isSandboxMode,
|
||||
isFreeTier: state.isFreeTier,
|
||||
isPremiumTier: state.isPremiumTier,
|
||||
isMdmEnabledAndConfigured: state.isMdmEnabledAndConfigured,
|
||||
isMacMdmEnabledAndConfigured: state.isMacMdmEnabledAndConfigured,
|
||||
isWindowsMdmEnabledAndConfigured: state.isWindowsMdmEnabledAndConfigured,
|
||||
isGlobalAdmin: state.isGlobalAdmin,
|
||||
isGlobalMaintainer: state.isGlobalMaintainer,
|
||||
isGlobalObserver: state.isGlobalObserver,
|
||||
|
|
|
|||
|
|
@ -66,7 +66,15 @@ export enum ActivityType {
|
|||
EditedWindowsUpdates = "edited_windows_updates",
|
||||
LockedHost = "locked_host",
|
||||
UnlockedHost = "unlocked_host",
|
||||
WipedHost = "wiped_host",
|
||||
}
|
||||
|
||||
// This is a subset of ActivityType that are shown only for the host past activities
|
||||
export type IHostPastActivityType =
|
||||
| ActivityType.RanScript
|
||||
| ActivityType.LockedHost
|
||||
| ActivityType.UnlockedHost;
|
||||
|
||||
export interface IActivity {
|
||||
created_at: string;
|
||||
id: number;
|
||||
|
|
@ -77,6 +85,11 @@ export interface IActivity {
|
|||
type: ActivityType;
|
||||
details?: IActivityDetails;
|
||||
}
|
||||
|
||||
export type IPastActivity = Omit<IActivity, "type"> & {
|
||||
type: IHostPastActivityType;
|
||||
};
|
||||
|
||||
export interface IActivityDetails {
|
||||
pack_id?: number;
|
||||
pack_name?: string;
|
||||
|
|
|
|||
|
|
@ -157,8 +157,8 @@ interface IMdmMacOsSetup {
|
|||
bootstrap_package_name: string;
|
||||
}
|
||||
|
||||
export type HostMdmDeviceStatus = "unlocked" | "locked";
|
||||
export type HostMdmPendingAction = "unlock" | "lock" | "";
|
||||
export type HostMdmDeviceStatus = "unlocked" | "locked" | "wiped";
|
||||
export type HostMdmPendingAction = "unlock" | "lock" | "wipe" | "";
|
||||
|
||||
export interface IHostMdmData {
|
||||
encryption_key_available: boolean;
|
||||
|
|
|
|||
|
|
@ -1165,4 +1165,17 @@ describe("Activity Feed", () => {
|
|||
screen.getByText("deleted multiple queries", { exact: false })
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
// test for wipe activity
|
||||
it("renders a 'wiped_host' type activity for a team", () => {
|
||||
const activity = createMockActivity({
|
||||
type: ActivityType.WipedHost,
|
||||
details: {
|
||||
host_display_name: "Foo Host",
|
||||
},
|
||||
});
|
||||
render(<ActivityItem activity={activity} isPremiumTier />);
|
||||
|
||||
expect(screen.getByText("wiped", { exact: false })).toBeInTheDocument();
|
||||
expect(screen.getByText("Foo Host", { exact: false })).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -755,6 +755,14 @@ const TAGGED_TEMPLATES = {
|
|||
</>
|
||||
);
|
||||
},
|
||||
wipedHost: (activity: IActivity) => {
|
||||
return (
|
||||
<>
|
||||
{" "}
|
||||
wiped <b>{activity.details?.host_display_name}</b>.
|
||||
</>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
const getDetail = (
|
||||
|
|
@ -907,6 +915,9 @@ const getDetail = (
|
|||
case ActivityType.UnlockedHost: {
|
||||
return TAGGED_TEMPLATES.unlockedHost(activity);
|
||||
}
|
||||
case ActivityType.WipedHost: {
|
||||
return TAGGED_TEMPLATES.wipedHost(activity);
|
||||
}
|
||||
default: {
|
||||
return TAGGED_TEMPLATES.defaultActivityTemplate(activity);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ describe("Integrations Page", () => {
|
|||
const render = createCustomRenderer({
|
||||
withBackendMock: true,
|
||||
context: {
|
||||
app: { isMdmEnabledAndConfigured: true },
|
||||
app: { isMacMdmEnabledAndConfigured: true },
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -94,7 +94,7 @@ describe("Host Actions Dropdown", () => {
|
|||
const render = createCustomRenderer({
|
||||
context: {
|
||||
app: {
|
||||
isMdmEnabledAndConfigured: true,
|
||||
isMacMdmEnabledAndConfigured: true,
|
||||
isGlobalAdmin: true,
|
||||
currentUser: createMockUser(),
|
||||
},
|
||||
|
|
@ -122,7 +122,7 @@ describe("Host Actions Dropdown", () => {
|
|||
const render = createCustomRenderer({
|
||||
context: {
|
||||
app: {
|
||||
isMdmEnabledAndConfigured: true,
|
||||
isMacMdmEnabledAndConfigured: true,
|
||||
isGlobalMaintainer: true,
|
||||
currentUser: createMockUser(),
|
||||
},
|
||||
|
|
@ -150,7 +150,7 @@ describe("Host Actions Dropdown", () => {
|
|||
const render = createCustomRenderer({
|
||||
context: {
|
||||
app: {
|
||||
isMdmEnabledAndConfigured: true,
|
||||
isMacMdmEnabledAndConfigured: true,
|
||||
currentUser: createMockUser({
|
||||
teams: [createMockTeam({ id: 1, role: "admin" })],
|
||||
}),
|
||||
|
|
@ -179,7 +179,7 @@ describe("Host Actions Dropdown", () => {
|
|||
const render = createCustomRenderer({
|
||||
context: {
|
||||
app: {
|
||||
isMdmEnabledAndConfigured: true,
|
||||
isMacMdmEnabledAndConfigured: true,
|
||||
currentUser: createMockUser({
|
||||
teams: [createMockTeam({ id: 1, role: "maintainer" })],
|
||||
}),
|
||||
|
|
@ -208,7 +208,7 @@ describe("Host Actions Dropdown", () => {
|
|||
const render = createCustomRenderer({
|
||||
context: {
|
||||
app: {
|
||||
isMdmEnabledAndConfigured: true,
|
||||
isMacMdmEnabledAndConfigured: true,
|
||||
currentUser: createMockUser(),
|
||||
},
|
||||
},
|
||||
|
|
@ -235,7 +235,7 @@ describe("Host Actions Dropdown", () => {
|
|||
const render = createCustomRenderer({
|
||||
context: {
|
||||
app: {
|
||||
isMdmEnabledAndConfigured: true,
|
||||
isMacMdmEnabledAndConfigured: true,
|
||||
isGlobalAdmin: true,
|
||||
currentUser: createMockUser(),
|
||||
},
|
||||
|
|
@ -267,7 +267,7 @@ describe("Host Actions Dropdown", () => {
|
|||
const render = createCustomRenderer({
|
||||
context: {
|
||||
app: {
|
||||
isMdmEnabledAndConfigured: true,
|
||||
isMacMdmEnabledAndConfigured: true,
|
||||
isGlobalAdmin: true,
|
||||
currentUser: createMockUser(),
|
||||
},
|
||||
|
|
@ -402,7 +402,7 @@ describe("Host Actions Dropdown", () => {
|
|||
context: {
|
||||
app: {
|
||||
isPremiumTier: true,
|
||||
isMdmEnabledAndConfigured: true,
|
||||
isMacMdmEnabledAndConfigured: true,
|
||||
isGlobalAdmin: true,
|
||||
currentUser: createMockUser(),
|
||||
},
|
||||
|
|
@ -431,7 +431,7 @@ describe("Host Actions Dropdown", () => {
|
|||
context: {
|
||||
app: {
|
||||
isPremiumTier: true,
|
||||
isMdmEnabledAndConfigured: true,
|
||||
isMacMdmEnabledAndConfigured: true,
|
||||
isGlobalAdmin: true,
|
||||
currentUser: createMockUser(),
|
||||
},
|
||||
|
|
@ -460,7 +460,7 @@ describe("Host Actions Dropdown", () => {
|
|||
context: {
|
||||
app: {
|
||||
isPremiumTier: true,
|
||||
isMdmEnabledAndConfigured: true,
|
||||
isMacMdmEnabledAndConfigured: true,
|
||||
isGlobalAdmin: true,
|
||||
currentUser: createMockUser(),
|
||||
},
|
||||
|
|
@ -491,7 +491,7 @@ describe("Host Actions Dropdown", () => {
|
|||
context: {
|
||||
app: {
|
||||
isPremiumTier: true,
|
||||
isMdmEnabledAndConfigured: true,
|
||||
isMacMdmEnabledAndConfigured: true,
|
||||
isGlobalAdmin: true,
|
||||
currentUser: createMockUser(),
|
||||
},
|
||||
|
|
@ -520,7 +520,7 @@ describe("Host Actions Dropdown", () => {
|
|||
context: {
|
||||
app: {
|
||||
isPremiumTier: true,
|
||||
isMdmEnabledAndConfigured: true,
|
||||
isMacMdmEnabledAndConfigured: true,
|
||||
isGlobalAdmin: true,
|
||||
currentUser: createMockUser(),
|
||||
},
|
||||
|
|
@ -549,7 +549,7 @@ describe("Host Actions Dropdown", () => {
|
|||
context: {
|
||||
app: {
|
||||
isPremiumTier: true,
|
||||
isMdmEnabledAndConfigured: true,
|
||||
isMacMdmEnabledAndConfigured: true,
|
||||
isGlobalAdmin: true,
|
||||
currentUser: createMockUser(),
|
||||
},
|
||||
|
|
@ -578,7 +578,7 @@ describe("Host Actions Dropdown", () => {
|
|||
context: {
|
||||
app: {
|
||||
isPremiumTier: true,
|
||||
isMdmEnabledAndConfigured: true,
|
||||
isMacMdmEnabledAndConfigured: true,
|
||||
isGlobalAdmin: true,
|
||||
currentUser: createMockUser(),
|
||||
},
|
||||
|
|
@ -601,5 +601,126 @@ describe("Host Actions Dropdown", () => {
|
|||
|
||||
expect(screen.queryByText("Unlock")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("does not renders when a mac host but does not have Fleet mac mdm enabled and configured", async () => {
|
||||
const render = createCustomRenderer({
|
||||
context: {
|
||||
app: {
|
||||
isPremiumTier: true,
|
||||
isMacMdmEnabledAndConfigured: false,
|
||||
isWindowsMdmEnabledAndConfigured: true,
|
||||
isGlobalAdmin: true,
|
||||
currentUser: createMockUser(),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const { user } = render(
|
||||
<HostActionsDropdown
|
||||
hostTeamId={null}
|
||||
onSelect={noop}
|
||||
hostStatus="online"
|
||||
hostMdmEnrollmentStatus="On (automatic)"
|
||||
mdmName="Fleet"
|
||||
hostPlatform="darwin"
|
||||
hostMdmDeviceStatus="locked"
|
||||
/>
|
||||
);
|
||||
|
||||
await user.click(screen.getByText("Actions"));
|
||||
|
||||
expect(screen.queryByText("Unlock")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Wipe action", () => {
|
||||
it("renders only when the host is unlocked", async () => {
|
||||
const render = createCustomRenderer({
|
||||
context: {
|
||||
app: {
|
||||
isPremiumTier: true,
|
||||
isMacMdmEnabledAndConfigured: true,
|
||||
isGlobalAdmin: true,
|
||||
currentUser: createMockUser(),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const { user } = render(
|
||||
<HostActionsDropdown
|
||||
hostTeamId={null}
|
||||
onSelect={noop}
|
||||
hostStatus="online"
|
||||
hostMdmEnrollmentStatus="On (automatic)"
|
||||
mdmName="Fleet"
|
||||
hostPlatform="darwin"
|
||||
hostMdmDeviceStatus="unlocked"
|
||||
/>
|
||||
);
|
||||
|
||||
await user.click(screen.getByText("Actions"));
|
||||
|
||||
expect(screen.getByText("Wipe")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("does not renders when a windows host but does not have Fleet windows mdm enabled and configured", async () => {
|
||||
const render = createCustomRenderer({
|
||||
context: {
|
||||
app: {
|
||||
isPremiumTier: true,
|
||||
isMacMdmEnabledAndConfigured: true,
|
||||
isWindowsMdmEnabledAndConfigured: false,
|
||||
isGlobalAdmin: true,
|
||||
currentUser: createMockUser(),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const { user } = render(
|
||||
<HostActionsDropdown
|
||||
hostTeamId={null}
|
||||
onSelect={noop}
|
||||
hostStatus="online"
|
||||
hostMdmEnrollmentStatus="On (automatic)"
|
||||
mdmName="Fleet"
|
||||
hostPlatform="windows"
|
||||
hostMdmDeviceStatus="unlocked"
|
||||
/>
|
||||
);
|
||||
|
||||
await user.click(screen.getByText("Actions"));
|
||||
|
||||
expect(screen.queryByText("Wipe")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("does not renders when a mac host but does not have Fleet mac mdm enabled and configured", async () => {
|
||||
const render = createCustomRenderer({
|
||||
context: {
|
||||
app: {
|
||||
isPremiumTier: true,
|
||||
isMacMdmEnabledAndConfigured: false,
|
||||
isWindowsMdmEnabledAndConfigured: true,
|
||||
isGlobalAdmin: true,
|
||||
currentUser: createMockUser(),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const { user } = render(
|
||||
<HostActionsDropdown
|
||||
hostTeamId={null}
|
||||
onSelect={noop}
|
||||
hostStatus="online"
|
||||
hostMdmEnrollmentStatus="On (automatic)"
|
||||
mdmName="Fleet"
|
||||
hostPlatform="darwin"
|
||||
hostMdmDeviceStatus="unlocked"
|
||||
/>
|
||||
);
|
||||
|
||||
await user.click(screen.getByText("Actions"));
|
||||
|
||||
expect(screen.queryByText("Wipe")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -38,7 +38,8 @@ const HostActionsDropdown = ({
|
|||
isPremiumTier = false,
|
||||
isGlobalAdmin = false,
|
||||
isGlobalMaintainer = false,
|
||||
isMdmEnabledAndConfigured = false,
|
||||
isMacMdmEnabledAndConfigured = false,
|
||||
isWindowsMdmEnabledAndConfigured = false,
|
||||
isSandboxMode = false,
|
||||
currentUser,
|
||||
} = useContext(AppContext);
|
||||
|
|
@ -67,7 +68,8 @@ const HostActionsDropdown = ({
|
|||
hostMdmEnrollmentStatus ?? ""
|
||||
),
|
||||
isFleetMdm: mdmName === "Fleet",
|
||||
isMdmEnabledAndConfigured,
|
||||
isMacMdmEnabledAndConfigured,
|
||||
isWindowsMdmEnabledAndConfigured,
|
||||
doesStoreEncryptionKey: doesStoreEncryptionKey ?? false,
|
||||
isSandboxMode,
|
||||
hostMdmDeviceStatus,
|
||||
|
|
|
|||
|
|
@ -44,11 +44,11 @@ const DEFAULT_OPTIONS = [
|
|||
value: "lock",
|
||||
disabled: false,
|
||||
},
|
||||
// {
|
||||
// label: "Wipe",
|
||||
// value: "wipe",
|
||||
// disabled: false,
|
||||
// },
|
||||
{
|
||||
label: "Wipe",
|
||||
value: "wipe",
|
||||
disabled: false,
|
||||
},
|
||||
{
|
||||
label: "Unlock",
|
||||
value: "unlock",
|
||||
|
|
@ -74,7 +74,8 @@ interface IHostActionConfigOptions {
|
|||
isHostOnline: boolean;
|
||||
isEnrolledInMdm: boolean;
|
||||
isFleetMdm: boolean;
|
||||
isMdmEnabledAndConfigured: boolean;
|
||||
isMacMdmEnabledAndConfigured: boolean;
|
||||
isWindowsMdmEnabledAndConfigured: boolean;
|
||||
doesStoreEncryptionKey: boolean;
|
||||
isSandboxMode: boolean;
|
||||
hostMdmDeviceStatus: HostMdmDeviceStatusUIState;
|
||||
|
|
@ -93,11 +94,11 @@ const canEditMdm = (config: IHostActionConfigOptions) => {
|
|||
isTeamMaintainer,
|
||||
isEnrolledInMdm,
|
||||
isFleetMdm,
|
||||
isMdmEnabledAndConfigured,
|
||||
isMacMdmEnabledAndConfigured,
|
||||
} = config;
|
||||
return (
|
||||
config.hostPlatform === "darwin" &&
|
||||
isMdmEnabledAndConfigured &&
|
||||
isMacMdmEnabledAndConfigured &&
|
||||
isEnrolledInMdm &&
|
||||
isFleetMdm &&
|
||||
(isGlobalAdmin || isGlobalMaintainer || isTeamAdmin || isTeamMaintainer)
|
||||
|
|
@ -107,7 +108,7 @@ const canEditMdm = (config: IHostActionConfigOptions) => {
|
|||
const canLockHost = ({
|
||||
isPremiumTier,
|
||||
hostPlatform,
|
||||
isMdmEnabledAndConfigured,
|
||||
isMacMdmEnabledAndConfigured,
|
||||
isEnrolledInMdm,
|
||||
isFleetMdm,
|
||||
isGlobalAdmin,
|
||||
|
|
@ -120,7 +121,7 @@ const canLockHost = ({
|
|||
const canLockDarwin =
|
||||
hostPlatform === "darwin" &&
|
||||
isFleetMdm &&
|
||||
isMdmEnabledAndConfigured &&
|
||||
isMacMdmEnabledAndConfigured &&
|
||||
isEnrolledInMdm;
|
||||
|
||||
return (
|
||||
|
|
@ -143,23 +144,23 @@ const canWipeHost = ({
|
|||
isTeamObserver,
|
||||
isFleetMdm,
|
||||
isEnrolledInMdm,
|
||||
isMdmEnabledAndConfigured,
|
||||
isMacMdmEnabledAndConfigured,
|
||||
isWindowsMdmEnabledAndConfigured,
|
||||
hostPlatform,
|
||||
hostMdmDeviceStatus,
|
||||
}: IHostActionConfigOptions) => {
|
||||
// TODO: remove when we work on wipe issue.
|
||||
return false;
|
||||
const hostMdmEnabled =
|
||||
(hostPlatform === "darwin" && isMacMdmEnabledAndConfigured) ||
|
||||
(hostPlatform === "windows" && isWindowsMdmEnabledAndConfigured);
|
||||
|
||||
// macOS and Windows hosts have the same conditions and can be wiped if they
|
||||
// are enrolled in MDM and the MDM is enabled.
|
||||
const canWipeMacOrWindows =
|
||||
(hostPlatform === "darwin" || hostPlatform === "windows") &&
|
||||
isFleetMdm &&
|
||||
isMdmEnabledAndConfigured &&
|
||||
isEnrolledInMdm;
|
||||
const canWipeMacOrWindows = hostMdmEnabled && isFleetMdm && isEnrolledInMdm;
|
||||
|
||||
return (
|
||||
isPremiumTier &&
|
||||
(hostPlatform === "linux" || canWipeMacOrWindows) &&
|
||||
hostMdmDeviceStatus === "unlocked" &&
|
||||
(isLinuxLike(hostPlatform) || canWipeMacOrWindows) &&
|
||||
(isGlobalAdmin ||
|
||||
isGlobalMaintainer ||
|
||||
isGlobalObserver ||
|
||||
|
|
@ -177,14 +178,14 @@ const canUnlock = ({
|
|||
isTeamMaintainer,
|
||||
isFleetMdm,
|
||||
isEnrolledInMdm,
|
||||
isMdmEnabledAndConfigured,
|
||||
isMacMdmEnabledAndConfigured,
|
||||
hostPlatform,
|
||||
hostMdmDeviceStatus,
|
||||
}: IHostActionConfigOptions) => {
|
||||
const canLockDarwin =
|
||||
const canUnlockDarwin =
|
||||
hostPlatform === "darwin" &&
|
||||
isFleetMdm &&
|
||||
isMdmEnabledAndConfigured &&
|
||||
isMacMdmEnabledAndConfigured &&
|
||||
isEnrolledInMdm;
|
||||
|
||||
// "unlocking" for a macOS host means that somebody saw the unlock pin, but
|
||||
|
|
@ -198,7 +199,7 @@ const canUnlock = ({
|
|||
isPremiumTier &&
|
||||
isValidState &&
|
||||
(isGlobalAdmin || isGlobalMaintainer || isTeamAdmin || isTeamMaintainer) &&
|
||||
(canLockDarwin || hostPlatform === "windows" || isLinuxLike(hostPlatform))
|
||||
(canUnlockDarwin || hostPlatform === "windows" || isLinuxLike(hostPlatform))
|
||||
);
|
||||
};
|
||||
|
||||
|
|
@ -265,9 +266,9 @@ const filterOutOptions = (
|
|||
options = options.filter((option) => option.value !== "lock");
|
||||
}
|
||||
|
||||
// if (!canWipeHost(config)) {
|
||||
// options = options.filter((option) => option.value !== "wipe");
|
||||
// }
|
||||
if (!canWipeHost(config)) {
|
||||
options = options.filter((option) => option.value !== "wipe");
|
||||
}
|
||||
|
||||
if (!canUnlock(config)) {
|
||||
options = options.filter((option) => option.value !== "unlock");
|
||||
|
|
@ -292,7 +293,12 @@ const setOptionsAsDisabled = (
|
|||
};
|
||||
|
||||
let optionsToDisable: IDropdownOption[] = [];
|
||||
if (!isHostOnline) {
|
||||
if (
|
||||
!isHostOnline ||
|
||||
isDeviceStatusUpdating(hostMdmDeviceStatus) ||
|
||||
hostMdmDeviceStatus === "locked" ||
|
||||
hostMdmDeviceStatus === "wiped"
|
||||
) {
|
||||
optionsToDisable = optionsToDisable.concat(
|
||||
options.filter(
|
||||
(option) => option.value === "query" || option.value === "mdmOff"
|
||||
|
|
@ -304,16 +310,6 @@ const setOptionsAsDisabled = (
|
|||
options.filter((option) => option.value === "transfer")
|
||||
);
|
||||
}
|
||||
if (
|
||||
isDeviceStatusUpdating(hostMdmDeviceStatus) ||
|
||||
hostMdmDeviceStatus === "locked"
|
||||
) {
|
||||
optionsToDisable = optionsToDisable.concat(
|
||||
options.filter(
|
||||
(option) => option.value === "query" || option.value === "mdmOff"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
disableOptions(optionsToDisable);
|
||||
return options;
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import { NotificationContext } from "context/notification";
|
|||
|
||||
import activitiesAPI, {
|
||||
IActivitiesResponse,
|
||||
IPastActivitiesResponse,
|
||||
IUpcomingActivitiesResponse,
|
||||
} from "services/entities/activities";
|
||||
import hostAPI from "services/entities/hosts";
|
||||
|
|
@ -90,6 +91,7 @@ import {
|
|||
HostMdmDeviceStatusUIState,
|
||||
getHostDeviceStatusUIState,
|
||||
} from "../helpers";
|
||||
import WipeModal from "./modals/WipeModal";
|
||||
|
||||
const baseClass = "host-details";
|
||||
|
||||
|
|
@ -164,6 +166,7 @@ const HostDetailsPage = ({
|
|||
);
|
||||
const [showLockHostModal, setShowLockHostModal] = useState(false);
|
||||
const [showUnlockHostModal, setShowUnlockHostModal] = useState(false);
|
||||
const [showWipeModal, setShowWipeModal] = useState(false);
|
||||
const [scriptDetailsId, setScriptDetailsId] = useState("");
|
||||
const [selectedPolicy, setSelectedPolicy] = useState<IHostPolicy | null>(
|
||||
null
|
||||
|
|
@ -366,9 +369,9 @@ const HostDetailsPage = ({
|
|||
isError: pastActivitiesIsError,
|
||||
refetch: refetchPastActivities,
|
||||
} = useQuery<
|
||||
IActivitiesResponse,
|
||||
IPastActivitiesResponse,
|
||||
Error,
|
||||
IActivitiesResponse,
|
||||
IPastActivitiesResponse,
|
||||
Array<{
|
||||
scope: string;
|
||||
pageIndex: number;
|
||||
|
|
@ -644,6 +647,9 @@ const HostDetailsPage = ({
|
|||
case "unlock":
|
||||
setShowUnlockHostModal(true);
|
||||
break;
|
||||
case "wipe":
|
||||
setShowWipeModal(true);
|
||||
break;
|
||||
default: // do nothing
|
||||
}
|
||||
};
|
||||
|
|
@ -976,6 +982,14 @@ const HostDetailsPage = ({
|
|||
onClose={() => setShowUnlockHostModal(false)}
|
||||
/>
|
||||
)}
|
||||
{showWipeModal && (
|
||||
<WipeModal
|
||||
id={host.id}
|
||||
hostName={host.display_name}
|
||||
onSuccess={() => setHostMdmDeviceState("wiping")}
|
||||
onClose={() => setShowWipeModal(false)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
</MainContent>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,77 @@
|
|||
import React, { useContext } from "react";
|
||||
|
||||
import hostAPI from "services/entities/hosts";
|
||||
import { getErrorReason } from "interfaces/errors";
|
||||
|
||||
import Modal from "components/Modal";
|
||||
import Button from "components/buttons/Button";
|
||||
import Checkbox from "components/forms/fields/Checkbox";
|
||||
import { NotificationContext } from "context/notification";
|
||||
|
||||
const baseClass = "wipe-modal";
|
||||
|
||||
interface IWipeModalProps {
|
||||
id: number;
|
||||
hostName: string;
|
||||
onSuccess: () => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const WipeModal = ({ id, hostName, onSuccess, onClose }: IWipeModalProps) => {
|
||||
const { renderFlash } = useContext(NotificationContext);
|
||||
const [lockChecked, setLockChecked] = React.useState(false);
|
||||
const [isWiping, setIsWiping] = React.useState(false);
|
||||
|
||||
const onWipe = async () => {
|
||||
setIsWiping(true);
|
||||
try {
|
||||
await hostAPI.wipeHost(id);
|
||||
onSuccess();
|
||||
renderFlash("success", "Success! Host is wiping.");
|
||||
} catch (e) {
|
||||
renderFlash("error", getErrorReason(e));
|
||||
}
|
||||
onClose();
|
||||
setIsWiping(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal className={baseClass} title="Wipe host" onExit={onClose}>
|
||||
<>
|
||||
<div className={`${baseClass}__modal-content`}>
|
||||
<p>All content will be erased on this host.</p>
|
||||
<div className={`${baseClass}__confirm-message`}>
|
||||
<span>
|
||||
<b>Please check to confirm:</b>
|
||||
</span>
|
||||
<Checkbox
|
||||
wrapperClassName={`${baseClass}__wipe-checkbox`}
|
||||
value={lockChecked}
|
||||
onChange={(value: boolean) => setLockChecked(value)}
|
||||
>
|
||||
I wish to wipe <b>{hostName}</b>
|
||||
</Checkbox>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="modal-cta-wrap">
|
||||
<Button
|
||||
type="button"
|
||||
onClick={onWipe}
|
||||
variant="alert"
|
||||
className="delete-loading"
|
||||
disabled={!lockChecked}
|
||||
isLoading={isWiping}
|
||||
>
|
||||
Wipe
|
||||
</Button>
|
||||
<Button onClick={onClose} variant="inverse-alert">
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default WipeModal;
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
.wipe-modal {
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
&__modal-content {
|
||||
display: grid;
|
||||
gap: $pad-large;
|
||||
}
|
||||
|
||||
&__wipe-checkbox {
|
||||
margin-top: $pad-small;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from "./WipeModal";
|
||||
|
|
@ -2,7 +2,10 @@ import React from "react";
|
|||
import { Tab, TabList, TabPanel, Tabs } from "react-tabs";
|
||||
|
||||
import { IActivityDetails } from "interfaces/activity";
|
||||
import { IActivitiesResponse } from "services/entities/activities";
|
||||
import {
|
||||
IPastActivitiesResponse,
|
||||
IUpcomingActivitiesResponse,
|
||||
} from "services/entities/activities";
|
||||
|
||||
import Card from "components/Card";
|
||||
import TabsWrapper from "components/TabsWrapper";
|
||||
|
|
@ -45,7 +48,7 @@ const UpcomingTooltip = () => {
|
|||
|
||||
interface IActivityProps {
|
||||
activeTab: "past" | "upcoming";
|
||||
activities?: IActivitiesResponse;
|
||||
activities?: IPastActivitiesResponse | IUpcomingActivitiesResponse;
|
||||
isLoading?: boolean;
|
||||
isError?: boolean;
|
||||
upcomingCount: number;
|
||||
|
|
@ -93,7 +96,7 @@ const Activity = ({
|
|||
</TabList>
|
||||
<TabPanel>
|
||||
<PastActivityFeed
|
||||
activities={activities}
|
||||
activities={activities as IPastActivitiesResponse | undefined}
|
||||
onDetailsClick={onShowDetails}
|
||||
isError={isError}
|
||||
onNextPage={onNextPage}
|
||||
|
|
@ -103,7 +106,7 @@ const Activity = ({
|
|||
<TabPanel>
|
||||
<UpcomingTooltip />
|
||||
<UpcomingActivityFeed
|
||||
activities={activities}
|
||||
activities={activities as IUpcomingActivitiesResponse | undefined}
|
||||
onDetailsClick={onShowDetails}
|
||||
isError={isError}
|
||||
onNextPage={onNextPage}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,34 @@
|
|||
import React from "react";
|
||||
|
||||
import {
|
||||
ActivityType,
|
||||
IHostPastActivityType,
|
||||
IPastActivity,
|
||||
} from "interfaces/activity";
|
||||
|
||||
import { ShowActivityDetailsHandler } from "./Activity";
|
||||
|
||||
import RanScriptActivityItem from "./ActivityItems/RanScriptActivityItem";
|
||||
import LockedHostActivityItem from "./ActivityItems/LockedHostActivityItem";
|
||||
import UnlockedHostActivityItem from "./ActivityItems/UnlockedHostActivityItem";
|
||||
|
||||
/** The component props that all host activity items must adhere to */
|
||||
export interface IHostActivityItemComponentProps {
|
||||
activity: IPastActivity;
|
||||
}
|
||||
|
||||
/** Used for activity items component that need a show details handler */
|
||||
export interface IHostActivityItemComponentPropsWithShowDetails
|
||||
extends IHostActivityItemComponentProps {
|
||||
onShowDetails: ShowActivityDetailsHandler;
|
||||
}
|
||||
|
||||
export const pastActivityComponentMap: Record<
|
||||
IHostPastActivityType,
|
||||
| React.FC<IHostActivityItemComponentProps>
|
||||
| React.FC<IHostActivityItemComponentPropsWithShowDetails>
|
||||
> = {
|
||||
[ActivityType.RanScript]: RanScriptActivityItem,
|
||||
[ActivityType.LockedHost]: LockedHostActivityItem,
|
||||
[ActivityType.UnlockedHost]: UnlockedHostActivityItem,
|
||||
};
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
import React from "react";
|
||||
|
||||
import { IHostActivityItemComponentProps } from "../../ActivityConfig";
|
||||
import HostActivityItem from "../../HostActivityItem";
|
||||
|
||||
const baseClass = "locked-host-activity-item";
|
||||
|
||||
const LockedHostActivityItem = ({
|
||||
activity,
|
||||
}: IHostActivityItemComponentProps) => {
|
||||
return (
|
||||
<HostActivityItem className={baseClass} activity={activity}>
|
||||
<b>{activity.actor_full_name}</b> locked this host.
|
||||
</HostActivityItem>
|
||||
);
|
||||
};
|
||||
|
||||
export default LockedHostActivityItem;
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from "./LockedHostActivityItem";
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
import React from "react";
|
||||
|
||||
import { formatScriptNameForActivityItem } from "utilities/helpers";
|
||||
|
||||
import HostActivityItem from "../../HostActivityItem";
|
||||
import { IHostActivityItemComponentPropsWithShowDetails } from "../../ActivityConfig";
|
||||
import ShowDetailsButton from "../../ShowDetailsButton";
|
||||
|
||||
const baseClass = "ran-script-activity-item";
|
||||
|
||||
const RanScriptActivityItem = ({
|
||||
activity,
|
||||
onShowDetails,
|
||||
}: IHostActivityItemComponentPropsWithShowDetails) => {
|
||||
return (
|
||||
<HostActivityItem className={baseClass} activity={activity}>
|
||||
<b>{activity.actor_full_name}</b>
|
||||
<>
|
||||
{" "}
|
||||
ran {formatScriptNameForActivityItem(activity.details?.script_name)} on
|
||||
this host.{" "}
|
||||
<ShowDetailsButton activity={activity} onShowDetails={onShowDetails} />
|
||||
</>
|
||||
</HostActivityItem>
|
||||
);
|
||||
};
|
||||
|
||||
export default RanScriptActivityItem;
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from "./RanScriptActivityItem";
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
import React from "react";
|
||||
|
||||
import { IHostActivityItemComponentProps } from "../../ActivityConfig";
|
||||
import HostActivityItem from "../../HostActivityItem";
|
||||
|
||||
const baseClass = "unlocked-host-activity-item";
|
||||
|
||||
const UnlockedHostActivityItem = ({
|
||||
activity,
|
||||
}: IHostActivityItemComponentProps) => {
|
||||
return (
|
||||
<HostActivityItem className={baseClass} activity={activity}>
|
||||
<b>{activity.actor_full_name}</b> unlocked this host.
|
||||
</HostActivityItem>
|
||||
);
|
||||
};
|
||||
|
||||
export default UnlockedHostActivityItem;
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from "./UnlockedHostActivityItem";
|
||||
|
|
@ -0,0 +1,94 @@
|
|||
import React from "react";
|
||||
import ReactTooltip from "react-tooltip";
|
||||
import { formatDistanceToNowStrict } from "date-fns";
|
||||
import classnames from "classnames";
|
||||
|
||||
import { IActivity } from "interfaces/activity";
|
||||
import {
|
||||
addGravatarUrlToResource,
|
||||
internationalTimeFormat,
|
||||
} from "utilities/helpers";
|
||||
import { DEFAULT_GRAVATAR_LINK } from "utilities/constants";
|
||||
|
||||
import Avatar from "components/Avatar";
|
||||
|
||||
import { COLORS } from "styles/var/colors";
|
||||
|
||||
const baseClass = "host-activity-item";
|
||||
|
||||
interface IHostActivityItemProps {
|
||||
activity: IActivity;
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* A wrapper that will render all the common elements of a host activity item.
|
||||
* This includes the avatar, the created at timestamp, and a dash to separate
|
||||
* the activity items. The `children` will be the specific details of the activity
|
||||
* implemented in the component that uses this wrapper.
|
||||
*/
|
||||
const HostActivityItem = ({
|
||||
activity,
|
||||
children,
|
||||
className,
|
||||
}: IHostActivityItemProps) => {
|
||||
const { actor_email } = activity;
|
||||
const { gravatar_url } = actor_email
|
||||
? addGravatarUrlToResource({ email: actor_email })
|
||||
: { gravatar_url: DEFAULT_GRAVATAR_LINK };
|
||||
|
||||
// wrapped just in case the date string does not parse correctly
|
||||
let activityCreatedAt: Date | null = null;
|
||||
try {
|
||||
activityCreatedAt = new Date(activity.created_at);
|
||||
} catch (e) {
|
||||
activityCreatedAt = null;
|
||||
}
|
||||
|
||||
const classNames = classnames(baseClass, className);
|
||||
|
||||
return (
|
||||
<div className={classNames}>
|
||||
<Avatar
|
||||
className={`${baseClass}__avatar-image`}
|
||||
user={{ gravatar_url }}
|
||||
size="small"
|
||||
hasWhiteBackground
|
||||
/>
|
||||
<div className={`${baseClass}__details-wrapper`}>
|
||||
<div className={"activity-details"}>
|
||||
<span className={`${baseClass}__details-topline`}>
|
||||
<span>{children}</span>
|
||||
</span>
|
||||
<br />
|
||||
<span
|
||||
className={`${baseClass}__details-bottomline`}
|
||||
data-tip
|
||||
data-for={`activity-${activity.id}`}
|
||||
>
|
||||
{activityCreatedAt &&
|
||||
formatDistanceToNowStrict(activityCreatedAt, {
|
||||
addSuffix: true,
|
||||
})}
|
||||
</span>
|
||||
{activityCreatedAt && (
|
||||
<ReactTooltip
|
||||
className="date-tooltip"
|
||||
place="top"
|
||||
type="dark"
|
||||
effect="solid"
|
||||
id={`activity-${activity.id}`}
|
||||
backgroundColor={COLORS["tooltip-bg"]}
|
||||
>
|
||||
{internationalTimeFormat(activityCreatedAt)}
|
||||
</ReactTooltip>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className={`${baseClass}__dash`} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default HostActivityItem;
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
.past-activity {
|
||||
.host-activity-item {
|
||||
display: grid; // Grid system is used to create variable dashed line lengths
|
||||
grid-template-columns: 16px 16px 1fr;
|
||||
grid-template-rows: 32px max-content;
|
||||
|
|
@ -62,4 +62,5 @@
|
|||
padding-bottom: $pad-xxlarge;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from "./HostActivityItem";
|
||||
|
|
@ -1,141 +0,0 @@
|
|||
import React from "react";
|
||||
import ReactTooltip from "react-tooltip";
|
||||
import { formatDistanceToNowStrict } from "date-fns";
|
||||
|
||||
import Avatar from "components/Avatar";
|
||||
import Icon from "components/Icon";
|
||||
import Button from "components/buttons/Button";
|
||||
|
||||
import { COLORS } from "styles/var/colors";
|
||||
import { DEFAULT_GRAVATAR_LINK } from "utilities/constants";
|
||||
import {
|
||||
addGravatarUrlToResource,
|
||||
formatScriptNameForActivityItem,
|
||||
internationalTimeFormat,
|
||||
} from "utilities/helpers";
|
||||
import { IActivity } from "interfaces/activity";
|
||||
import { ShowActivityDetailsHandler } from "../Activity";
|
||||
|
||||
const baseClass = "past-activity";
|
||||
|
||||
interface IPastActivityProps {
|
||||
activity: IActivity;
|
||||
// TODO: To handle clicks for different activity types, this could be refactored as a reducer that
|
||||
// takes the activity and dispatches the relevant show details action based on the activity type
|
||||
onDetailsClick: ShowActivityDetailsHandler;
|
||||
}
|
||||
|
||||
const RanScriptActivityDetails = ({
|
||||
activity,
|
||||
onDetailsClick,
|
||||
}: Pick<IPastActivityProps, "activity" | "onDetailsClick">) => (
|
||||
<span className={`${baseClass}__details-topline`}>
|
||||
<b>{activity.actor_full_name}</b>
|
||||
<>
|
||||
{" "}
|
||||
ran {formatScriptNameForActivityItem(activity.details?.script_name)} on
|
||||
this host.{" "}
|
||||
<Button
|
||||
className={`${baseClass}__show-query-link`}
|
||||
variant="text-link"
|
||||
onClick={() => onDetailsClick?.(activity)}
|
||||
>
|
||||
Show details{" "}
|
||||
<Icon className={`${baseClass}__show-query-icon`} name="eye" />
|
||||
</Button>
|
||||
</>
|
||||
</span>
|
||||
);
|
||||
|
||||
const LockedHostActivityDetails = ({
|
||||
activity,
|
||||
}: Pick<IPastActivityProps, "activity">) => (
|
||||
<span className={`${baseClass}__details-topline`}>
|
||||
<b>{activity.actor_full_name}</b> locked this host.
|
||||
</span>
|
||||
);
|
||||
|
||||
const UnlockedHostActivityDetails = ({
|
||||
activity,
|
||||
}: Pick<IPastActivityProps, "activity">) => (
|
||||
<span className={`${baseClass}__details-topline`}>
|
||||
<b>{activity.actor_full_name}</b>{" "}
|
||||
{activity.details?.host_platform === "darwin"
|
||||
? "viewed the six-digit unlock PIN for"
|
||||
: "unlocked"}{" "}
|
||||
this host.
|
||||
</span>
|
||||
);
|
||||
|
||||
const PastActivityTopline = ({
|
||||
activity,
|
||||
onDetailsClick,
|
||||
}: IPastActivityProps) => {
|
||||
switch (activity.type) {
|
||||
case "ran_script":
|
||||
return (
|
||||
<RanScriptActivityDetails
|
||||
activity={activity}
|
||||
onDetailsClick={onDetailsClick}
|
||||
/>
|
||||
);
|
||||
case "locked_host":
|
||||
return <LockedHostActivityDetails activity={activity} />;
|
||||
case "unlocked_host":
|
||||
return <UnlockedHostActivityDetails activity={activity} />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
// TODO: Combine this with ./UpcomingActivity/UpcomingActivity.tsx and
|
||||
// frontend/pages/DashboardPage/cards/ActivityFeed/ActivityItem/ActivityItem.tsx
|
||||
const PastActivity = ({ activity, onDetailsClick }: IPastActivityProps) => {
|
||||
const { actor_email } = activity;
|
||||
const { gravatar_url } = actor_email
|
||||
? addGravatarUrlToResource({ email: actor_email })
|
||||
: { gravatar_url: DEFAULT_GRAVATAR_LINK };
|
||||
const activityCreatedAt = new Date(activity.created_at);
|
||||
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
<Avatar
|
||||
className={`${baseClass}__avatar-image`}
|
||||
user={{ gravatar_url }}
|
||||
size="small"
|
||||
hasWhiteBackground
|
||||
/>
|
||||
<div className={`${baseClass}__details-wrapper`}>
|
||||
<div className="activity-details">
|
||||
<PastActivityTopline
|
||||
activity={activity}
|
||||
onDetailsClick={onDetailsClick}
|
||||
/>
|
||||
<br />
|
||||
<span
|
||||
className={`${baseClass}__details-bottomline`}
|
||||
data-tip
|
||||
data-for={`activity-${activity.id}`}
|
||||
>
|
||||
{formatDistanceToNowStrict(activityCreatedAt, {
|
||||
addSuffix: true,
|
||||
})}
|
||||
</span>
|
||||
<ReactTooltip
|
||||
className="date-tooltip"
|
||||
place="top"
|
||||
type="dark"
|
||||
effect="solid"
|
||||
id={`activity-${activity.id}`}
|
||||
backgroundColor={COLORS["tooltip-bg"]}
|
||||
>
|
||||
{internationalTimeFormat(activityCreatedAt)}
|
||||
</ReactTooltip>
|
||||
</div>
|
||||
</div>
|
||||
<div className={`${baseClass}__dash`} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PastActivity;
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { default } from "./PastActivity";
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import React from "react";
|
||||
|
||||
import { IActivity } from "interfaces/activity";
|
||||
import { IActivitiesResponse } from "services/entities/activities";
|
||||
import { IPastActivity } from "interfaces/activity";
|
||||
import { IPastActivitiesResponse } from "services/entities/activities";
|
||||
|
||||
// @ts-ignore
|
||||
import FleetIcon from "components/icons/FleetIcon";
|
||||
|
|
@ -9,13 +9,14 @@ import Button from "components/buttons/Button";
|
|||
import DataError from "components/DataError";
|
||||
|
||||
import EmptyFeed from "../EmptyFeed/EmptyFeed";
|
||||
import PastActivity from "../PastActivity/PastActivity";
|
||||
import { ShowActivityDetailsHandler } from "../Activity";
|
||||
|
||||
import { pastActivityComponentMap } from "../ActivityConfig";
|
||||
|
||||
const baseClass = "past-activity-feed";
|
||||
|
||||
interface IPastActivityFeedProps {
|
||||
activities?: IActivitiesResponse;
|
||||
activities?: IPastActivitiesResponse;
|
||||
isError?: boolean;
|
||||
onDetailsClick: ShowActivityDetailsHandler;
|
||||
onNextPage: () => void;
|
||||
|
|
@ -52,9 +53,16 @@ const PastActivityFeed = ({
|
|||
return (
|
||||
<div className={baseClass}>
|
||||
<div>
|
||||
{activitiesList.map((activity: IActivity) => (
|
||||
<PastActivity activity={activity} onDetailsClick={onDetailsClick} />
|
||||
))}
|
||||
{activitiesList.map((activity: IPastActivity) => {
|
||||
const ActivityItemComponent = pastActivityComponentMap[activity.type];
|
||||
return (
|
||||
<ActivityItemComponent
|
||||
key={activity.id}
|
||||
activity={activity}
|
||||
onShowDetails={onDetailsClick}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className={`${baseClass}__pagination`}>
|
||||
<Button
|
||||
|
|
|
|||
|
|
@ -0,0 +1,33 @@
|
|||
import React from "react";
|
||||
|
||||
import { IActivity } from "interfaces/activity";
|
||||
|
||||
import Icon from "components/Icon";
|
||||
import Button from "components/buttons/Button";
|
||||
|
||||
import { ShowActivityDetailsHandler } from "../Activity";
|
||||
|
||||
const baseClass = "show-details-button";
|
||||
|
||||
interface IShowDetailsButtonProps {
|
||||
activity: IActivity;
|
||||
onShowDetails: ShowActivityDetailsHandler;
|
||||
}
|
||||
|
||||
const ShowDetailsButton = ({
|
||||
activity,
|
||||
onShowDetails,
|
||||
}: IShowDetailsButtonProps) => {
|
||||
return (
|
||||
<Button
|
||||
className={baseClass}
|
||||
variant="text-link"
|
||||
onClick={() => onShowDetails?.(activity)}
|
||||
>
|
||||
Show details{" "}
|
||||
<Icon className={`${baseClass}__show-details-icon`} name="eye" />
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
export default ShowDetailsButton;
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
.show-details-button {
|
||||
&__show-details-icon {
|
||||
margin-left: $pad-xsmall;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from "./ShowDetailsButton";
|
||||
|
|
@ -12,7 +12,8 @@
|
|||
}
|
||||
|
||||
&.error {
|
||||
background-color: $ui-error;
|
||||
color: $core-white;
|
||||
background-color: $core-vibrant-red;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -39,6 +39,20 @@ export const DEVICE_STATUS_TAGS: DeviceStatusTagConfig = {
|
|||
generateTooltip: (platform) =>
|
||||
"Host will lock when it comes online. If the host is online, it will lock the next time it checks in to Fleet.",
|
||||
},
|
||||
wiped: {
|
||||
title: "WIPED",
|
||||
tagType: "error",
|
||||
generateTooltip: (platform) =>
|
||||
platform === "darwin"
|
||||
? "Host is wiped. To prevent the host from automatically reenrolling to Fleet, first release the host from Apple Business Manager and then delete the host in Fleet."
|
||||
: "Host is wiped.",
|
||||
},
|
||||
wiping: {
|
||||
title: "WIPE PENDING",
|
||||
tagType: "error",
|
||||
generateTooltip: () =>
|
||||
"Host will wipe when it comes online. If the host is online, it will wipe the next time it checks in to Fleet.",
|
||||
},
|
||||
};
|
||||
|
||||
// We exclude "unlocked" as we dont display a tooltip for it.
|
||||
|
|
@ -66,4 +80,14 @@ export const REFETCH_TOOLTIP_MESSAGES: Record<
|
|||
You can't fetch data from <br /> a locked host.
|
||||
</>
|
||||
),
|
||||
wiping: (
|
||||
<>
|
||||
You can't fetch data from <br /> a wiping host.
|
||||
</>
|
||||
),
|
||||
wiped: (
|
||||
<>
|
||||
You can't fetch data from <br /> a wiped host.
|
||||
</>
|
||||
),
|
||||
} as const;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
/** Helpers used across the host details and my device pages and components. */
|
||||
import { is } from "date-fns/locale";
|
||||
import { HostMdmDeviceStatus, HostMdmPendingAction } from "interfaces/host";
|
||||
import {
|
||||
IHostMdmProfile,
|
||||
|
|
@ -39,7 +38,9 @@ export type HostMdmDeviceStatusUIState =
|
|||
| "unlocked"
|
||||
| "locked"
|
||||
| "unlocking"
|
||||
| "locking";
|
||||
| "locking"
|
||||
| "wiped"
|
||||
| "wiping";
|
||||
|
||||
// Exclude the empty string from HostPendingAction as that doesn't represent a
|
||||
// valid device status.
|
||||
|
|
@ -51,9 +52,11 @@ const API_TO_UI_DEVICE_STATUS_MAP: Record<
|
|||
locked: "locked",
|
||||
unlock: "unlocking",
|
||||
lock: "locking",
|
||||
wiped: "wiped",
|
||||
wipe: "wiping",
|
||||
};
|
||||
|
||||
const deviceUpdatingStates = ["unlocking", "locking"] as const;
|
||||
const deviceUpdatingStates = ["unlocking", "locking", "wiping"] as const;
|
||||
|
||||
/**
|
||||
* Gets the current UI state for the host device status. This helps us know what
|
||||
|
|
@ -74,7 +77,7 @@ export const getHostDeviceStatusUIState = (
|
|||
};
|
||||
|
||||
/**
|
||||
* Helps check if our device status UI state is in an updating state.
|
||||
* Checks if our device status UI state is in an updating state.
|
||||
*/
|
||||
export const isDeviceStatusUpdating = (
|
||||
deviceStatus: HostMdmDeviceStatusUIState
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import endpoints from "utilities/endpoints";
|
||||
import { IActivity } from "interfaces/activity";
|
||||
import { IActivity, IPastActivity } from "interfaces/activity";
|
||||
import sendRequest from "services";
|
||||
import { buildQueryStringFromParams } from "utilities/url";
|
||||
|
||||
|
|
@ -16,6 +16,14 @@ export interface IActivitiesResponse {
|
|||
};
|
||||
}
|
||||
|
||||
export interface IPastActivitiesResponse {
|
||||
activities: IPastActivity[] | null;
|
||||
meta: {
|
||||
has_next_results: boolean;
|
||||
has_previous_results: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export interface IUpcomingActivitiesResponse extends IActivitiesResponse {
|
||||
count: number;
|
||||
}
|
||||
|
|
@ -45,7 +53,7 @@ export default {
|
|||
id: number,
|
||||
page = DEFAULT_PAGE,
|
||||
perPage = DEFAULT_PAGE_SIZE
|
||||
): Promise<IActivitiesResponse> => {
|
||||
): Promise<IPastActivitiesResponse> => {
|
||||
const { HOST_PAST_ACTIVITIES } = endpoints;
|
||||
|
||||
const queryParams = {
|
||||
|
|
|
|||
|
|
@ -397,8 +397,14 @@ export default {
|
|||
const { HOST_LOCK } = endpoints;
|
||||
return sendRequest("POST", HOST_LOCK(id));
|
||||
},
|
||||
|
||||
unlockHost: (id: number): Promise<IUnlockHostResponse> => {
|
||||
const { HOST_UNLOCK } = endpoints;
|
||||
return sendRequest("POST", HOST_UNLOCK(id));
|
||||
},
|
||||
|
||||
wipeHost: (id: number) => {
|
||||
const { HOST_WIPE } = endpoints;
|
||||
return sendRequest("POST", HOST_WIPE(id));
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ export default {
|
|||
HOSTS_TRANSFER_BY_FILTER: `/${API_VERSION}/fleet/hosts/transfer/filter`,
|
||||
HOST_LOCK: (id: number) => `/${API_VERSION}/fleet/hosts/${id}/lock`,
|
||||
HOST_UNLOCK: (id: number) => `/${API_VERSION}/fleet/hosts/${id}/unlock`,
|
||||
HOST_WIPE: (id: number) => `/${API_VERSION}/fleet/hosts/${id}/wipe`,
|
||||
|
||||
INVITES: `/${API_VERSION}/fleet/invites`,
|
||||
LABELS: `/${API_VERSION}/fleet/labels`,
|
||||
|
|
|
|||
|
|
@ -13,10 +13,14 @@ export const isPremiumTier = (config: IConfig): boolean => {
|
|||
return config.license.tier === "premium";
|
||||
};
|
||||
|
||||
export const isMdmEnabledAndConfigured = (config: IConfig): boolean => {
|
||||
export const isMacMdmEnabledAndConfigured = (config: IConfig): boolean => {
|
||||
return Boolean(config.mdm.enabled_and_configured);
|
||||
};
|
||||
|
||||
export const isWindowsMdmEnabledAndConfigured = (config: IConfig): boolean => {
|
||||
return Boolean(config.mdm.windows_enabled_and_configured);
|
||||
};
|
||||
|
||||
export const isGlobalAdmin = (user: IUser): boolean => {
|
||||
return user.global_role === "admin";
|
||||
};
|
||||
|
|
@ -142,7 +146,8 @@ export default {
|
|||
isSandboxMode,
|
||||
isFreeTier,
|
||||
isPremiumTier,
|
||||
isMdmEnabledAndConfigured,
|
||||
isMacMdmEnabledAndConfigured,
|
||||
isWindowsMdmEnabledAndConfigured,
|
||||
isGlobalAdmin,
|
||||
isGlobalMaintainer,
|
||||
isGlobalObserver,
|
||||
|
|
|
|||
|
|
@ -653,6 +653,11 @@ func updateMDMAppleHostDB(
|
|||
return ctxerr.Wrap(ctx, err, "update mdm apple host")
|
||||
}
|
||||
|
||||
// clear any host_mdm_actions following re-enrollment here
|
||||
if _, err := tx.ExecContext(ctx, `DELETE FROM host_mdm_actions WHERE host_id = ?`, hostID); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "error clearing mdm apple host_mdm_actions")
|
||||
}
|
||||
|
||||
if err := upsertMDMAppleHostMDMInfoDB(ctx, tx, appCfg.ServerSettings, false, hostID); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "ingest mdm apple host upsert MDM info")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ func TestMDMApple(t *testing.T) {
|
|||
{"TestMDMAppleConfigProfileHash", testMDMAppleConfigProfileHash},
|
||||
{"TestResetMDMAppleEnrollment", testResetMDMAppleEnrollment},
|
||||
{"TestMDMAppleDeleteHostDEPAssignments", testMDMAppleDeleteHostDEPAssignments},
|
||||
{"CleanMacOSMDMLock", testCleanMacOSMDMLock},
|
||||
{"LockUnlockWipeMacOS", testLockUnlockWipeMacOS},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
|
|
@ -4416,18 +4416,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"),
|
||||
|
|
@ -4439,11 +4430,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)
|
||||
|
|
@ -4457,18 +4448,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, host.FleetPlatform(), 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) {
|
||||
|
|
|
|||
|
|
@ -784,8 +784,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 = `
|
||||
|
|
@ -1526,9 +1524,9 @@ func filterHostsByVulnerability(sqlstmt string, opt fleet.HostListOptions, param
|
|||
SELECT hs.host_id FROM host_software hs
|
||||
JOIN software_cve sc ON sc.software_id = hs.software_id
|
||||
WHERE sc.cve = ?
|
||||
|
||||
|
||||
UNION
|
||||
|
||||
|
||||
SELECT hos.host_id FROM host_operating_system hos
|
||||
JOIN operating_system_vulnerabilities osv ON osv.operating_system_id = hos.os_id
|
||||
WHERE osv.cve = ?)`
|
||||
|
|
@ -1798,6 +1796,11 @@ func (ds *Datastore) EnrollOrbit(ctx context.Context, isMDMEnabled bool, hostInf
|
|||
}
|
||||
host.ID = hostID
|
||||
|
||||
// clear any host_mdm_actions following re-enrollment here
|
||||
if _, err := tx.ExecContext(ctx, `DELETE FROM host_mdm_actions WHERE host_id = ?`, hostID); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "orbit enroll error clearing host_mdm_actions")
|
||||
}
|
||||
|
||||
case errors.Is(err, sql.ErrNoRows):
|
||||
zeroTime := time.Unix(0, 0).Add(24 * time.Hour)
|
||||
// Create new host record. We always create newly enrolled hosts with refetch_requested = true
|
||||
|
|
@ -1920,6 +1923,11 @@ func (ds *Datastore) EnrollHost(ctx context.Context, isMDMEnabled bool, osqueryH
|
|||
return ctxerr.Wrap(ctx, err, "cleanup policy membership on re-enroll")
|
||||
}
|
||||
|
||||
// clear any host_mdm_actions following re-enrollment here
|
||||
if _, err := tx.ExecContext(ctx, `DELETE FROM host_mdm_actions WHERE host_id = ?`, matchedID); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "error clearing host_mdm_actions")
|
||||
}
|
||||
|
||||
// Update existing host record
|
||||
sqlUpdate := `
|
||||
UPDATE hosts
|
||||
|
|
|
|||
|
|
@ -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,27 @@ 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,
|
||||
fleet_platform
|
||||
)
|
||||
VALUES (?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
wipe_ref = VALUES(wipe_ref)`
|
||||
|
||||
if _, err := tx.ExecContext(ctx, stmt, host.ID, cmd.CommandUUID, host.FleetPlatform()); 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) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,25 @@
|
|||
package tables
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func init() {
|
||||
MigrationClient.AddMigration(Up_20240301173035, Down_20240301173035)
|
||||
}
|
||||
|
||||
func Up_20240301173035(tx *sql.Tx) error {
|
||||
_, err := tx.Exec(`
|
||||
ALTER TABLE host_mdm_actions
|
||||
ADD COLUMN fleet_platform VARCHAR(255) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT ''
|
||||
`)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to alter host_mdm_actions table: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func Down_20240301173035(tx *sql.Tx) error {
|
||||
return nil
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
package tables
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestUp_20240301173035(t *testing.T) {
|
||||
db := applyUpToPrev(t)
|
||||
|
||||
// create an existing host_mdm_actions row
|
||||
_, err := db.Exec("INSERT INTO host_mdm_actions (host_id, lock_ref) VALUES (1, 'a')")
|
||||
require.NoError(t, err)
|
||||
|
||||
applyNext(t, db)
|
||||
|
||||
var hostActions []struct {
|
||||
HostID uint `db:"host_id"`
|
||||
LockRef *string `db:"lock_ref"`
|
||||
FleetPlatform string `db:"fleet_platform"`
|
||||
}
|
||||
|
||||
// fleet platform is left empty for pre-existing rows
|
||||
err = sqlx.Select(db, &hostActions, `SELECT host_id, lock_ref, fleet_platform FROM host_mdm_actions`)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, hostActions, 1)
|
||||
require.Equal(t, uint(1), hostActions[0].HostID)
|
||||
require.NotNil(t, hostActions[0].LockRef)
|
||||
require.Equal(t, "a", *hostActions[0].LockRef)
|
||||
require.Empty(t, hostActions[0].FleetPlatform)
|
||||
}
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -86,28 +86,26 @@ func (s *NanoMDMStorage) EnqueueDeviceLockCommand(
|
|||
cmd *mdm.Command,
|
||||
pin string,
|
||||
) error {
|
||||
|
||||
return withRetryTxx(ctx, s.db, func(tx sqlx.ExtContext) error {
|
||||
if err := enqueueCommandDB(ctx, tx, []string{host.UUID}, cmd); err != nil {
|
||||
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,
|
||||
fleet_platform
|
||||
)
|
||||
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 {
|
||||
if _, err := tx.ExecContext(ctx, stmt, host.ID, cmd.CommandUUID, pin, host.FleetPlatform()); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "modifying host_mdm_actions for DeviceLock")
|
||||
}
|
||||
|
||||
|
|
@ -115,6 +113,31 @@ 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,
|
||||
fleet_platform
|
||||
)
|
||||
VALUES (?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
wipe_ref = VALUES(wipe_ref)`
|
||||
|
||||
if _, err := tx.ExecContext(ctx, stmt, host.ID, cmd.CommandUUID, host.FleetPlatform()); 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)
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -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")
|
||||
}
|
||||
|
|
@ -146,7 +146,6 @@ func (ds *Datastore) SetHostScriptExecutionResult(ctx context.Context, result *f
|
|||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -557,13 +556,14 @@ 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,
|
||||
wipe_ref,
|
||||
unlock_ref,
|
||||
unlock_pin
|
||||
unlock_pin,
|
||||
fleet_platform
|
||||
FROM
|
||||
host_mdm_actions
|
||||
WHERE
|
||||
|
|
@ -571,16 +571,18 @@ func (ds *Datastore) GetHostLockWipeStatus(ctx context.Context, hostID uint, fle
|
|||
`
|
||||
|
||||
var mdmActions struct {
|
||||
LockRef *string `db:"lock_ref"`
|
||||
WipeRef *string `db:"wipe_ref"`
|
||||
UnlockRef *string `db:"unlock_ref"`
|
||||
UnlockPIN *string `db:"unlock_pin"`
|
||||
LockRef *string `db:"lock_ref"`
|
||||
WipeRef *string `db:"wipe_ref"`
|
||||
UnlockRef *string `db:"unlock_ref"`
|
||||
UnlockPIN *string `db:"unlock_pin"`
|
||||
FleetPlatform string `db:"fleet_platform"`
|
||||
}
|
||||
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.
|
||||
|
|
@ -589,6 +591,14 @@ func (ds *Datastore) GetHostLockWipeStatus(ctx context.Context, hostID uint, fle
|
|||
return nil, ctxerr.Wrap(ctx, err, "get host lock/wipe status")
|
||||
}
|
||||
|
||||
// if we have a fleet platform stored in host_mdm_actions, use it instead of
|
||||
// the host.FleetPlatform() because the platform can be overwritten with an
|
||||
// unknown OS name when a Wipe gets executed.
|
||||
if mdmActions.FleetPlatform != "" {
|
||||
fleetPlatform = mdmActions.FleetPlatform
|
||||
status.HostFleetPlatform = fleetPlatform
|
||||
}
|
||||
|
||||
switch fleetPlatform {
|
||||
case "darwin":
|
||||
if mdmActions.UnlockPIN != nil {
|
||||
|
|
@ -608,34 +618,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,13 +653,92 @@ 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 {
|
||||
func (ds *Datastore) LockHostViaScript(ctx context.Context, request *fleet.HostScriptRequestPayload, hostFleetPlatform string) error {
|
||||
var res *fleet.HostScriptResult
|
||||
return ds.withRetryTxx(ctx, func(tx sqlx.ExtContext) error {
|
||||
var err error
|
||||
|
|
@ -680,9 +757,9 @@ func (ds *Datastore) LockHostViaScript(ctx context.Context, request *fleet.HostS
|
|||
(
|
||||
host_id,
|
||||
lock_ref,
|
||||
unlock_ref
|
||||
fleet_platform
|
||||
)
|
||||
VALUES (?,?,NULL)
|
||||
VALUES (?,?,?)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
lock_ref = VALUES(lock_ref)
|
||||
`
|
||||
|
|
@ -690,6 +767,7 @@ func (ds *Datastore) LockHostViaScript(ctx context.Context, request *fleet.HostS
|
|||
_, err = tx.ExecContext(ctx, stmt,
|
||||
request.HostID,
|
||||
res.ExecutionID,
|
||||
hostFleetPlatform,
|
||||
)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "lock host via script update mdm actions")
|
||||
|
|
@ -701,7 +779,7 @@ func (ds *Datastore) LockHostViaScript(ctx context.Context, request *fleet.HostS
|
|||
|
||||
// UnlockHostViaScript will create the script execution request and update
|
||||
// host_mdm_actions in a single transaction.
|
||||
func (ds *Datastore) UnlockHostViaScript(ctx context.Context, request *fleet.HostScriptRequestPayload) error {
|
||||
func (ds *Datastore) UnlockHostViaScript(ctx context.Context, request *fleet.HostScriptRequestPayload, hostFleetPlatform string) error {
|
||||
var res *fleet.HostScriptResult
|
||||
return ds.withRetryTxx(ctx, func(tx sqlx.ExtContext) error {
|
||||
var err error
|
||||
|
|
@ -720,9 +798,9 @@ func (ds *Datastore) UnlockHostViaScript(ctx context.Context, request *fleet.Hos
|
|||
(
|
||||
host_id,
|
||||
unlock_ref,
|
||||
lock_ref
|
||||
fleet_platform
|
||||
)
|
||||
VALUES (?,?,NULL)
|
||||
VALUES (?,?,?)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
unlock_ref = VALUES(unlock_ref),
|
||||
unlock_pin = NULL
|
||||
|
|
@ -731,6 +809,7 @@ func (ds *Datastore) UnlockHostViaScript(ctx context.Context, request *fleet.Hos
|
|||
_, err = tx.ExecContext(ctx, stmt,
|
||||
request.HostID,
|
||||
res.ExecutionID,
|
||||
hostFleetPlatform,
|
||||
)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "unlock host via script update mdm actions")
|
||||
|
|
@ -740,14 +819,55 @@ func (ds *Datastore) UnlockHostViaScript(ctx context.Context, request *fleet.Hos
|
|||
})
|
||||
}
|
||||
|
||||
func (ds *Datastore) UnlockHostManually(ctx context.Context, hostID uint, ts time.Time) error {
|
||||
// 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, hostFleetPlatform string) 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,
|
||||
fleet_platform
|
||||
)
|
||||
VALUES (?,?,?)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
wipe_ref = VALUES(wipe_ref)
|
||||
`
|
||||
|
||||
_, err = tx.ExecContext(ctx, stmt,
|
||||
request.HostID,
|
||||
res.ExecutionID,
|
||||
hostFleetPlatform,
|
||||
)
|
||||
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, hostFleetPlatform string, ts time.Time) error {
|
||||
const stmt = `
|
||||
INSERT INTO host_mdm_actions
|
||||
(
|
||||
host_id,
|
||||
unlock_ref
|
||||
unlock_ref,
|
||||
fleet_platform
|
||||
)
|
||||
VALUES (?, ?)
|
||||
VALUES (?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
-- do not overwrite if a value is already set
|
||||
unlock_ref = IF(unlock_ref IS NULL, VALUES(unlock_ref), unlock_ref)
|
||||
|
|
@ -758,16 +878,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)
|
||||
_, err := ds.writer(ctx).ExecContext(ctx, stmt, hostID, unlockRef, hostFleetPlatform)
|
||||
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 +901,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 {
|
||||
|
|
@ -730,12 +731,12 @@ func testLockHostViaScript(t *testing.T, ds *Datastore) {
|
|||
ScriptContents: script,
|
||||
UserID: &user.ID,
|
||||
SyncRequest: false,
|
||||
})
|
||||
}, "windows")
|
||||
|
||||
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())
|
||||
|
|
@ -781,12 +782,12 @@ func testUnlockHostViaScript(t *testing.T, ds *Datastore) {
|
|||
ScriptContents: script,
|
||||
UserID: &user.ID,
|
||||
SyncRequest: false,
|
||||
})
|
||||
}, "windows")
|
||||
|
||||
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
|
||||
|
|
@ -834,10 +835,10 @@ func testLockUnlockViaScripts(t *testing.T, ds *Datastore) {
|
|||
ScriptContents: "lock",
|
||||
UserID: &user.ID,
|
||||
SyncRequest: false,
|
||||
})
|
||||
}, platform)
|
||||
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)
|
||||
|
||||
|
|
@ -859,10 +860,10 @@ func testLockUnlockViaScripts(t *testing.T, ds *Datastore) {
|
|||
ScriptContents: "unlock",
|
||||
UserID: &user.ID,
|
||||
SyncRequest: false,
|
||||
})
|
||||
}, platform)
|
||||
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)
|
||||
|
||||
|
|
@ -885,10 +886,10 @@ func testLockUnlockViaScripts(t *testing.T, ds *Datastore) {
|
|||
ScriptContents: "unlock",
|
||||
UserID: &user.ID,
|
||||
SyncRequest: false,
|
||||
})
|
||||
}, platform)
|
||||
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)
|
||||
|
||||
|
|
@ -911,10 +912,10 @@ func testLockUnlockViaScripts(t *testing.T, ds *Datastore) {
|
|||
ScriptContents: "lock",
|
||||
UserID: &user.ID,
|
||||
SyncRequest: false,
|
||||
})
|
||||
}, platform)
|
||||
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,
|
||||
}, platform)
|
||||
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,
|
||||
}, platform)
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -938,19 +1023,19 @@ func testLockUnlockManually(t *testing.T, ds *Datastore) {
|
|||
|
||||
twoDaysAgo := time.Now().AddDate(0, 0, -2).UTC()
|
||||
today := time.Now().UTC()
|
||||
err := ds.UnlockHostManually(ctx, 1, twoDaysAgo)
|
||||
err := ds.UnlockHostManually(ctx, 1, "darwin", 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)
|
||||
|
||||
// if the unlock request already exists, it is not overwritten by subsequent
|
||||
// requests
|
||||
err = ds.UnlockHostManually(ctx, 1, today)
|
||||
err = ds.UnlockHostManually(ctx, 1, "darwin", 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)
|
||||
|
|
@ -961,9 +1046,9 @@ func testLockUnlockManually(t *testing.T, ds *Datastore) {
|
|||
_, err := tx.ExecContext(ctx, "INSERT INTO host_mdm_actions (host_id) VALUES (2)")
|
||||
return err
|
||||
})
|
||||
err = ds.UnlockHostManually(ctx, 2, today)
|
||||
err = ds.UnlockHostManually(ctx, 2, "darwin", 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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1312,24 +1312,38 @@ 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
|
||||
LockHostViaScript(ctx context.Context, request *HostScriptRequestPayload) error
|
||||
LockHostViaScript(ctx context.Context, request *HostScriptRequestPayload, hostFleetPlatform string) error
|
||||
|
||||
// UnlockHostViaScript sends a script to unlock a host and updates the
|
||||
// states in host_mdm_actions
|
||||
UnlockHostViaScript(ctx context.Context, request *HostScriptRequestPayload) error
|
||||
UnlockHostViaScript(ctx context.Context, request *HostScriptRequestPayload, hostFleetPlatform string) error
|
||||
|
||||
// UnlockHostmanually records a request to unlock a host that requires manual
|
||||
// intervention (such as for macOS). It indicates the an unlock request is
|
||||
// pending.
|
||||
UnlockHostManually(ctx context.Context, hostID uint, ts time.Time) error
|
||||
UnlockHostManually(ctx context.Context, hostID uint, hostFleetPlatform string, ts time.Time) error
|
||||
|
||||
// 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, hostFleetPlatform string) 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
|
||||
|
|
@ -1337,6 +1351,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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -836,16 +836,22 @@ 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
|
||||
type LockHostViaScriptFunc func(ctx context.Context, request *fleet.HostScriptRequestPayload, hostFleetPlatform string) error
|
||||
|
||||
type UnlockHostViaScriptFunc func(ctx context.Context, request *fleet.HostScriptRequestPayload) error
|
||||
type UnlockHostViaScriptFunc func(ctx context.Context, request *fleet.HostScriptRequestPayload, hostFleetPlatform string) error
|
||||
|
||||
type UnlockHostManuallyFunc func(ctx context.Context, hostID uint, ts time.Time) error
|
||||
type UnlockHostManuallyFunc func(ctx context.Context, hostID uint, hostFleetPlatform string, ts time.Time) error
|
||||
|
||||
type CleanMacOSMDMLockFunc func(ctx context.Context, hostUUID string) error
|
||||
|
||||
type WipeHostViaScriptFunc func(ctx context.Context, request *fleet.HostScriptRequestPayload, hostFleetPlatform string) 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
|
||||
|
|
@ -2089,6 +2095,15 @@ type DataStore struct {
|
|||
CleanMacOSMDMLockFunc CleanMacOSMDMLockFunc
|
||||
CleanMacOSMDMLockFuncInvoked bool
|
||||
|
||||
WipeHostViaScriptFunc WipeHostViaScriptFunc
|
||||
WipeHostViaScriptFuncInvoked bool
|
||||
|
||||
WipeHostViaWindowsMDMFunc WipeHostViaWindowsMDMFunc
|
||||
WipeHostViaWindowsMDMFuncInvoked bool
|
||||
|
||||
UpdateHostLockWipeStatusFromAppleMDMResultFunc UpdateHostLockWipeStatusFromAppleMDMResultFunc
|
||||
UpdateHostLockWipeStatusFromAppleMDMResultFuncInvoked bool
|
||||
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
|
|
@ -4955,32 +4970,32 @@ 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 {
|
||||
func (s *DataStore) LockHostViaScript(ctx context.Context, request *fleet.HostScriptRequestPayload, hostFleetPlatform string) error {
|
||||
s.mu.Lock()
|
||||
s.LockHostViaScriptFuncInvoked = true
|
||||
s.mu.Unlock()
|
||||
return s.LockHostViaScriptFunc(ctx, request)
|
||||
return s.LockHostViaScriptFunc(ctx, request, hostFleetPlatform)
|
||||
}
|
||||
|
||||
func (s *DataStore) UnlockHostViaScript(ctx context.Context, request *fleet.HostScriptRequestPayload) error {
|
||||
func (s *DataStore) UnlockHostViaScript(ctx context.Context, request *fleet.HostScriptRequestPayload, hostFleetPlatform string) error {
|
||||
s.mu.Lock()
|
||||
s.UnlockHostViaScriptFuncInvoked = true
|
||||
s.mu.Unlock()
|
||||
return s.UnlockHostViaScriptFunc(ctx, request)
|
||||
return s.UnlockHostViaScriptFunc(ctx, request, hostFleetPlatform)
|
||||
}
|
||||
|
||||
func (s *DataStore) UnlockHostManually(ctx context.Context, hostID uint, ts time.Time) error {
|
||||
func (s *DataStore) UnlockHostManually(ctx context.Context, hostID uint, hostFleetPlatform string, ts time.Time) error {
|
||||
s.mu.Lock()
|
||||
s.UnlockHostManuallyFuncInvoked = true
|
||||
s.mu.Unlock()
|
||||
return s.UnlockHostManuallyFunc(ctx, hostID, ts)
|
||||
return s.UnlockHostManuallyFunc(ctx, hostID, hostFleetPlatform, ts)
|
||||
}
|
||||
|
||||
func (s *DataStore) CleanMacOSMDMLock(ctx context.Context, hostUUID string) error {
|
||||
|
|
@ -4989,3 +5004,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, hostFleetPlatform string) error {
|
||||
s.mu.Lock()
|
||||
s.WipeHostViaScriptFuncInvoked = true
|
||||
s.mu.Unlock()
|
||||
return s.WipeHostViaScriptFunc(ctx, request, hostFleetPlatform)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -388,3 +388,11 @@ func (c *Client) MDMUnlockHost(hostID uint) (string, error) {
|
|||
}
|
||||
return response.UnlockPIN, nil
|
||||
}
|
||||
|
||||
func (c *Client) MDMWipeHost(hostID uint) error {
|
||||
var response wipeHostResponse
|
||||
if err := c.authenticatedRequest(nil, "POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/wipe", hostID), &response); err != nil {
|
||||
return fmt.Errorf("wipe host request: %w", err)
|
||||
}
|
||||
return 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
|
||||
|
|
|
|||
|
|
@ -1103,7 +1103,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")
|
||||
}
|
||||
|
|
@ -1114,10 +1114,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 {
|
||||
ds.LockHostViaScriptFunc = func(ctx context.Context, request *fleet.HostScriptRequestPayload, platform string) error {
|
||||
return nil
|
||||
}
|
||||
ds.HostLiteFunc = func(ctx context.Context, hostID uint) (*fleet.Host, error) {
|
||||
if hostID == 1 {
|
||||
if hostID == teamHostID {
|
||||
return teamHost, nil
|
||||
}
|
||||
|
||||
|
|
@ -1505,7 +1510,7 @@ func TestLockUnlockHostAuth(t *testing.T) {
|
|||
ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error {
|
||||
return nil
|
||||
}
|
||||
ds.UnlockHostManuallyFunc = func(ctx context.Context, hostID uint, ts time.Time) error {
|
||||
ds.UnlockHostManuallyFunc = func(ctx context.Context, hostID uint, platform string, ts time.Time) error {
|
||||
return 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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5395,9 +5395,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() {
|
||||
|
|
|
|||
|
|
@ -6867,7 +6867,7 @@ func (s *integrationEnterpriseTestSuite) TestAllSoftwareTitles() {
|
|||
)
|
||||
}
|
||||
|
||||
func (s *integrationEnterpriseTestSuite) TestLockUnlockWindowsLinux() {
|
||||
func (s *integrationEnterpriseTestSuite) TestLockUnlockWipeWindowsLinux() {
|
||||
ctx := context.Background()
|
||||
t := s.T()
|
||||
|
||||
|
|
@ -6889,19 +6889,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
|
||||
|
|
@ -6923,6 +6926,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
|
||||
|
|
|
|||
|
|
@ -11449,12 +11449,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} {
|
||||
|
|
@ -11487,7 +11490,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
|
||||
|
|
@ -11504,6 +11507,10 @@ func (s *integrationMDMTestSuite) TestLockUnlockWindowsLinux() {
|
|||
|
||||
// try to lock the host again
|
||||
s.Do("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/lock", host.ID), nil, http.StatusConflict)
|
||||
// try to wipe a locked host
|
||||
res = s.Do("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/wipe", host.ID), nil, http.StatusUnprocessableEntity)
|
||||
errMsg = extractServerErrorText(res.Body)
|
||||
require.Contains(t, errMsg, "Host cannot be wiped until it is unlocked.")
|
||||
|
||||
// unlock the host
|
||||
s.Do("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/unlock", host.ID), nil, http.StatusNoContent)
|
||||
|
|
@ -11521,7 +11528,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",
|
||||
|
|
@ -11534,10 +11541,267 @@ 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 lock/unlock the host fails
|
||||
res = s.Do("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/lock", host.ID), nil, http.StatusUnprocessableEntity)
|
||||
errMsg = extractServerErrorText(res.Body)
|
||||
require.Contains(t, errMsg, "Cannot process lock requests once host is wiped.")
|
||||
res = s.Do("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/unlock", host.ID), nil, http.StatusUnprocessableEntity)
|
||||
errMsg = extractServerErrorText(res.Body)
|
||||
require.Contains(t, errMsg, "Cannot process unlock requests once host is wiped.")
|
||||
|
||||
// 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)
|
||||
|
||||
// re-enroll the host, simulating that another user received the wiped host
|
||||
newOrbitKey := uuid.New().String()
|
||||
newHost, err := s.ds.EnrollOrbit(ctx, true, fleet.OrbitHostInfo{
|
||||
HardwareUUID: *host.OsqueryHostID,
|
||||
HardwareSerial: host.HardwareSerial,
|
||||
}, newOrbitKey, nil)
|
||||
require.NoError(t, err)
|
||||
// it re-enrolled using the same host record
|
||||
require.Equal(t, host.ID, newHost.ID)
|
||||
|
||||
// refresh the host's status, it is back to 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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
// try to wipe a locked host
|
||||
res = s.Do("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/wipe", host.ID), nil, http.StatusUnprocessableEntity)
|
||||
errMsg = extractServerErrorText(res.Body)
|
||||
require.Contains(t, errMsg, "Host cannot be wiped until it is unlocked.")
|
||||
|
||||
// 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 lock/unlock the host fails
|
||||
res = s.Do("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/lock", host.ID), nil, http.StatusUnprocessableEntity)
|
||||
errMsg = extractServerErrorText(res.Body)
|
||||
require.Contains(t, errMsg, "Cannot process lock requests once host is wiped.")
|
||||
res = s.Do("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/unlock", host.ID), nil, http.StatusUnprocessableEntity)
|
||||
errMsg = extractServerErrorText(res.Body)
|
||||
require.Contains(t, errMsg, "Cannot process unlock requests once host is wiped.")
|
||||
|
||||
// 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)
|
||||
|
||||
// re-enroll the host, simulating that another user received the wiped host
|
||||
err = mdmClient.Enroll()
|
||||
require.NoError(t, err)
|
||||
|
||||
// refresh the host's status, it is back to 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)
|
||||
}
|
||||
|
||||
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 a new issue