mirror of
https://github.com/fleetdm/fleet
synced 2026-05-24 09:28:54 +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
|
}, 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
|
return &fleet.HostLockWipeStatus{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ func mdmCommand() *cli.Command {
|
||||||
mdmRunCommand(),
|
mdmRunCommand(),
|
||||||
mdmLockCommand(),
|
mdmLockCommand(),
|
||||||
mdmUnlockCommand(),
|
mdmUnlockCommand(),
|
||||||
|
mdmWipeCommand(),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -179,38 +180,11 @@ func mdmLockCommand() *cli.Command {
|
||||||
Action: func(c *cli.Context) error {
|
Action: func(c *cli.Context) error {
|
||||||
hostIdent := c.String("host")
|
hostIdent := c.String("host")
|
||||||
|
|
||||||
if len(hostIdent) == 0 {
|
client, host, err := hostMdmActionSetup(c, hostIdent, "lock")
|
||||||
return errors.New("No host targeted. Please provide --host.")
|
|
||||||
}
|
|
||||||
|
|
||||||
client, err := clientFromCLI(c)
|
|
||||||
if err != nil {
|
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
|
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 {
|
if err := client.MDMLockHost(host.ID); err != nil {
|
||||||
return fmt.Errorf("Failed to lock host: %w", err)
|
return fmt.Errorf("Failed to lock host: %w", err)
|
||||||
}
|
}
|
||||||
|
|
@ -245,38 +219,11 @@ func mdmUnlockCommand() *cli.Command {
|
||||||
Action: func(c *cli.Context) error {
|
Action: func(c *cli.Context) error {
|
||||||
hostIdent := c.String("host")
|
hostIdent := c.String("host")
|
||||||
|
|
||||||
if len(hostIdent) == 0 {
|
client, host, err := hostMdmActionSetup(c, hostIdent, "unlock")
|
||||||
return errors.New("No host targeted. Please provide --host.")
|
|
||||||
}
|
|
||||||
|
|
||||||
client, err := clientFromCLI(c)
|
|
||||||
if err != nil {
|
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
|
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)
|
pin, err := client.MDMUnlockHost(host.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("Failed to unlock host: %w", err)
|
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) {
|
ds.GetHostMDMMacOSSetupFunc = func(ctx context.Context, hostID uint) (*fleet.HostMDMMacOSSetup, error) {
|
||||||
return nil, nil
|
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
|
return &fleet.HostLockWipeStatus{}, nil
|
||||||
}
|
}
|
||||||
ds.ListHostsLiteByUUIDsFunc = func(ctx context.Context, filter fleet.TeamFilter, uuids []string) ([]*fleet.Host, error) {
|
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},
|
MDMInfo: &fleet.HostMDM{Enrolled: true, Name: fleet.WellKnownMDMFleet},
|
||||||
MDM: fleet.MDMHostData{Name: fleet.WellKnownMDMFleet, EnrollmentStatus: ptr.String("On (manual)")},
|
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)
|
hostByUUID := make(map[string]*fleet.Host)
|
||||||
hostsByID := make(map[uint]*fleet.Host)
|
hostsByID := make(map[uint]*fleet.Host)
|
||||||
for _, h := range []*fleet.Host{
|
for _, h := range []*fleet.Host{
|
||||||
|
|
@ -384,6 +399,8 @@ func TestMDMLockCommand(t *testing.T) {
|
||||||
macEnrolledUP,
|
macEnrolledUP,
|
||||||
winEnrolledLP,
|
winEnrolledLP,
|
||||||
macEnrolledLP,
|
macEnrolledLP,
|
||||||
|
winEnrolledWP,
|
||||||
|
macEnrolledWP,
|
||||||
} {
|
} {
|
||||||
hostByUUID[h.UUID] = h
|
hostByUUID[h.UUID] = h
|
||||||
hostsByID[h.ID] = h
|
hostsByID[h.ID] = h
|
||||||
|
|
@ -393,58 +410,28 @@ func TestMDMLockCommand(t *testing.T) {
|
||||||
winEnrolledUP.ID: winEnrolledUP,
|
winEnrolledUP.ID: winEnrolledUP,
|
||||||
macEnrolledUP.ID: macEnrolledUP,
|
macEnrolledUP.ID: macEnrolledUP,
|
||||||
}
|
}
|
||||||
|
|
||||||
lockPending := map[uint]*fleet.Host{
|
lockPending := map[uint]*fleet.Host{
|
||||||
winEnrolledLP.ID: winEnrolledLP,
|
winEnrolledLP.ID: winEnrolledLP,
|
||||||
macEnrolledLP.ID: macEnrolledLP,
|
macEnrolledLP.ID: macEnrolledLP,
|
||||||
}
|
}
|
||||||
|
|
||||||
enqueuer := new(mock.MDMAppleStore)
|
wipePending := map[uint]*fleet.Host{
|
||||||
license := &fleet.LicenseInfo{Tier: fleet.TierPremium, Expiration: time.Now().Add(24 * time.Hour)}
|
winEnrolledWP.ID: winEnrolledWP,
|
||||||
|
macEnrolledWP.ID: macEnrolledWP,
|
||||||
|
}
|
||||||
|
|
||||||
_, ds := runServerWithMockedDS(t, &service.TestServerOpts{
|
ds := setupTestServer(t)
|
||||||
MDMStorage: enqueuer,
|
setupDSMocks(ds, hostByUUID, hostsByID)
|
||||||
MDMPusher: mockPusher{},
|
|
||||||
License: license,
|
// custom ds mocks for these tests
|
||||||
NoCacheDatastore: true,
|
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
|
var status fleet.HostLockWipeStatus
|
||||||
status.HostFleetPlatform = fleetPlatform
|
status.HostFleetPlatform = fleetPlatform
|
||||||
|
|
||||||
if _, ok := unlockPending[hostID]; ok {
|
if _, ok := unlockPending[host.ID]; ok {
|
||||||
if fleetPlatform == "darwin" {
|
if fleetPlatform == "darwin" {
|
||||||
status.UnlockPIN = "1234"
|
status.UnlockPIN = "1234"
|
||||||
status.UnlockRequestedAt = time.Now()
|
status.UnlockRequestedAt = time.Now()
|
||||||
|
|
@ -454,7 +441,7 @@ func TestMDMLockCommand(t *testing.T) {
|
||||||
status.UnlockScript = &fleet.HostScriptResult{}
|
status.UnlockScript = &fleet.HostScriptResult{}
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, ok := lockPending[hostID]; ok {
|
if _, ok := lockPending[host.ID]; ok {
|
||||||
if fleetPlatform == "darwin" {
|
if fleetPlatform == "darwin" {
|
||||||
status.LockMDMCommand = &fleet.MDMCommand{}
|
status.LockMDMCommand = &fleet.MDMCommand{}
|
||||||
return &status, nil
|
return &status, nil
|
||||||
|
|
@ -463,38 +450,24 @@ func TestMDMLockCommand(t *testing.T) {
|
||||||
status.LockScript = &fleet.HostScriptResult{}
|
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
|
return &status, 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) {
|
|
||||||
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
appCfgAllMDM := &fleet.AppConfig{MDM: fleet.MDM{EnabledAndConfigured: true, WindowsEnabledAndConfigured: true}}
|
appCfgAllMDM, appCfgWinMDM, appCfgMacMDM, appCfgNoMDM := setupAppConigs()
|
||||||
appCfgWinMDM := &fleet.AppConfig{MDM: fleet.MDM{WindowsEnabledAndConfigured: true}}
|
|
||||||
appCfgMacMDM := &fleet.AppConfig{MDM: fleet.MDM{EnabledAndConfigured: true}}
|
|
||||||
appCfgNoMDM := &fleet.AppConfig{MDM: fleet.MDM{}}
|
|
||||||
|
|
||||||
successfulOutput := func(ident string) string {
|
successfulOutput := func(ident string) string {
|
||||||
return fmt.Sprintf(`
|
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 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 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 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 {
|
runTestCases(t, ds, "lock", successfulOutput, 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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMDMUnlockCommand(t *testing.T) {
|
func TestMDMUnlockCommand(t *testing.T) {
|
||||||
|
|
@ -614,7 +573,6 @@ func TestMDMUnlockCommand(t *testing.T) {
|
||||||
MDMInfo: &fleet.HostMDM{Enrolled: true, Name: fleet.WellKnownMDMFleet},
|
MDMInfo: &fleet.HostMDM{Enrolled: true, Name: fleet.WellKnownMDMFleet},
|
||||||
MDM: fleet.MDMHostData{Name: fleet.WellKnownMDMFleet, EnrollmentStatus: ptr.String("On (manual)")},
|
MDM: fleet.MDMHostData{Name: fleet.WellKnownMDMFleet, EnrollmentStatus: ptr.String("On (manual)")},
|
||||||
}
|
}
|
||||||
|
|
||||||
winEnrolledLP := &fleet.Host{
|
winEnrolledLP := &fleet.Host{
|
||||||
ID: 10,
|
ID: 10,
|
||||||
UUID: "win-enrolled-lp",
|
UUID: "win-enrolled-lp",
|
||||||
|
|
@ -629,6 +587,20 @@ func TestMDMUnlockCommand(t *testing.T) {
|
||||||
MDMInfo: &fleet.HostMDM{Enrolled: true, Name: fleet.WellKnownMDMFleet},
|
MDMInfo: &fleet.HostMDM{Enrolled: true, Name: fleet.WellKnownMDMFleet},
|
||||||
MDM: fleet.MDMHostData{Name: fleet.WellKnownMDMFleet, EnrollmentStatus: ptr.String("On (manual)")},
|
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)
|
hostByUUID := make(map[string]*fleet.Host)
|
||||||
hostsByID := make(map[uint]*fleet.Host)
|
hostsByID := make(map[uint]*fleet.Host)
|
||||||
|
|
@ -644,6 +616,8 @@ func TestMDMUnlockCommand(t *testing.T) {
|
||||||
macEnrolledUP,
|
macEnrolledUP,
|
||||||
winEnrolledLP,
|
winEnrolledLP,
|
||||||
macEnrolledLP,
|
macEnrolledLP,
|
||||||
|
winEnrolledWP,
|
||||||
|
macEnrolledWP,
|
||||||
} {
|
} {
|
||||||
hostByUUID[h.UUID] = h
|
hostByUUID[h.UUID] = h
|
||||||
hostsByID[h.ID] = h
|
hostsByID[h.ID] = h
|
||||||
|
|
@ -664,56 +638,21 @@ func TestMDMUnlockCommand(t *testing.T) {
|
||||||
macEnrolledLP.ID: macEnrolledLP,
|
macEnrolledLP.ID: macEnrolledLP,
|
||||||
}
|
}
|
||||||
|
|
||||||
enqueuer := new(mock.MDMAppleStore)
|
wipePending := map[uint]*fleet.Host{
|
||||||
license := &fleet.LicenseInfo{Tier: fleet.TierPremium, Expiration: time.Now().Add(24 * time.Hour)}
|
winEnrolledWP.ID: winEnrolledWP,
|
||||||
|
macEnrolledWP.ID: macEnrolledWP,
|
||||||
enqueuer.EnqueueDeviceLockCommandFunc = func(ctx context.Context, host *fleet.Host, cmd *mdm.Command, pin string) error {
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_, ds := runServerWithMockedDS(t, &service.TestServerOpts{
|
ds := setupTestServer(t)
|
||||||
MDMStorage: enqueuer,
|
setupDSMocks(ds, hostByUUID, hostsByID)
|
||||||
MDMPusher: mockPusher{},
|
|
||||||
License: license,
|
// custom mocks for these test
|
||||||
NoCacheDatastore: true,
|
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
|
var status fleet.HostLockWipeStatus
|
||||||
status.HostFleetPlatform = fleetPlatform
|
status.HostFleetPlatform = fleetPlatform
|
||||||
if _, ok := locked[hostID]; ok {
|
if _, ok := locked[host.ID]; ok {
|
||||||
if fleetPlatform == "darwin" {
|
if fleetPlatform == "darwin" {
|
||||||
status.LockMDMCommand = &fleet.MDMCommand{}
|
status.LockMDMCommand = &fleet.MDMCommand{}
|
||||||
status.LockMDMCommandResult = &fleet.MDMCommandResult{Status: fleet.MDMAppleStatusAcknowledged}
|
status.LockMDMCommandResult = &fleet.MDMCommandResult{Status: fleet.MDMAppleStatusAcknowledged}
|
||||||
|
|
@ -723,7 +662,7 @@ func TestMDMUnlockCommand(t *testing.T) {
|
||||||
status.LockScript = &fleet.HostScriptResult{ExitCode: ptr.Int64(0)}
|
status.LockScript = &fleet.HostScriptResult{ExitCode: ptr.Int64(0)}
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, ok := unlockPending[hostID]; ok {
|
if _, ok := unlockPending[host.ID]; ok {
|
||||||
if fleetPlatform == "darwin" {
|
if fleetPlatform == "darwin" {
|
||||||
status.UnlockPIN = "1234"
|
status.UnlockPIN = "1234"
|
||||||
status.UnlockRequestedAt = time.Now()
|
status.UnlockRequestedAt = time.Now()
|
||||||
|
|
@ -733,7 +672,7 @@ func TestMDMUnlockCommand(t *testing.T) {
|
||||||
status.UnlockScript = &fleet.HostScriptResult{}
|
status.UnlockScript = &fleet.HostScriptResult{}
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, ok := lockPending[hostID]; ok {
|
if _, ok := lockPending[host.ID]; ok {
|
||||||
if fleetPlatform == "darwin" {
|
if fleetPlatform == "darwin" {
|
||||||
status.LockMDMCommand = &fleet.MDMCommand{}
|
status.LockMDMCommand = &fleet.MDMCommand{}
|
||||||
return &status, nil
|
return &status, nil
|
||||||
|
|
@ -742,41 +681,27 @@ func TestMDMUnlockCommand(t *testing.T) {
|
||||||
status.LockScript = &fleet.HostScriptResult{}
|
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
|
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
|
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
|
|
||||||
}
|
|
||||||
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
appCfgAllMDM := &fleet.AppConfig{MDM: fleet.MDM{EnabledAndConfigured: true, WindowsEnabledAndConfigured: true}}
|
appCfgAllMDM, appCfgWinMDM, appCfgMacMDM, appCfgNoMDM := setupAppConigs()
|
||||||
appCfgWinMDM := &fleet.AppConfig{MDM: fleet.MDM{WindowsEnabledAndConfigured: true}}
|
|
||||||
appCfgMacMDM := &fleet.AppConfig{MDM: fleet.MDM{EnabledAndConfigured: true}}
|
|
||||||
appCfgNoMDM := &fleet.AppConfig{MDM: fleet.MDM{}}
|
|
||||||
|
|
||||||
successfulOutput := func(ident string) string {
|
successfulOutput := func(ident string) string {
|
||||||
h := hostByUUID[ident]
|
h := hostByUUID[ident]
|
||||||
|
|
@ -801,7 +726,7 @@ fleetctl get host %s
|
||||||
}{
|
}{
|
||||||
{appCfgAllMDM, "no flags", nil, `Required flag "host" not set`},
|
{appCfgAllMDM, "no flags", nil, `Required flag "host" not set`},
|
||||||
{appCfgAllMDM, "host flag empty", []string{"--host", ""}, `No host targeted. Please provide --host.`},
|
{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.`},
|
{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 windows", []string{"--host", winEnrolled.UUID}, ""},
|
||||||
{appCfgAllMDM, "valid macos", []string{"--host", macEnrolled.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 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 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 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 {
|
runTestCases(t, ds, "unlock", successfulOutput, cases)
|
||||||
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
|
}
|
||||||
return c.appCfg, nil
|
|
||||||
}
|
func TestMDMWipeCommand(t *testing.T) {
|
||||||
buf, err := runAppNoChecks(append([]string{"mdm", "unlock"}, c.flags...))
|
macEnrolled := &fleet.Host{
|
||||||
if c.wantErr != "" {
|
ID: 1,
|
||||||
require.Error(t, err, c.desc)
|
UUID: "mac-enrolled",
|
||||||
require.ErrorContains(t, err, c.wantErr, c.desc)
|
Platform: "darwin",
|
||||||
} else {
|
MDMInfo: &fleet.HostMDM{Enrolled: true, Name: fleet.WellKnownMDMFleet},
|
||||||
require.NoError(t, err, c.desc)
|
MDM: fleet.MDMHostData{Name: fleet.WellKnownMDMFleet, EnrollmentStatus: ptr.String("On (manual)")},
|
||||||
require.Contains(t, buf.String(), successfulOutput(c.flags[1]), c.desc)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
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 {
|
func writeTmpAppleMDMCmd(t *testing.T, commandName string) string {
|
||||||
|
|
@ -882,3 +1095,116 @@ func writeTmpMobileconfig(t *testing.T, name string) string {
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
return tmpFile.Name()
|
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
|
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
|
return &fleet.HostLockWipeStatus{}, nil
|
||||||
}
|
}
|
||||||
ds.NewHostScriptExecutionRequestFunc = func(ctx context.Context, req *fleet.HostScriptRequestPayload) (*fleet.HostScriptResult, error) {
|
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="title" value="Audit logs">
|
||||||
<meta name="pageOrderInSection" value="1400">
|
<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 (
|
import (
|
||||||
"context"
|
"context"
|
||||||
_ "embed"
|
_ "embed"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
@ -55,21 +56,22 @@ func (svc *Service) LockHost(ctx context.Context, hostID uint) error {
|
||||||
return err
|
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
|
// locking validations are based on the platform of the host
|
||||||
switch host.FleetPlatform() {
|
switch host.FleetPlatform() {
|
||||||
case "darwin":
|
case "darwin":
|
||||||
if err := svc.VerifyMDMAppleConfigured(ctx); err != nil {
|
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")
|
return ctxerr.Wrap(ctx, err, "check macOS MDM enabled")
|
||||||
}
|
}
|
||||||
|
|
||||||
// on macOS, the lock command requires the host to be MDM-enrolled in Fleet
|
// on macOS, the lock command requires the host to be MDM-enrolled in Fleet
|
||||||
hostMDM, err := svc.ds.GetHostMDM(ctx, host.ID)
|
hostMDM, err := svc.ds.GetHostMDM(ctx, host.ID)
|
||||||
if err != nil {
|
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")
|
return ctxerr.Wrap(ctx, err, "get host MDM information")
|
||||||
}
|
}
|
||||||
if !hostMDM.IsFleetEnrolled() {
|
if !hostMDM.IsFleetEnrolled() {
|
||||||
|
|
@ -79,7 +81,9 @@ func (svc *Service) LockHost(ctx context.Context, hostID uint) error {
|
||||||
case "windows", "linux":
|
case "windows", "linux":
|
||||||
if host.FleetPlatform() == "windows" {
|
if host.FleetPlatform() == "windows" {
|
||||||
if err := svc.VerifyMDMWindowsConfigured(ctx); err != nil {
|
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")
|
return ctxerr.Wrap(ctx, err, "check windows MDM enabled")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -94,13 +98,12 @@ func (svc *Service) LockHost(ctx context.Context, hostID uint) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
default:
|
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)))
|
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
|
// if there's a lock, unlock or wipe action pending, do not accept the lock
|
||||||
// request.
|
// request.
|
||||||
lockWipe, err := svc.ds.GetHostLockWipeStatus(ctx, host.ID, host.FleetPlatform())
|
lockWipe, err := svc.ds.GetHostLockWipeStatus(ctx, host)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ctxerr.Wrap(ctx, err, "get host lock/wipe status")
|
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."))
|
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():
|
case lockWipe.IsPendingWipe():
|
||||||
return ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("host_id", "Host has pending wipe request. Cannot process lock requests once host is wiped."))
|
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():
|
case lockWipe.IsLocked():
|
||||||
return ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("host_id", "Host is already locked.").WithStatus(http.StatusConflict))
|
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
|
// be enabled
|
||||||
if host.FleetPlatform() == "windows" {
|
if host.FleetPlatform() == "windows" {
|
||||||
if err := svc.VerifyMDMWindowsConfigured(ctx); err != nil {
|
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")
|
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:
|
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)))
|
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 {
|
if err != nil {
|
||||||
return "", ctxerr.Wrap(ctx, err, "get host lock/wipe status")
|
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."))
|
return "", ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("host_id", "Host has pending unlock request. The host will unlock when it comes online."))
|
||||||
case lockWipe.IsPendingWipe():
|
case lockWipe.IsPendingWipe():
|
||||||
return "", ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("host_id", "Host has pending wipe request. Cannot process unlock requests once host is wiped."))
|
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():
|
case lockWipe.IsUnlocked():
|
||||||
return "", ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("host_id", "Host is already unlocked.").WithStatus(http.StatusConflict))
|
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)
|
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 {
|
func (svc *Service) enqueueLockHostRequest(ctx context.Context, host *fleet.Host, lockStatus *fleet.HostLockWipeStatus) error {
|
||||||
vc, ok := viewer.FromContext(ctx)
|
vc, ok := viewer.FromContext(ctx)
|
||||||
if !ok {
|
if !ok {
|
||||||
|
|
@ -232,7 +331,7 @@ func (svc *Service) enqueueLockHostRequest(ctx context.Context, host *fleet.Host
|
||||||
ScriptContents: string(script),
|
ScriptContents: string(script),
|
||||||
UserID: &vc.User.ID,
|
UserID: &vc.User.ID,
|
||||||
SyncRequest: false,
|
SyncRequest: false,
|
||||||
}); err != nil {
|
}, host.FleetPlatform()); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -260,7 +359,7 @@ func (svc *Service) enqueueUnlockHostRequest(ctx context.Context, host *fleet.Ho
|
||||||
if lockStatus.HostFleetPlatform == "darwin" {
|
if lockStatus.HostFleetPlatform == "darwin" {
|
||||||
// record the unlock request if it was not already recorded
|
// record the unlock request if it was not already recorded
|
||||||
if lockStatus.UnlockRequestedAt.IsZero() {
|
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
|
return "", err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -281,7 +380,7 @@ func (svc *Service) enqueueUnlockHostRequest(ctx context.Context, host *fleet.Ho
|
||||||
ScriptContents: string(script),
|
ScriptContents: string(script),
|
||||||
UserID: &vc.User.ID,
|
UserID: &vc.User.ID,
|
||||||
SyncRequest: false,
|
SyncRequest: false,
|
||||||
}); err != nil {
|
}, host.FleetPlatform()); err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -301,6 +400,59 @@ func (svc *Service) enqueueUnlockHostRequest(ctx context.Context, host *fleet.Ho
|
||||||
return unlockPIN, nil
|
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/..
|
// 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
|
// 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
|
// not possible (not a Go package) and I don't know if those script locations
|
||||||
|
|
@ -316,4 +468,21 @@ var (
|
||||||
linuxLockScript []byte
|
linuxLockScript []byte
|
||||||
//go:embed embedded_scripts/linux_unlock.sh
|
//go:embed embedded_scripts/linux_unlock.sh
|
||||||
linuxUnlockScript []byte
|
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
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: save the pin (first return value) in the database
|
err = svc.mdmAppleCommander.EraseDevice(ctx, host, uuid.New().String())
|
||||||
// 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())
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -106,7 +106,8 @@ type InitialStateType = {
|
||||||
isSandboxMode?: boolean;
|
isSandboxMode?: boolean;
|
||||||
isFreeTier?: boolean;
|
isFreeTier?: boolean;
|
||||||
isPremiumTier?: boolean;
|
isPremiumTier?: boolean;
|
||||||
isMdmEnabledAndConfigured?: boolean;
|
isMacMdmEnabledAndConfigured?: boolean;
|
||||||
|
isWindowsMdmEnabledAndConfigured?: boolean;
|
||||||
isGlobalAdmin?: boolean;
|
isGlobalAdmin?: boolean;
|
||||||
isGlobalMaintainer?: boolean;
|
isGlobalMaintainer?: boolean;
|
||||||
isGlobalObserver?: boolean;
|
isGlobalObserver?: boolean;
|
||||||
|
|
@ -156,7 +157,8 @@ export const initialState = {
|
||||||
isSandboxMode: false,
|
isSandboxMode: false,
|
||||||
isFreeTier: undefined,
|
isFreeTier: undefined,
|
||||||
isPremiumTier: undefined,
|
isPremiumTier: undefined,
|
||||||
isMdmEnabledAndConfigured: undefined,
|
isMacMdmEnabledAndConfigured: undefined,
|
||||||
|
isWindowsMdmEnabledAndConfigured: undefined,
|
||||||
isGlobalAdmin: undefined,
|
isGlobalAdmin: undefined,
|
||||||
isGlobalMaintainer: undefined,
|
isGlobalMaintainer: undefined,
|
||||||
isGlobalObserver: undefined,
|
isGlobalObserver: undefined,
|
||||||
|
|
@ -212,7 +214,12 @@ const setPermissions = (
|
||||||
isSandboxMode: permissions.isSandboxMode(config),
|
isSandboxMode: permissions.isSandboxMode(config),
|
||||||
isFreeTier: permissions.isFreeTier(config),
|
isFreeTier: permissions.isFreeTier(config),
|
||||||
isPremiumTier: permissions.isPremiumTier(config),
|
isPremiumTier: permissions.isPremiumTier(config),
|
||||||
isMdmEnabledAndConfigured: permissions.isMdmEnabledAndConfigured(config),
|
isMacMdmEnabledAndConfigured: permissions.isMacMdmEnabledAndConfigured(
|
||||||
|
config
|
||||||
|
),
|
||||||
|
isWindowsMdmEnabledAndConfigured: permissions.isWindowsMdmEnabledAndConfigured(
|
||||||
|
config
|
||||||
|
),
|
||||||
isGlobalAdmin: permissions.isGlobalAdmin(user),
|
isGlobalAdmin: permissions.isGlobalAdmin(user),
|
||||||
isGlobalMaintainer: permissions.isGlobalMaintainer(user),
|
isGlobalMaintainer: permissions.isGlobalMaintainer(user),
|
||||||
isGlobalObserver: permissions.isGlobalObserver(user),
|
isGlobalObserver: permissions.isGlobalObserver(user),
|
||||||
|
|
@ -365,7 +372,8 @@ const AppProvider = ({ children }: Props): JSX.Element => {
|
||||||
isSandboxMode: state.isSandboxMode,
|
isSandboxMode: state.isSandboxMode,
|
||||||
isFreeTier: state.isFreeTier,
|
isFreeTier: state.isFreeTier,
|
||||||
isPremiumTier: state.isPremiumTier,
|
isPremiumTier: state.isPremiumTier,
|
||||||
isMdmEnabledAndConfigured: state.isMdmEnabledAndConfigured,
|
isMacMdmEnabledAndConfigured: state.isMacMdmEnabledAndConfigured,
|
||||||
|
isWindowsMdmEnabledAndConfigured: state.isWindowsMdmEnabledAndConfigured,
|
||||||
isGlobalAdmin: state.isGlobalAdmin,
|
isGlobalAdmin: state.isGlobalAdmin,
|
||||||
isGlobalMaintainer: state.isGlobalMaintainer,
|
isGlobalMaintainer: state.isGlobalMaintainer,
|
||||||
isGlobalObserver: state.isGlobalObserver,
|
isGlobalObserver: state.isGlobalObserver,
|
||||||
|
|
|
||||||
|
|
@ -66,7 +66,15 @@ export enum ActivityType {
|
||||||
EditedWindowsUpdates = "edited_windows_updates",
|
EditedWindowsUpdates = "edited_windows_updates",
|
||||||
LockedHost = "locked_host",
|
LockedHost = "locked_host",
|
||||||
UnlockedHost = "unlocked_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 {
|
export interface IActivity {
|
||||||
created_at: string;
|
created_at: string;
|
||||||
id: number;
|
id: number;
|
||||||
|
|
@ -77,6 +85,11 @@ export interface IActivity {
|
||||||
type: ActivityType;
|
type: ActivityType;
|
||||||
details?: IActivityDetails;
|
details?: IActivityDetails;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type IPastActivity = Omit<IActivity, "type"> & {
|
||||||
|
type: IHostPastActivityType;
|
||||||
|
};
|
||||||
|
|
||||||
export interface IActivityDetails {
|
export interface IActivityDetails {
|
||||||
pack_id?: number;
|
pack_id?: number;
|
||||||
pack_name?: string;
|
pack_name?: string;
|
||||||
|
|
|
||||||
|
|
@ -157,8 +157,8 @@ interface IMdmMacOsSetup {
|
||||||
bootstrap_package_name: string;
|
bootstrap_package_name: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type HostMdmDeviceStatus = "unlocked" | "locked";
|
export type HostMdmDeviceStatus = "unlocked" | "locked" | "wiped";
|
||||||
export type HostMdmPendingAction = "unlock" | "lock" | "";
|
export type HostMdmPendingAction = "unlock" | "lock" | "wipe" | "";
|
||||||
|
|
||||||
export interface IHostMdmData {
|
export interface IHostMdmData {
|
||||||
encryption_key_available: boolean;
|
encryption_key_available: boolean;
|
||||||
|
|
|
||||||
|
|
@ -1165,4 +1165,17 @@ describe("Activity Feed", () => {
|
||||||
screen.getByText("deleted multiple queries", { exact: false })
|
screen.getByText("deleted multiple queries", { exact: false })
|
||||||
).toBeInTheDocument();
|
).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 = (
|
const getDetail = (
|
||||||
|
|
@ -907,6 +915,9 @@ const getDetail = (
|
||||||
case ActivityType.UnlockedHost: {
|
case ActivityType.UnlockedHost: {
|
||||||
return TAGGED_TEMPLATES.unlockedHost(activity);
|
return TAGGED_TEMPLATES.unlockedHost(activity);
|
||||||
}
|
}
|
||||||
|
case ActivityType.WipedHost: {
|
||||||
|
return TAGGED_TEMPLATES.wipedHost(activity);
|
||||||
|
}
|
||||||
default: {
|
default: {
|
||||||
return TAGGED_TEMPLATES.defaultActivityTemplate(activity);
|
return TAGGED_TEMPLATES.defaultActivityTemplate(activity);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ describe("Integrations Page", () => {
|
||||||
const render = createCustomRenderer({
|
const render = createCustomRenderer({
|
||||||
withBackendMock: true,
|
withBackendMock: true,
|
||||||
context: {
|
context: {
|
||||||
app: { isMdmEnabledAndConfigured: true },
|
app: { isMacMdmEnabledAndConfigured: true },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -94,7 +94,7 @@ describe("Host Actions Dropdown", () => {
|
||||||
const render = createCustomRenderer({
|
const render = createCustomRenderer({
|
||||||
context: {
|
context: {
|
||||||
app: {
|
app: {
|
||||||
isMdmEnabledAndConfigured: true,
|
isMacMdmEnabledAndConfigured: true,
|
||||||
isGlobalAdmin: true,
|
isGlobalAdmin: true,
|
||||||
currentUser: createMockUser(),
|
currentUser: createMockUser(),
|
||||||
},
|
},
|
||||||
|
|
@ -122,7 +122,7 @@ describe("Host Actions Dropdown", () => {
|
||||||
const render = createCustomRenderer({
|
const render = createCustomRenderer({
|
||||||
context: {
|
context: {
|
||||||
app: {
|
app: {
|
||||||
isMdmEnabledAndConfigured: true,
|
isMacMdmEnabledAndConfigured: true,
|
||||||
isGlobalMaintainer: true,
|
isGlobalMaintainer: true,
|
||||||
currentUser: createMockUser(),
|
currentUser: createMockUser(),
|
||||||
},
|
},
|
||||||
|
|
@ -150,7 +150,7 @@ describe("Host Actions Dropdown", () => {
|
||||||
const render = createCustomRenderer({
|
const render = createCustomRenderer({
|
||||||
context: {
|
context: {
|
||||||
app: {
|
app: {
|
||||||
isMdmEnabledAndConfigured: true,
|
isMacMdmEnabledAndConfigured: true,
|
||||||
currentUser: createMockUser({
|
currentUser: createMockUser({
|
||||||
teams: [createMockTeam({ id: 1, role: "admin" })],
|
teams: [createMockTeam({ id: 1, role: "admin" })],
|
||||||
}),
|
}),
|
||||||
|
|
@ -179,7 +179,7 @@ describe("Host Actions Dropdown", () => {
|
||||||
const render = createCustomRenderer({
|
const render = createCustomRenderer({
|
||||||
context: {
|
context: {
|
||||||
app: {
|
app: {
|
||||||
isMdmEnabledAndConfigured: true,
|
isMacMdmEnabledAndConfigured: true,
|
||||||
currentUser: createMockUser({
|
currentUser: createMockUser({
|
||||||
teams: [createMockTeam({ id: 1, role: "maintainer" })],
|
teams: [createMockTeam({ id: 1, role: "maintainer" })],
|
||||||
}),
|
}),
|
||||||
|
|
@ -208,7 +208,7 @@ describe("Host Actions Dropdown", () => {
|
||||||
const render = createCustomRenderer({
|
const render = createCustomRenderer({
|
||||||
context: {
|
context: {
|
||||||
app: {
|
app: {
|
||||||
isMdmEnabledAndConfigured: true,
|
isMacMdmEnabledAndConfigured: true,
|
||||||
currentUser: createMockUser(),
|
currentUser: createMockUser(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -235,7 +235,7 @@ describe("Host Actions Dropdown", () => {
|
||||||
const render = createCustomRenderer({
|
const render = createCustomRenderer({
|
||||||
context: {
|
context: {
|
||||||
app: {
|
app: {
|
||||||
isMdmEnabledAndConfigured: true,
|
isMacMdmEnabledAndConfigured: true,
|
||||||
isGlobalAdmin: true,
|
isGlobalAdmin: true,
|
||||||
currentUser: createMockUser(),
|
currentUser: createMockUser(),
|
||||||
},
|
},
|
||||||
|
|
@ -267,7 +267,7 @@ describe("Host Actions Dropdown", () => {
|
||||||
const render = createCustomRenderer({
|
const render = createCustomRenderer({
|
||||||
context: {
|
context: {
|
||||||
app: {
|
app: {
|
||||||
isMdmEnabledAndConfigured: true,
|
isMacMdmEnabledAndConfigured: true,
|
||||||
isGlobalAdmin: true,
|
isGlobalAdmin: true,
|
||||||
currentUser: createMockUser(),
|
currentUser: createMockUser(),
|
||||||
},
|
},
|
||||||
|
|
@ -402,7 +402,7 @@ describe("Host Actions Dropdown", () => {
|
||||||
context: {
|
context: {
|
||||||
app: {
|
app: {
|
||||||
isPremiumTier: true,
|
isPremiumTier: true,
|
||||||
isMdmEnabledAndConfigured: true,
|
isMacMdmEnabledAndConfigured: true,
|
||||||
isGlobalAdmin: true,
|
isGlobalAdmin: true,
|
||||||
currentUser: createMockUser(),
|
currentUser: createMockUser(),
|
||||||
},
|
},
|
||||||
|
|
@ -431,7 +431,7 @@ describe("Host Actions Dropdown", () => {
|
||||||
context: {
|
context: {
|
||||||
app: {
|
app: {
|
||||||
isPremiumTier: true,
|
isPremiumTier: true,
|
||||||
isMdmEnabledAndConfigured: true,
|
isMacMdmEnabledAndConfigured: true,
|
||||||
isGlobalAdmin: true,
|
isGlobalAdmin: true,
|
||||||
currentUser: createMockUser(),
|
currentUser: createMockUser(),
|
||||||
},
|
},
|
||||||
|
|
@ -460,7 +460,7 @@ describe("Host Actions Dropdown", () => {
|
||||||
context: {
|
context: {
|
||||||
app: {
|
app: {
|
||||||
isPremiumTier: true,
|
isPremiumTier: true,
|
||||||
isMdmEnabledAndConfigured: true,
|
isMacMdmEnabledAndConfigured: true,
|
||||||
isGlobalAdmin: true,
|
isGlobalAdmin: true,
|
||||||
currentUser: createMockUser(),
|
currentUser: createMockUser(),
|
||||||
},
|
},
|
||||||
|
|
@ -491,7 +491,7 @@ describe("Host Actions Dropdown", () => {
|
||||||
context: {
|
context: {
|
||||||
app: {
|
app: {
|
||||||
isPremiumTier: true,
|
isPremiumTier: true,
|
||||||
isMdmEnabledAndConfigured: true,
|
isMacMdmEnabledAndConfigured: true,
|
||||||
isGlobalAdmin: true,
|
isGlobalAdmin: true,
|
||||||
currentUser: createMockUser(),
|
currentUser: createMockUser(),
|
||||||
},
|
},
|
||||||
|
|
@ -520,7 +520,7 @@ describe("Host Actions Dropdown", () => {
|
||||||
context: {
|
context: {
|
||||||
app: {
|
app: {
|
||||||
isPremiumTier: true,
|
isPremiumTier: true,
|
||||||
isMdmEnabledAndConfigured: true,
|
isMacMdmEnabledAndConfigured: true,
|
||||||
isGlobalAdmin: true,
|
isGlobalAdmin: true,
|
||||||
currentUser: createMockUser(),
|
currentUser: createMockUser(),
|
||||||
},
|
},
|
||||||
|
|
@ -549,7 +549,7 @@ describe("Host Actions Dropdown", () => {
|
||||||
context: {
|
context: {
|
||||||
app: {
|
app: {
|
||||||
isPremiumTier: true,
|
isPremiumTier: true,
|
||||||
isMdmEnabledAndConfigured: true,
|
isMacMdmEnabledAndConfigured: true,
|
||||||
isGlobalAdmin: true,
|
isGlobalAdmin: true,
|
||||||
currentUser: createMockUser(),
|
currentUser: createMockUser(),
|
||||||
},
|
},
|
||||||
|
|
@ -578,7 +578,7 @@ describe("Host Actions Dropdown", () => {
|
||||||
context: {
|
context: {
|
||||||
app: {
|
app: {
|
||||||
isPremiumTier: true,
|
isPremiumTier: true,
|
||||||
isMdmEnabledAndConfigured: true,
|
isMacMdmEnabledAndConfigured: true,
|
||||||
isGlobalAdmin: true,
|
isGlobalAdmin: true,
|
||||||
currentUser: createMockUser(),
|
currentUser: createMockUser(),
|
||||||
},
|
},
|
||||||
|
|
@ -601,5 +601,126 @@ describe("Host Actions Dropdown", () => {
|
||||||
|
|
||||||
expect(screen.queryByText("Unlock")).not.toBeInTheDocument();
|
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,
|
isPremiumTier = false,
|
||||||
isGlobalAdmin = false,
|
isGlobalAdmin = false,
|
||||||
isGlobalMaintainer = false,
|
isGlobalMaintainer = false,
|
||||||
isMdmEnabledAndConfigured = false,
|
isMacMdmEnabledAndConfigured = false,
|
||||||
|
isWindowsMdmEnabledAndConfigured = false,
|
||||||
isSandboxMode = false,
|
isSandboxMode = false,
|
||||||
currentUser,
|
currentUser,
|
||||||
} = useContext(AppContext);
|
} = useContext(AppContext);
|
||||||
|
|
@ -67,7 +68,8 @@ const HostActionsDropdown = ({
|
||||||
hostMdmEnrollmentStatus ?? ""
|
hostMdmEnrollmentStatus ?? ""
|
||||||
),
|
),
|
||||||
isFleetMdm: mdmName === "Fleet",
|
isFleetMdm: mdmName === "Fleet",
|
||||||
isMdmEnabledAndConfigured,
|
isMacMdmEnabledAndConfigured,
|
||||||
|
isWindowsMdmEnabledAndConfigured,
|
||||||
doesStoreEncryptionKey: doesStoreEncryptionKey ?? false,
|
doesStoreEncryptionKey: doesStoreEncryptionKey ?? false,
|
||||||
isSandboxMode,
|
isSandboxMode,
|
||||||
hostMdmDeviceStatus,
|
hostMdmDeviceStatus,
|
||||||
|
|
|
||||||
|
|
@ -44,11 +44,11 @@ const DEFAULT_OPTIONS = [
|
||||||
value: "lock",
|
value: "lock",
|
||||||
disabled: false,
|
disabled: false,
|
||||||
},
|
},
|
||||||
// {
|
{
|
||||||
// label: "Wipe",
|
label: "Wipe",
|
||||||
// value: "wipe",
|
value: "wipe",
|
||||||
// disabled: false,
|
disabled: false,
|
||||||
// },
|
},
|
||||||
{
|
{
|
||||||
label: "Unlock",
|
label: "Unlock",
|
||||||
value: "unlock",
|
value: "unlock",
|
||||||
|
|
@ -74,7 +74,8 @@ interface IHostActionConfigOptions {
|
||||||
isHostOnline: boolean;
|
isHostOnline: boolean;
|
||||||
isEnrolledInMdm: boolean;
|
isEnrolledInMdm: boolean;
|
||||||
isFleetMdm: boolean;
|
isFleetMdm: boolean;
|
||||||
isMdmEnabledAndConfigured: boolean;
|
isMacMdmEnabledAndConfigured: boolean;
|
||||||
|
isWindowsMdmEnabledAndConfigured: boolean;
|
||||||
doesStoreEncryptionKey: boolean;
|
doesStoreEncryptionKey: boolean;
|
||||||
isSandboxMode: boolean;
|
isSandboxMode: boolean;
|
||||||
hostMdmDeviceStatus: HostMdmDeviceStatusUIState;
|
hostMdmDeviceStatus: HostMdmDeviceStatusUIState;
|
||||||
|
|
@ -93,11 +94,11 @@ const canEditMdm = (config: IHostActionConfigOptions) => {
|
||||||
isTeamMaintainer,
|
isTeamMaintainer,
|
||||||
isEnrolledInMdm,
|
isEnrolledInMdm,
|
||||||
isFleetMdm,
|
isFleetMdm,
|
||||||
isMdmEnabledAndConfigured,
|
isMacMdmEnabledAndConfigured,
|
||||||
} = config;
|
} = config;
|
||||||
return (
|
return (
|
||||||
config.hostPlatform === "darwin" &&
|
config.hostPlatform === "darwin" &&
|
||||||
isMdmEnabledAndConfigured &&
|
isMacMdmEnabledAndConfigured &&
|
||||||
isEnrolledInMdm &&
|
isEnrolledInMdm &&
|
||||||
isFleetMdm &&
|
isFleetMdm &&
|
||||||
(isGlobalAdmin || isGlobalMaintainer || isTeamAdmin || isTeamMaintainer)
|
(isGlobalAdmin || isGlobalMaintainer || isTeamAdmin || isTeamMaintainer)
|
||||||
|
|
@ -107,7 +108,7 @@ const canEditMdm = (config: IHostActionConfigOptions) => {
|
||||||
const canLockHost = ({
|
const canLockHost = ({
|
||||||
isPremiumTier,
|
isPremiumTier,
|
||||||
hostPlatform,
|
hostPlatform,
|
||||||
isMdmEnabledAndConfigured,
|
isMacMdmEnabledAndConfigured,
|
||||||
isEnrolledInMdm,
|
isEnrolledInMdm,
|
||||||
isFleetMdm,
|
isFleetMdm,
|
||||||
isGlobalAdmin,
|
isGlobalAdmin,
|
||||||
|
|
@ -120,7 +121,7 @@ const canLockHost = ({
|
||||||
const canLockDarwin =
|
const canLockDarwin =
|
||||||
hostPlatform === "darwin" &&
|
hostPlatform === "darwin" &&
|
||||||
isFleetMdm &&
|
isFleetMdm &&
|
||||||
isMdmEnabledAndConfigured &&
|
isMacMdmEnabledAndConfigured &&
|
||||||
isEnrolledInMdm;
|
isEnrolledInMdm;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -143,23 +144,23 @@ const canWipeHost = ({
|
||||||
isTeamObserver,
|
isTeamObserver,
|
||||||
isFleetMdm,
|
isFleetMdm,
|
||||||
isEnrolledInMdm,
|
isEnrolledInMdm,
|
||||||
isMdmEnabledAndConfigured,
|
isMacMdmEnabledAndConfigured,
|
||||||
|
isWindowsMdmEnabledAndConfigured,
|
||||||
hostPlatform,
|
hostPlatform,
|
||||||
|
hostMdmDeviceStatus,
|
||||||
}: IHostActionConfigOptions) => {
|
}: IHostActionConfigOptions) => {
|
||||||
// TODO: remove when we work on wipe issue.
|
const hostMdmEnabled =
|
||||||
return false;
|
(hostPlatform === "darwin" && isMacMdmEnabledAndConfigured) ||
|
||||||
|
(hostPlatform === "windows" && isWindowsMdmEnabledAndConfigured);
|
||||||
|
|
||||||
// macOS and Windows hosts have the same conditions and can be wiped if they
|
// macOS and Windows hosts have the same conditions and can be wiped if they
|
||||||
// are enrolled in MDM and the MDM is enabled.
|
// are enrolled in MDM and the MDM is enabled.
|
||||||
const canWipeMacOrWindows =
|
const canWipeMacOrWindows = hostMdmEnabled && isFleetMdm && isEnrolledInMdm;
|
||||||
(hostPlatform === "darwin" || hostPlatform === "windows") &&
|
|
||||||
isFleetMdm &&
|
|
||||||
isMdmEnabledAndConfigured &&
|
|
||||||
isEnrolledInMdm;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
isPremiumTier &&
|
isPremiumTier &&
|
||||||
(hostPlatform === "linux" || canWipeMacOrWindows) &&
|
hostMdmDeviceStatus === "unlocked" &&
|
||||||
|
(isLinuxLike(hostPlatform) || canWipeMacOrWindows) &&
|
||||||
(isGlobalAdmin ||
|
(isGlobalAdmin ||
|
||||||
isGlobalMaintainer ||
|
isGlobalMaintainer ||
|
||||||
isGlobalObserver ||
|
isGlobalObserver ||
|
||||||
|
|
@ -177,14 +178,14 @@ const canUnlock = ({
|
||||||
isTeamMaintainer,
|
isTeamMaintainer,
|
||||||
isFleetMdm,
|
isFleetMdm,
|
||||||
isEnrolledInMdm,
|
isEnrolledInMdm,
|
||||||
isMdmEnabledAndConfigured,
|
isMacMdmEnabledAndConfigured,
|
||||||
hostPlatform,
|
hostPlatform,
|
||||||
hostMdmDeviceStatus,
|
hostMdmDeviceStatus,
|
||||||
}: IHostActionConfigOptions) => {
|
}: IHostActionConfigOptions) => {
|
||||||
const canLockDarwin =
|
const canUnlockDarwin =
|
||||||
hostPlatform === "darwin" &&
|
hostPlatform === "darwin" &&
|
||||||
isFleetMdm &&
|
isFleetMdm &&
|
||||||
isMdmEnabledAndConfigured &&
|
isMacMdmEnabledAndConfigured &&
|
||||||
isEnrolledInMdm;
|
isEnrolledInMdm;
|
||||||
|
|
||||||
// "unlocking" for a macOS host means that somebody saw the unlock pin, but
|
// "unlocking" for a macOS host means that somebody saw the unlock pin, but
|
||||||
|
|
@ -198,7 +199,7 @@ const canUnlock = ({
|
||||||
isPremiumTier &&
|
isPremiumTier &&
|
||||||
isValidState &&
|
isValidState &&
|
||||||
(isGlobalAdmin || isGlobalMaintainer || isTeamAdmin || isTeamMaintainer) &&
|
(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");
|
options = options.filter((option) => option.value !== "lock");
|
||||||
}
|
}
|
||||||
|
|
||||||
// if (!canWipeHost(config)) {
|
if (!canWipeHost(config)) {
|
||||||
// options = options.filter((option) => option.value !== "wipe");
|
options = options.filter((option) => option.value !== "wipe");
|
||||||
// }
|
}
|
||||||
|
|
||||||
if (!canUnlock(config)) {
|
if (!canUnlock(config)) {
|
||||||
options = options.filter((option) => option.value !== "unlock");
|
options = options.filter((option) => option.value !== "unlock");
|
||||||
|
|
@ -292,7 +293,12 @@ const setOptionsAsDisabled = (
|
||||||
};
|
};
|
||||||
|
|
||||||
let optionsToDisable: IDropdownOption[] = [];
|
let optionsToDisable: IDropdownOption[] = [];
|
||||||
if (!isHostOnline) {
|
if (
|
||||||
|
!isHostOnline ||
|
||||||
|
isDeviceStatusUpdating(hostMdmDeviceStatus) ||
|
||||||
|
hostMdmDeviceStatus === "locked" ||
|
||||||
|
hostMdmDeviceStatus === "wiped"
|
||||||
|
) {
|
||||||
optionsToDisable = optionsToDisable.concat(
|
optionsToDisable = optionsToDisable.concat(
|
||||||
options.filter(
|
options.filter(
|
||||||
(option) => option.value === "query" || option.value === "mdmOff"
|
(option) => option.value === "query" || option.value === "mdmOff"
|
||||||
|
|
@ -304,16 +310,6 @@ const setOptionsAsDisabled = (
|
||||||
options.filter((option) => option.value === "transfer")
|
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);
|
disableOptions(optionsToDisable);
|
||||||
return options;
|
return options;
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ import { NotificationContext } from "context/notification";
|
||||||
|
|
||||||
import activitiesAPI, {
|
import activitiesAPI, {
|
||||||
IActivitiesResponse,
|
IActivitiesResponse,
|
||||||
|
IPastActivitiesResponse,
|
||||||
IUpcomingActivitiesResponse,
|
IUpcomingActivitiesResponse,
|
||||||
} from "services/entities/activities";
|
} from "services/entities/activities";
|
||||||
import hostAPI from "services/entities/hosts";
|
import hostAPI from "services/entities/hosts";
|
||||||
|
|
@ -90,6 +91,7 @@ import {
|
||||||
HostMdmDeviceStatusUIState,
|
HostMdmDeviceStatusUIState,
|
||||||
getHostDeviceStatusUIState,
|
getHostDeviceStatusUIState,
|
||||||
} from "../helpers";
|
} from "../helpers";
|
||||||
|
import WipeModal from "./modals/WipeModal";
|
||||||
|
|
||||||
const baseClass = "host-details";
|
const baseClass = "host-details";
|
||||||
|
|
||||||
|
|
@ -164,6 +166,7 @@ const HostDetailsPage = ({
|
||||||
);
|
);
|
||||||
const [showLockHostModal, setShowLockHostModal] = useState(false);
|
const [showLockHostModal, setShowLockHostModal] = useState(false);
|
||||||
const [showUnlockHostModal, setShowUnlockHostModal] = useState(false);
|
const [showUnlockHostModal, setShowUnlockHostModal] = useState(false);
|
||||||
|
const [showWipeModal, setShowWipeModal] = useState(false);
|
||||||
const [scriptDetailsId, setScriptDetailsId] = useState("");
|
const [scriptDetailsId, setScriptDetailsId] = useState("");
|
||||||
const [selectedPolicy, setSelectedPolicy] = useState<IHostPolicy | null>(
|
const [selectedPolicy, setSelectedPolicy] = useState<IHostPolicy | null>(
|
||||||
null
|
null
|
||||||
|
|
@ -366,9 +369,9 @@ const HostDetailsPage = ({
|
||||||
isError: pastActivitiesIsError,
|
isError: pastActivitiesIsError,
|
||||||
refetch: refetchPastActivities,
|
refetch: refetchPastActivities,
|
||||||
} = useQuery<
|
} = useQuery<
|
||||||
IActivitiesResponse,
|
IPastActivitiesResponse,
|
||||||
Error,
|
Error,
|
||||||
IActivitiesResponse,
|
IPastActivitiesResponse,
|
||||||
Array<{
|
Array<{
|
||||||
scope: string;
|
scope: string;
|
||||||
pageIndex: number;
|
pageIndex: number;
|
||||||
|
|
@ -644,6 +647,9 @@ const HostDetailsPage = ({
|
||||||
case "unlock":
|
case "unlock":
|
||||||
setShowUnlockHostModal(true);
|
setShowUnlockHostModal(true);
|
||||||
break;
|
break;
|
||||||
|
case "wipe":
|
||||||
|
setShowWipeModal(true);
|
||||||
|
break;
|
||||||
default: // do nothing
|
default: // do nothing
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -976,6 +982,14 @@ const HostDetailsPage = ({
|
||||||
onClose={() => setShowUnlockHostModal(false)}
|
onClose={() => setShowUnlockHostModal(false)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{showWipeModal && (
|
||||||
|
<WipeModal
|
||||||
|
id={host.id}
|
||||||
|
hostName={host.display_name}
|
||||||
|
onSuccess={() => setHostMdmDeviceState("wiping")}
|
||||||
|
onClose={() => setShowWipeModal(false)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
</MainContent>
|
</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 { Tab, TabList, TabPanel, Tabs } from "react-tabs";
|
||||||
|
|
||||||
import { IActivityDetails } from "interfaces/activity";
|
import { IActivityDetails } from "interfaces/activity";
|
||||||
import { IActivitiesResponse } from "services/entities/activities";
|
import {
|
||||||
|
IPastActivitiesResponse,
|
||||||
|
IUpcomingActivitiesResponse,
|
||||||
|
} from "services/entities/activities";
|
||||||
|
|
||||||
import Card from "components/Card";
|
import Card from "components/Card";
|
||||||
import TabsWrapper from "components/TabsWrapper";
|
import TabsWrapper from "components/TabsWrapper";
|
||||||
|
|
@ -45,7 +48,7 @@ const UpcomingTooltip = () => {
|
||||||
|
|
||||||
interface IActivityProps {
|
interface IActivityProps {
|
||||||
activeTab: "past" | "upcoming";
|
activeTab: "past" | "upcoming";
|
||||||
activities?: IActivitiesResponse;
|
activities?: IPastActivitiesResponse | IUpcomingActivitiesResponse;
|
||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
isError?: boolean;
|
isError?: boolean;
|
||||||
upcomingCount: number;
|
upcomingCount: number;
|
||||||
|
|
@ -93,7 +96,7 @@ const Activity = ({
|
||||||
</TabList>
|
</TabList>
|
||||||
<TabPanel>
|
<TabPanel>
|
||||||
<PastActivityFeed
|
<PastActivityFeed
|
||||||
activities={activities}
|
activities={activities as IPastActivitiesResponse | undefined}
|
||||||
onDetailsClick={onShowDetails}
|
onDetailsClick={onShowDetails}
|
||||||
isError={isError}
|
isError={isError}
|
||||||
onNextPage={onNextPage}
|
onNextPage={onNextPage}
|
||||||
|
|
@ -103,7 +106,7 @@ const Activity = ({
|
||||||
<TabPanel>
|
<TabPanel>
|
||||||
<UpcomingTooltip />
|
<UpcomingTooltip />
|
||||||
<UpcomingActivityFeed
|
<UpcomingActivityFeed
|
||||||
activities={activities}
|
activities={activities as IUpcomingActivitiesResponse | undefined}
|
||||||
onDetailsClick={onShowDetails}
|
onDetailsClick={onShowDetails}
|
||||||
isError={isError}
|
isError={isError}
|
||||||
onNextPage={onNextPage}
|
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
|
display: grid; // Grid system is used to create variable dashed line lengths
|
||||||
grid-template-columns: 16px 16px 1fr;
|
grid-template-columns: 16px 16px 1fr;
|
||||||
grid-template-rows: 32px max-content;
|
grid-template-rows: 32px max-content;
|
||||||
|
|
@ -62,4 +62,5 @@
|
||||||
padding-bottom: $pad-xxlarge;
|
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 React from "react";
|
||||||
|
|
||||||
import { IActivity } from "interfaces/activity";
|
import { IPastActivity } from "interfaces/activity";
|
||||||
import { IActivitiesResponse } from "services/entities/activities";
|
import { IPastActivitiesResponse } from "services/entities/activities";
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import FleetIcon from "components/icons/FleetIcon";
|
import FleetIcon from "components/icons/FleetIcon";
|
||||||
|
|
@ -9,13 +9,14 @@ import Button from "components/buttons/Button";
|
||||||
import DataError from "components/DataError";
|
import DataError from "components/DataError";
|
||||||
|
|
||||||
import EmptyFeed from "../EmptyFeed/EmptyFeed";
|
import EmptyFeed from "../EmptyFeed/EmptyFeed";
|
||||||
import PastActivity from "../PastActivity/PastActivity";
|
|
||||||
import { ShowActivityDetailsHandler } from "../Activity";
|
import { ShowActivityDetailsHandler } from "../Activity";
|
||||||
|
|
||||||
|
import { pastActivityComponentMap } from "../ActivityConfig";
|
||||||
|
|
||||||
const baseClass = "past-activity-feed";
|
const baseClass = "past-activity-feed";
|
||||||
|
|
||||||
interface IPastActivityFeedProps {
|
interface IPastActivityFeedProps {
|
||||||
activities?: IActivitiesResponse;
|
activities?: IPastActivitiesResponse;
|
||||||
isError?: boolean;
|
isError?: boolean;
|
||||||
onDetailsClick: ShowActivityDetailsHandler;
|
onDetailsClick: ShowActivityDetailsHandler;
|
||||||
onNextPage: () => void;
|
onNextPage: () => void;
|
||||||
|
|
@ -52,9 +53,16 @@ const PastActivityFeed = ({
|
||||||
return (
|
return (
|
||||||
<div className={baseClass}>
|
<div className={baseClass}>
|
||||||
<div>
|
<div>
|
||||||
{activitiesList.map((activity: IActivity) => (
|
{activitiesList.map((activity: IPastActivity) => {
|
||||||
<PastActivity activity={activity} onDetailsClick={onDetailsClick} />
|
const ActivityItemComponent = pastActivityComponentMap[activity.type];
|
||||||
))}
|
return (
|
||||||
|
<ActivityItemComponent
|
||||||
|
key={activity.id}
|
||||||
|
activity={activity}
|
||||||
|
onShowDetails={onDetailsClick}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
<div className={`${baseClass}__pagination`}>
|
<div className={`${baseClass}__pagination`}>
|
||||||
<Button
|
<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 {
|
&.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) =>
|
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.",
|
"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.
|
// 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.
|
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;
|
} as const;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
/** Helpers used across the host details and my device pages and components. */
|
/** 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 { HostMdmDeviceStatus, HostMdmPendingAction } from "interfaces/host";
|
||||||
import {
|
import {
|
||||||
IHostMdmProfile,
|
IHostMdmProfile,
|
||||||
|
|
@ -39,7 +38,9 @@ export type HostMdmDeviceStatusUIState =
|
||||||
| "unlocked"
|
| "unlocked"
|
||||||
| "locked"
|
| "locked"
|
||||||
| "unlocking"
|
| "unlocking"
|
||||||
| "locking";
|
| "locking"
|
||||||
|
| "wiped"
|
||||||
|
| "wiping";
|
||||||
|
|
||||||
// Exclude the empty string from HostPendingAction as that doesn't represent a
|
// Exclude the empty string from HostPendingAction as that doesn't represent a
|
||||||
// valid device status.
|
// valid device status.
|
||||||
|
|
@ -51,9 +52,11 @@ const API_TO_UI_DEVICE_STATUS_MAP: Record<
|
||||||
locked: "locked",
|
locked: "locked",
|
||||||
unlock: "unlocking",
|
unlock: "unlocking",
|
||||||
lock: "locking",
|
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
|
* 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 = (
|
export const isDeviceStatusUpdating = (
|
||||||
deviceStatus: HostMdmDeviceStatusUIState
|
deviceStatus: HostMdmDeviceStatusUIState
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import endpoints from "utilities/endpoints";
|
import endpoints from "utilities/endpoints";
|
||||||
import { IActivity } from "interfaces/activity";
|
import { IActivity, IPastActivity } from "interfaces/activity";
|
||||||
import sendRequest from "services";
|
import sendRequest from "services";
|
||||||
import { buildQueryStringFromParams } from "utilities/url";
|
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 {
|
export interface IUpcomingActivitiesResponse extends IActivitiesResponse {
|
||||||
count: number;
|
count: number;
|
||||||
}
|
}
|
||||||
|
|
@ -45,7 +53,7 @@ export default {
|
||||||
id: number,
|
id: number,
|
||||||
page = DEFAULT_PAGE,
|
page = DEFAULT_PAGE,
|
||||||
perPage = DEFAULT_PAGE_SIZE
|
perPage = DEFAULT_PAGE_SIZE
|
||||||
): Promise<IActivitiesResponse> => {
|
): Promise<IPastActivitiesResponse> => {
|
||||||
const { HOST_PAST_ACTIVITIES } = endpoints;
|
const { HOST_PAST_ACTIVITIES } = endpoints;
|
||||||
|
|
||||||
const queryParams = {
|
const queryParams = {
|
||||||
|
|
|
||||||
|
|
@ -397,8 +397,14 @@ export default {
|
||||||
const { HOST_LOCK } = endpoints;
|
const { HOST_LOCK } = endpoints;
|
||||||
return sendRequest("POST", HOST_LOCK(id));
|
return sendRequest("POST", HOST_LOCK(id));
|
||||||
},
|
},
|
||||||
|
|
||||||
unlockHost: (id: number): Promise<IUnlockHostResponse> => {
|
unlockHost: (id: number): Promise<IUnlockHostResponse> => {
|
||||||
const { HOST_UNLOCK } = endpoints;
|
const { HOST_UNLOCK } = endpoints;
|
||||||
return sendRequest("POST", HOST_UNLOCK(id));
|
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`,
|
HOSTS_TRANSFER_BY_FILTER: `/${API_VERSION}/fleet/hosts/transfer/filter`,
|
||||||
HOST_LOCK: (id: number) => `/${API_VERSION}/fleet/hosts/${id}/lock`,
|
HOST_LOCK: (id: number) => `/${API_VERSION}/fleet/hosts/${id}/lock`,
|
||||||
HOST_UNLOCK: (id: number) => `/${API_VERSION}/fleet/hosts/${id}/unlock`,
|
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`,
|
INVITES: `/${API_VERSION}/fleet/invites`,
|
||||||
LABELS: `/${API_VERSION}/fleet/labels`,
|
LABELS: `/${API_VERSION}/fleet/labels`,
|
||||||
|
|
|
||||||
|
|
@ -13,10 +13,14 @@ export const isPremiumTier = (config: IConfig): boolean => {
|
||||||
return config.license.tier === "premium";
|
return config.license.tier === "premium";
|
||||||
};
|
};
|
||||||
|
|
||||||
export const isMdmEnabledAndConfigured = (config: IConfig): boolean => {
|
export const isMacMdmEnabledAndConfigured = (config: IConfig): boolean => {
|
||||||
return Boolean(config.mdm.enabled_and_configured);
|
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 => {
|
export const isGlobalAdmin = (user: IUser): boolean => {
|
||||||
return user.global_role === "admin";
|
return user.global_role === "admin";
|
||||||
};
|
};
|
||||||
|
|
@ -142,7 +146,8 @@ export default {
|
||||||
isSandboxMode,
|
isSandboxMode,
|
||||||
isFreeTier,
|
isFreeTier,
|
||||||
isPremiumTier,
|
isPremiumTier,
|
||||||
isMdmEnabledAndConfigured,
|
isMacMdmEnabledAndConfigured,
|
||||||
|
isWindowsMdmEnabledAndConfigured,
|
||||||
isGlobalAdmin,
|
isGlobalAdmin,
|
||||||
isGlobalMaintainer,
|
isGlobalMaintainer,
|
||||||
isGlobalObserver,
|
isGlobalObserver,
|
||||||
|
|
|
||||||
|
|
@ -653,6 +653,11 @@ func updateMDMAppleHostDB(
|
||||||
return ctxerr.Wrap(ctx, err, "update mdm apple host")
|
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 {
|
if err := upsertMDMAppleHostMDMInfoDB(ctx, tx, appCfg.ServerSettings, false, hostID); err != nil {
|
||||||
return ctxerr.Wrap(ctx, err, "ingest mdm apple host upsert MDM info")
|
return ctxerr.Wrap(ctx, err, "ingest mdm apple host upsert MDM info")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -67,7 +67,7 @@ func TestMDMApple(t *testing.T) {
|
||||||
{"TestMDMAppleConfigProfileHash", testMDMAppleConfigProfileHash},
|
{"TestMDMAppleConfigProfileHash", testMDMAppleConfigProfileHash},
|
||||||
{"TestResetMDMAppleEnrollment", testResetMDMAppleEnrollment},
|
{"TestResetMDMAppleEnrollment", testResetMDMAppleEnrollment},
|
||||||
{"TestMDMAppleDeleteHostDEPAssignments", testMDMAppleDeleteHostDEPAssignments},
|
{"TestMDMAppleDeleteHostDEPAssignments", testMDMAppleDeleteHostDEPAssignments},
|
||||||
{"CleanMacOSMDMLock", testCleanMacOSMDMLock},
|
{"LockUnlockWipeMacOS", testLockUnlockWipeMacOS},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, c := range cases {
|
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()
|
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{
|
host, err := ds.NewHost(ctx, &fleet.Host{
|
||||||
Hostname: "test-host1-name",
|
Hostname: "test-host1-name",
|
||||||
OsqueryHostID: ptr.String("1337"),
|
OsqueryHostID: ptr.String("1337"),
|
||||||
|
|
@ -4439,11 +4430,11 @@ func testCleanMacOSMDMLock(t *testing.T, ds *Datastore) {
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
nanoEnroll(t, ds, host, false)
|
nanoEnroll(t, ds, host, false)
|
||||||
|
|
||||||
status, err := ds.GetHostLockWipeStatus(ctx, host.ID, "macos")
|
status, err := ds.GetHostLockWipeStatus(ctx, host)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// default state
|
// 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)
|
appleStore, err := ds.NewMDMAppleMDMStorage(nil, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
@ -4457,18 +4448,117 @@ func testCleanMacOSMDMLock(t *testing.T, ds *Datastore) {
|
||||||
err = appleStore.EnqueueDeviceLockCommand(ctx, host, cmd, "123456")
|
err = appleStore.EnqueueDeviceLockCommand(ctx, host, cmd, "123456")
|
||||||
require.NoError(t, err)
|
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)
|
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
|
// execute CleanMacOSMDMLock to simulate successful unlock
|
||||||
err = ds.CleanMacOSMDMLock(ctx, host.UUID)
|
err = ds.CleanMacOSMDMLock(ctx, host.UUID)
|
||||||
require.NoError(t, err)
|
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)
|
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)
|
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) {
|
func TestMDMAppleProfileVerification(t *testing.T) {
|
||||||
|
|
|
||||||
|
|
@ -784,8 +784,6 @@ const hostMDMSelect = `,
|
||||||
) mdm_host_data
|
) 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
|
// hostMDMJoin is the SQL fragment used to join MDM-related tables to the hosts table. It is a
|
||||||
// dependency of the hostMDMSelect fragment.
|
// dependency of the hostMDMSelect fragment.
|
||||||
const hostMDMJoin = `
|
const hostMDMJoin = `
|
||||||
|
|
@ -1798,6 +1796,11 @@ func (ds *Datastore) EnrollOrbit(ctx context.Context, isMDMEnabled bool, hostInf
|
||||||
}
|
}
|
||||||
host.ID = hostID
|
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):
|
case errors.Is(err, sql.ErrNoRows):
|
||||||
zeroTime := time.Unix(0, 0).Add(24 * time.Hour)
|
zeroTime := time.Unix(0, 0).Add(24 * time.Hour)
|
||||||
// Create new host record. We always create newly enrolled hosts with refetch_requested = true
|
// 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")
|
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
|
// Update existing host record
|
||||||
sqlUpdate := `
|
sqlUpdate := `
|
||||||
UPDATE hosts
|
UPDATE hosts
|
||||||
|
|
|
||||||
|
|
@ -142,28 +142,32 @@ func (ds *Datastore) MDMWindowsInsertCommandForHosts(ctx context.Context, hostUU
|
||||||
}
|
}
|
||||||
|
|
||||||
return ds.withRetryTxx(ctx, func(tx sqlx.ExtContext) error {
|
return ds.withRetryTxx(ctx, func(tx sqlx.ExtContext) error {
|
||||||
// first, create the command entry
|
return ds.mdmWindowsInsertCommandForHostsDB(ctx, tx, hostUUIDsOrDeviceIDs, cmd)
|
||||||
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) 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 {
|
func (ds *Datastore) mdmWindowsInsertHostCommandDB(ctx context.Context, tx sqlx.ExecerContext, hostUUIDOrDeviceID, commandUUID string) error {
|
||||||
stmt := `
|
stmt := `
|
||||||
INSERT INTO windows_mdm_command_queue (enrollment_id, command_uuid)
|
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")
|
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 (?, ?)`
|
insertResultsStmt = `
|
||||||
|
|
||||||
const dequeueCommandsStmt = `DELETE FROM windows_mdm_command_queue WHERE command_uuid IN (?)`
|
|
||||||
|
|
||||||
const insertResultsStmt = `
|
|
||||||
INSERT INTO windows_mdm_command_results
|
INSERT INTO windows_mdm_command_results
|
||||||
(enrollment_id, command_uuid, raw_result, response_id, status_code)
|
(enrollment_id, command_uuid, raw_result, response_id, status_code)
|
||||||
VALUES %s
|
VALUES %s
|
||||||
ON DUPLICATE KEY UPDATE
|
ON DUPLICATE KEY UPDATE
|
||||||
raw_result = COALESCE(VALUES(raw_result), raw_result),
|
raw_result = COALESCE(VALUES(raw_result), raw_result),
|
||||||
status_code = COALESCE(VALUES(status_code), status_code)
|
status_code = COALESCE(VALUES(status_code), status_code)
|
||||||
`
|
`
|
||||||
|
)
|
||||||
|
|
||||||
enrollment, err := ds.MDMWindowsGetEnrolledDeviceWithDeviceID(ctx, deviceID)
|
enrollment, err := ds.MDMWindowsGetEnrolledDeviceWithDeviceID(ctx, deviceID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -301,9 +305,15 @@ ON DUPLICATE KEY UPDATE
|
||||||
|
|
||||||
// for all the matching UUIDs, try to find any <Status> or
|
// for all the matching UUIDs, try to find any <Status> or
|
||||||
// <Result> entries to track them as responses.
|
// <Result> entries to track them as responses.
|
||||||
var args []any
|
var (
|
||||||
var sb strings.Builder
|
args []any
|
||||||
var potentialProfilePayloads []*fleet.MDMWindowsProfilePayload
|
sb strings.Builder
|
||||||
|
potentialProfilePayloads []*fleet.MDMWindowsProfilePayload
|
||||||
|
|
||||||
|
wipeCmdUUID string
|
||||||
|
wipeCmdStatus string
|
||||||
|
)
|
||||||
|
|
||||||
for _, cmd := range matchingCmds {
|
for _, cmd := range matchingCmds {
|
||||||
statusCode := ""
|
statusCode := ""
|
||||||
if status, ok := uuidsToStatus[cmd.CommandUUID]; ok && status.Data != nil {
|
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)
|
args = append(args, enrollment.ID, cmd.CommandUUID, rawResult, responseID, statusCode)
|
||||||
sb.WriteString("(?, ?, ?, ?, ?),")
|
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 {
|
if err := updateMDMWindowsHostProfileStatusFromResponseDB(ctx, tx, potentialProfilePayloads); err != nil {
|
||||||
|
|
@ -339,6 +356,14 @@ ON DUPLICATE KEY UPDATE
|
||||||
return ctxerr.Wrap(ctx, err, "inserting command results")
|
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
|
// dequeue the commands
|
||||||
var matchingUUIDs []string
|
var matchingUUIDs []string
|
||||||
for _, cmd := range matchingCmds {
|
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
|
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)
|
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()
|
ctx := context.Background()
|
||||||
d1 := &fleet.MDMWindowsEnrolledDevice{
|
d1 := &fleet.MDMWindowsEnrolledDevice{
|
||||||
MDMDeviceID: uuid.New().String(),
|
MDMDeviceID: uuid.New().String(),
|
||||||
|
|
@ -1285,6 +1286,7 @@ func windowsEnroll(t *testing.T, ds fleet.Datastore, h *fleet.Host) {
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
err = ds.UpdateMDMWindowsEnrollmentsHostUUID(ctx, d1.HostUUID, d1.MDMDeviceID)
|
err = ds.UpdateMDMWindowsEnrollmentsHostUUID(ctx, d1.HostUUID, d1.MDMDeviceID)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
return d1.MDMDeviceID
|
||||||
}
|
}
|
||||||
|
|
||||||
func testMDMWindowsProfileManagement(t *testing.T, ds *Datastore) {
|
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
|
// time of the Exec call, and the result simply returns the integers it
|
||||||
// already holds:
|
// already holds:
|
||||||
// https://github.com/go-sql-driver/mysql/blob/bcc459a906419e2890a50fc2c99ea6dd927a88f2/result.go
|
// https://github.com/go-sql-driver/mysql/blob/bcc459a906419e2890a50fc2c99ea6dd927a88f2/result.go
|
||||||
//
|
|
||||||
// TODO(mna): would that work on mariadb too?
|
|
||||||
|
|
||||||
lastID, _ := res.LastInsertId()
|
lastID, _ := res.LastInsertId()
|
||||||
aff, _ := res.RowsAffected()
|
aff, _ := res.RowsAffected()
|
||||||
|
|
|
||||||
|
|
@ -86,28 +86,26 @@ func (s *NanoMDMStorage) EnqueueDeviceLockCommand(
|
||||||
cmd *mdm.Command,
|
cmd *mdm.Command,
|
||||||
pin string,
|
pin string,
|
||||||
) error {
|
) error {
|
||||||
|
|
||||||
return withRetryTxx(ctx, s.db, func(tx sqlx.ExtContext) error {
|
return withRetryTxx(ctx, s.db, func(tx sqlx.ExtContext) error {
|
||||||
if err := enqueueCommandDB(ctx, tx, []string{host.UUID}, cmd); err != nil {
|
if err := enqueueCommandDB(ctx, tx, []string{host.UUID}, cmd); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO(roberto): call @mna's transactionable method to update
|
|
||||||
// these tables when it's ready.
|
|
||||||
stmt := `
|
stmt := `
|
||||||
INSERT INTO host_mdm_actions (
|
INSERT INTO host_mdm_actions (
|
||||||
host_id,
|
host_id,
|
||||||
lock_ref,
|
lock_ref,
|
||||||
unlock_pin
|
unlock_pin,
|
||||||
)
|
fleet_platform
|
||||||
VALUES (?, ?, ?)
|
)
|
||||||
ON DUPLICATE KEY UPDATE
|
VALUES (?, ?, ?, ?)
|
||||||
wipe_ref = NULL,
|
ON DUPLICATE KEY UPDATE
|
||||||
unlock_ref = NULL,
|
wipe_ref = NULL,
|
||||||
|
unlock_ref = NULL,
|
||||||
unlock_pin = VALUES(unlock_pin),
|
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")
|
return ctxerr.Wrap(ctx, err, "modifying host_mdm_actions for DeviceLock")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -115,6 +113,31 @@ func (s *NanoMDMStorage) EnqueueDeviceLockCommand(
|
||||||
}, s.logger)
|
}, 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
|
// NewMDMAppleDEPStorage returns a MySQL nanodep storage that uses the Datastore
|
||||||
// underlying MySQL writer *sql.DB.
|
// underlying MySQL writer *sql.DB.
|
||||||
func (ds *Datastore) NewMDMAppleDEPStorage(tok nanodep_client.OAuth1Tokens) (*NanoDEPStorage, error) {
|
func (ds *Datastore) NewMDMAppleDEPStorage(tok nanodep_client.OAuth1Tokens) (*NanoDEPStorage, error) {
|
||||||
|
|
|
||||||
|
|
@ -78,7 +78,7 @@ func testEnqueueDeviceLockCommand(t *testing.T, ds *Datastore) {
|
||||||
},
|
},
|
||||||
}, res)
|
}, res)
|
||||||
|
|
||||||
status, err := ds.GetHostLockWipeStatus(ctx, host.ID, "darwin")
|
status, err := ds.GetHostLockWipeStatus(ctx, host)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, "cmd-uuid", status.LockMDMCommand.CommandUUID)
|
require.Equal(t, "cmd-uuid", status.LockMDMCommand.CommandUUID)
|
||||||
require.Equal(t, "123456", status.UnlockPIN)
|
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")
|
return ctxerr.Wrap(ctx, err, "lookup host script corresponding mdm action")
|
||||||
}
|
}
|
||||||
if refCol != "" {
|
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 {
|
if err != nil {
|
||||||
return ctxerr.Wrap(ctx, err, "update host mdm action based on script result")
|
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
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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 = `
|
const stmt = `
|
||||||
SELECT
|
SELECT
|
||||||
lock_ref,
|
lock_ref,
|
||||||
wipe_ref,
|
wipe_ref,
|
||||||
unlock_ref,
|
unlock_ref,
|
||||||
unlock_pin
|
unlock_pin,
|
||||||
|
fleet_platform
|
||||||
FROM
|
FROM
|
||||||
host_mdm_actions
|
host_mdm_actions
|
||||||
WHERE
|
WHERE
|
||||||
|
|
@ -571,16 +571,18 @@ func (ds *Datastore) GetHostLockWipeStatus(ctx context.Context, hostID uint, fle
|
||||||
`
|
`
|
||||||
|
|
||||||
var mdmActions struct {
|
var mdmActions struct {
|
||||||
LockRef *string `db:"lock_ref"`
|
LockRef *string `db:"lock_ref"`
|
||||||
WipeRef *string `db:"wipe_ref"`
|
WipeRef *string `db:"wipe_ref"`
|
||||||
UnlockRef *string `db:"unlock_ref"`
|
UnlockRef *string `db:"unlock_ref"`
|
||||||
UnlockPIN *string `db:"unlock_pin"`
|
UnlockPIN *string `db:"unlock_pin"`
|
||||||
|
FleetPlatform string `db:"fleet_platform"`
|
||||||
}
|
}
|
||||||
|
fleetPlatform := host.FleetPlatform()
|
||||||
status := &fleet.HostLockWipeStatus{
|
status := &fleet.HostLockWipeStatus{
|
||||||
HostFleetPlatform: fleetPlatform,
|
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 {
|
if err == sql.ErrNoRows {
|
||||||
// do not return a Not Found error, return the zero-value status, which
|
// do not return a Not Found error, return the zero-value status, which
|
||||||
// will report the correct states.
|
// 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")
|
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 {
|
switch fleetPlatform {
|
||||||
case "darwin":
|
case "darwin":
|
||||||
if mdmActions.UnlockPIN != nil {
|
if mdmActions.UnlockPIN != nil {
|
||||||
|
|
@ -608,34 +618,22 @@ func (ds *Datastore) GetHostLockWipeStatus(ctx context.Context, hostID uint, fle
|
||||||
|
|
||||||
if mdmActions.LockRef != nil {
|
if mdmActions.LockRef != nil {
|
||||||
// the lock reference is an MDM command
|
// 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 {
|
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.LockMDMCommand = cmd
|
||||||
|
status.LockMDMCommandResult = cmdRes
|
||||||
|
}
|
||||||
|
|
||||||
// get the MDM command result, which may be not found (indicating the
|
if mdmActions.WipeRef != nil {
|
||||||
// command is pending)
|
// the wipe reference is an MDM command
|
||||||
cmdRes, err := ds.GetMDMAppleCommandResults(ctx, *mdmActions.LockRef)
|
cmd, cmdRes, err := ds.getHostMDMAppleCommand(ctx, *mdmActions.WipeRef, host.UUID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, ctxerr.Wrap(ctx, err, "get lock reference MDM command result")
|
return nil, ctxerr.Wrap(ctx, err, "get wipe reference")
|
||||||
}
|
|
||||||
// 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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
status.WipeMDMCommand = cmd
|
||||||
|
status.WipeMDMCommandResult = cmdRes
|
||||||
}
|
}
|
||||||
|
|
||||||
case "windows", "linux":
|
case "windows", "linux":
|
||||||
|
|
@ -655,13 +653,92 @@ func (ds *Datastore) GetHostLockWipeStatus(ctx context.Context, hostID uint, fle
|
||||||
}
|
}
|
||||||
status.UnlockScript = hsr
|
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
|
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
|
// LockHostViaScript will create the script execution request and update
|
||||||
// host_mdm_actions in a single transaction.
|
// 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
|
var res *fleet.HostScriptResult
|
||||||
return ds.withRetryTxx(ctx, func(tx sqlx.ExtContext) error {
|
return ds.withRetryTxx(ctx, func(tx sqlx.ExtContext) error {
|
||||||
var err error
|
var err error
|
||||||
|
|
@ -680,9 +757,9 @@ func (ds *Datastore) LockHostViaScript(ctx context.Context, request *fleet.HostS
|
||||||
(
|
(
|
||||||
host_id,
|
host_id,
|
||||||
lock_ref,
|
lock_ref,
|
||||||
unlock_ref
|
fleet_platform
|
||||||
)
|
)
|
||||||
VALUES (?,?,NULL)
|
VALUES (?,?,?)
|
||||||
ON DUPLICATE KEY UPDATE
|
ON DUPLICATE KEY UPDATE
|
||||||
lock_ref = VALUES(lock_ref)
|
lock_ref = VALUES(lock_ref)
|
||||||
`
|
`
|
||||||
|
|
@ -690,6 +767,7 @@ func (ds *Datastore) LockHostViaScript(ctx context.Context, request *fleet.HostS
|
||||||
_, err = tx.ExecContext(ctx, stmt,
|
_, err = tx.ExecContext(ctx, stmt,
|
||||||
request.HostID,
|
request.HostID,
|
||||||
res.ExecutionID,
|
res.ExecutionID,
|
||||||
|
hostFleetPlatform,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ctxerr.Wrap(ctx, err, "lock host via script update mdm actions")
|
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
|
// UnlockHostViaScript will create the script execution request and update
|
||||||
// host_mdm_actions in a single transaction.
|
// 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
|
var res *fleet.HostScriptResult
|
||||||
return ds.withRetryTxx(ctx, func(tx sqlx.ExtContext) error {
|
return ds.withRetryTxx(ctx, func(tx sqlx.ExtContext) error {
|
||||||
var err error
|
var err error
|
||||||
|
|
@ -720,9 +798,9 @@ func (ds *Datastore) UnlockHostViaScript(ctx context.Context, request *fleet.Hos
|
||||||
(
|
(
|
||||||
host_id,
|
host_id,
|
||||||
unlock_ref,
|
unlock_ref,
|
||||||
lock_ref
|
fleet_platform
|
||||||
)
|
)
|
||||||
VALUES (?,?,NULL)
|
VALUES (?,?,?)
|
||||||
ON DUPLICATE KEY UPDATE
|
ON DUPLICATE KEY UPDATE
|
||||||
unlock_ref = VALUES(unlock_ref),
|
unlock_ref = VALUES(unlock_ref),
|
||||||
unlock_pin = NULL
|
unlock_pin = NULL
|
||||||
|
|
@ -731,6 +809,7 @@ func (ds *Datastore) UnlockHostViaScript(ctx context.Context, request *fleet.Hos
|
||||||
_, err = tx.ExecContext(ctx, stmt,
|
_, err = tx.ExecContext(ctx, stmt,
|
||||||
request.HostID,
|
request.HostID,
|
||||||
res.ExecutionID,
|
res.ExecutionID,
|
||||||
|
hostFleetPlatform,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ctxerr.Wrap(ctx, err, "unlock host via script update mdm actions")
|
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 = `
|
const stmt = `
|
||||||
INSERT INTO host_mdm_actions
|
INSERT INTO host_mdm_actions
|
||||||
(
|
(
|
||||||
host_id,
|
host_id,
|
||||||
unlock_ref
|
unlock_ref,
|
||||||
|
fleet_platform
|
||||||
)
|
)
|
||||||
VALUES (?, ?)
|
VALUES (?, ?, ?)
|
||||||
ON DUPLICATE KEY UPDATE
|
ON DUPLICATE KEY UPDATE
|
||||||
-- do not overwrite if a value is already set
|
-- do not overwrite if a value is already set
|
||||||
unlock_ref = IF(unlock_ref IS NULL, VALUES(unlock_ref), unlock_ref)
|
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
|
// 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
|
// 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
|
// from then on, the host is marked as "pending unlock" until the device is
|
||||||
// actually unlocked with the PIN.
|
// actually unlocked with the PIN. The actual unlocking happens when the
|
||||||
// TODO(mna): to be determined how we then get notified that it has been
|
// device sends an Idle MDM request.
|
||||||
// unlocked, so that it can transition to unlocked (not pending).
|
|
||||||
unlockRef := ts.Format(time.DateTime)
|
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")
|
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 {
|
func buildHostLockWipeStatusUpdateStmt(refCol string, succeeded bool, joinPart string) string {
|
||||||
stmt := `UPDATE host_mdm_actions SET %s WHERE host_id = ?`
|
var alias string
|
||||||
|
|
||||||
|
stmt := `UPDATE host_mdm_actions `
|
||||||
|
if joinPart != "" {
|
||||||
|
stmt += `hma ` + joinPart
|
||||||
|
alias = "hma."
|
||||||
|
}
|
||||||
|
stmt += ` SET `
|
||||||
|
|
||||||
if succeeded {
|
if succeeded {
|
||||||
switch refCol {
|
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
|
// 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
|
// lock request does generate the PIN and store it there to be used by an
|
||||||
// eventual unlock.
|
// eventual unlock.
|
||||||
stmt = fmt.Sprintf(stmt, "unlock_ref = NULL")
|
stmt += fmt.Sprintf("%sunlock_ref = NULL, %[1]swipe_ref = NULL", alias)
|
||||||
case "unlock_ref":
|
case "unlock_ref":
|
||||||
// a successful unlock clears itself as well as the lock ref, because
|
// 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
|
// unlock is the default state so we don't need to keep its unlock_ref
|
||||||
// around once it's confirmed.
|
// 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":
|
case "wipe_ref":
|
||||||
// TODO(mna): implement when implementing the wipe story
|
stmt += fmt.Sprintf("%slock_ref = NULL, %[1]sunlock_ref = NULL, %[1]sunlock_pin = NULL", alias)
|
||||||
default:
|
|
||||||
return ctxerr.Errorf(ctx, "unknown reference column %q", refCol)
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// if the action failed, then we clear the reference to that action itself so
|
// 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
|
// the host stays in the previous state (it doesn't transition to the new
|
||||||
// state).
|
// 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)
|
_, err := tx.ExecContext(ctx, stmt, hostID)
|
||||||
return ctxerr.Wrap(ctx, err, "update host lock/wipe status from result")
|
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/fleet"
|
||||||
"github.com/fleetdm/fleet/v4/server/ptr"
|
"github.com/fleetdm/fleet/v4/server/ptr"
|
||||||
"github.com/fleetdm/fleet/v4/server/test"
|
"github.com/fleetdm/fleet/v4/server/test"
|
||||||
|
"github.com/google/uuid"
|
||||||
"github.com/jmoiron/sqlx"
|
"github.com/jmoiron/sqlx"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
@ -29,7 +30,7 @@ func TestScripts(t *testing.T) {
|
||||||
{"BatchSetScripts", testBatchSetScripts},
|
{"BatchSetScripts", testBatchSetScripts},
|
||||||
{"TestLockHostViaScript", testLockHostViaScript},
|
{"TestLockHostViaScript", testLockHostViaScript},
|
||||||
{"TestUnlockHostViaScript", testUnlockHostViaScript},
|
{"TestUnlockHostViaScript", testUnlockHostViaScript},
|
||||||
{"TestLockUnlockViaScripts", testLockUnlockViaScripts},
|
{"TestLockUnlockWipeViaScripts", testLockUnlockWipeViaScripts},
|
||||||
{"TestLockUnlockManually", testLockUnlockManually},
|
{"TestLockUnlockManually", testLockUnlockManually},
|
||||||
}
|
}
|
||||||
for _, c := range cases {
|
for _, c := range cases {
|
||||||
|
|
@ -730,12 +731,12 @@ func testLockHostViaScript(t *testing.T, ds *Datastore) {
|
||||||
ScriptContents: script,
|
ScriptContents: script,
|
||||||
UserID: &user.ID,
|
UserID: &user.ID,
|
||||||
SyncRequest: false,
|
SyncRequest: false,
|
||||||
})
|
}, "windows")
|
||||||
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// verify that we have created entries in host_mdm_actions and host_script_results
|
// 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.NoError(t, err)
|
||||||
require.Equal(t, "windows", status.HostFleetPlatform)
|
require.Equal(t, "windows", status.HostFleetPlatform)
|
||||||
require.NotNil(t, status.LockScript)
|
require.NotNil(t, status.LockScript)
|
||||||
|
|
@ -756,7 +757,7 @@ func testLockHostViaScript(t *testing.T, ds *Datastore) {
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
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.NoError(t, err)
|
||||||
require.True(t, status.IsLocked())
|
require.True(t, status.IsLocked())
|
||||||
require.False(t, status.IsPendingLock())
|
require.False(t, status.IsPendingLock())
|
||||||
|
|
@ -781,12 +782,12 @@ func testUnlockHostViaScript(t *testing.T, ds *Datastore) {
|
||||||
ScriptContents: script,
|
ScriptContents: script,
|
||||||
UserID: &user.ID,
|
UserID: &user.ID,
|
||||||
SyncRequest: false,
|
SyncRequest: false,
|
||||||
})
|
}, "windows")
|
||||||
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// verify that we have created entries in host_mdm_actions and host_script_results
|
// 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.NoError(t, err)
|
||||||
require.Equal(t, "windows", status.HostFleetPlatform)
|
require.Equal(t, "windows", status.HostFleetPlatform)
|
||||||
require.NotNil(t, status.UnlockScript)
|
require.NotNil(t, status.UnlockScript)
|
||||||
|
|
@ -807,14 +808,14 @@ func testUnlockHostViaScript(t *testing.T, ds *Datastore) {
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
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.NoError(t, err)
|
||||||
require.True(t, status.IsUnlocked())
|
require.True(t, status.IsUnlocked())
|
||||||
require.False(t, status.IsPendingUnlock())
|
require.False(t, status.IsPendingUnlock())
|
||||||
require.False(t, status.IsLocked())
|
require.False(t, status.IsLocked())
|
||||||
}
|
}
|
||||||
|
|
||||||
func testLockUnlockViaScripts(t *testing.T, ds *Datastore) {
|
func testLockUnlockWipeViaScripts(t *testing.T, ds *Datastore) {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
user := test.NewUser(t, ds, "Bob", "bob@example.com", true)
|
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)
|
hostID := uint(i + 1)
|
||||||
|
|
||||||
t.Run(platform, func(t *testing.T) {
|
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)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// default state
|
// default state
|
||||||
|
|
@ -834,10 +835,10 @@ func testLockUnlockViaScripts(t *testing.T, ds *Datastore) {
|
||||||
ScriptContents: "lock",
|
ScriptContents: "lock",
|
||||||
UserID: &user.ID,
|
UserID: &user.ID,
|
||||||
SyncRequest: false,
|
SyncRequest: false,
|
||||||
})
|
}, platform)
|
||||||
require.NoError(t, err)
|
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)
|
require.NoError(t, err)
|
||||||
checkLockWipeState(t, status, true, false, false, false, true, false)
|
checkLockWipeState(t, status, true, false, false, false, true, false)
|
||||||
|
|
||||||
|
|
@ -849,7 +850,7 @@ func testLockUnlockViaScripts(t *testing.T, ds *Datastore) {
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
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)
|
require.NoError(t, err)
|
||||||
checkLockWipeState(t, status, false, true, false, false, false, false)
|
checkLockWipeState(t, status, false, true, false, false, false, false)
|
||||||
|
|
||||||
|
|
@ -859,10 +860,10 @@ func testLockUnlockViaScripts(t *testing.T, ds *Datastore) {
|
||||||
ScriptContents: "unlock",
|
ScriptContents: "unlock",
|
||||||
UserID: &user.ID,
|
UserID: &user.ID,
|
||||||
SyncRequest: false,
|
SyncRequest: false,
|
||||||
})
|
}, platform)
|
||||||
require.NoError(t, err)
|
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)
|
require.NoError(t, err)
|
||||||
checkLockWipeState(t, status, false, true, false, true, false, false)
|
checkLockWipeState(t, status, false, true, false, true, false, false)
|
||||||
|
|
||||||
|
|
@ -875,7 +876,7 @@ func testLockUnlockViaScripts(t *testing.T, ds *Datastore) {
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// still locked
|
// 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)
|
require.NoError(t, err)
|
||||||
checkLockWipeState(t, status, false, true, false, false, false, false)
|
checkLockWipeState(t, status, false, true, false, false, false, false)
|
||||||
|
|
||||||
|
|
@ -885,10 +886,10 @@ func testLockUnlockViaScripts(t *testing.T, ds *Datastore) {
|
||||||
ScriptContents: "unlock",
|
ScriptContents: "unlock",
|
||||||
UserID: &user.ID,
|
UserID: &user.ID,
|
||||||
SyncRequest: false,
|
SyncRequest: false,
|
||||||
})
|
}, platform)
|
||||||
require.NoError(t, err)
|
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)
|
require.NoError(t, err)
|
||||||
checkLockWipeState(t, status, false, true, false, true, false, false)
|
checkLockWipeState(t, status, false, true, false, true, false, false)
|
||||||
|
|
||||||
|
|
@ -901,7 +902,7 @@ func testLockUnlockViaScripts(t *testing.T, ds *Datastore) {
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// host is now unlocked
|
// 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)
|
require.NoError(t, err)
|
||||||
checkLockWipeState(t, status, true, false, false, false, false, false)
|
checkLockWipeState(t, status, true, false, false, false, false, false)
|
||||||
|
|
||||||
|
|
@ -911,10 +912,10 @@ func testLockUnlockViaScripts(t *testing.T, ds *Datastore) {
|
||||||
ScriptContents: "lock",
|
ScriptContents: "lock",
|
||||||
UserID: &user.ID,
|
UserID: &user.ID,
|
||||||
SyncRequest: false,
|
SyncRequest: false,
|
||||||
})
|
}, platform)
|
||||||
require.NoError(t, err)
|
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)
|
require.NoError(t, err)
|
||||||
checkLockWipeState(t, status, true, false, false, false, true, false)
|
checkLockWipeState(t, status, true, false, false, false, true, false)
|
||||||
|
|
||||||
|
|
@ -926,9 +927,93 @@ func testLockUnlockViaScripts(t *testing.T, ds *Datastore) {
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
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)
|
require.NoError(t, err)
|
||||||
checkLockWipeState(t, status, true, false, false, false, false, false)
|
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()
|
twoDaysAgo := time.Now().AddDate(0, 0, -2).UTC()
|
||||||
today := time.Now().UTC()
|
today := time.Now().UTC()
|
||||||
err := ds.UnlockHostManually(ctx, 1, twoDaysAgo)
|
err := ds.UnlockHostManually(ctx, 1, "darwin", twoDaysAgo)
|
||||||
require.NoError(t, err)
|
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.NoError(t, err)
|
||||||
require.False(t, status.UnlockRequestedAt.IsZero())
|
require.False(t, status.UnlockRequestedAt.IsZero())
|
||||||
require.WithinDuration(t, twoDaysAgo, status.UnlockRequestedAt, 1*time.Second)
|
require.WithinDuration(t, twoDaysAgo, status.UnlockRequestedAt, 1*time.Second)
|
||||||
|
|
||||||
// if the unlock request already exists, it is not overwritten by subsequent
|
// if the unlock request already exists, it is not overwritten by subsequent
|
||||||
// requests
|
// requests
|
||||||
err = ds.UnlockHostManually(ctx, 1, today)
|
err = ds.UnlockHostManually(ctx, 1, "darwin", today)
|
||||||
require.NoError(t, err)
|
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.NoError(t, err)
|
||||||
require.False(t, status.UnlockRequestedAt.IsZero())
|
require.False(t, status.UnlockRequestedAt.IsZero())
|
||||||
require.WithinDuration(t, twoDaysAgo, status.UnlockRequestedAt, 1*time.Second)
|
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)")
|
_, err := tx.ExecContext(ctx, "INSERT INTO host_mdm_actions (host_id) VALUES (2)")
|
||||||
return err
|
return err
|
||||||
})
|
})
|
||||||
err = ds.UnlockHostManually(ctx, 2, today)
|
err = ds.UnlockHostManually(ctx, 2, "darwin", today)
|
||||||
require.NoError(t, err)
|
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.NoError(t, err)
|
||||||
require.False(t, status.UnlockRequestedAt.IsZero())
|
require.False(t, status.UnlockRequestedAt.IsZero())
|
||||||
require.WithinDuration(t, today, status.UnlockRequestedAt, 1*time.Second)
|
require.WithinDuration(t, today, status.UnlockRequestedAt, 1*time.Second)
|
||||||
|
|
|
||||||
|
|
@ -83,6 +83,7 @@ var ActivityDetailsList = []ActivityDetails{
|
||||||
|
|
||||||
ActivityTypeLockedHost{},
|
ActivityTypeLockedHost{},
|
||||||
ActivityTypeUnlockedHost{},
|
ActivityTypeUnlockedHost{},
|
||||||
|
ActivityTypeWipedHost{},
|
||||||
}
|
}
|
||||||
|
|
||||||
type ActivityDetails interface {
|
type ActivityDetails interface {
|
||||||
|
|
@ -1234,6 +1235,20 @@ type ActivityTypeEditedWindowsProfile struct {
|
||||||
TeamName *string `json:"team_name"`
|
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 {
|
type ActivityTypeLockedHost struct {
|
||||||
HostID uint `json:"host_id"`
|
HostID uint `json:"host_id"`
|
||||||
HostDisplayName string `json:"host_display_name"`
|
HostDisplayName string `json:"host_display_name"`
|
||||||
|
|
@ -1283,17 +1298,22 @@ func (a ActivityTypeUnlockedHost) Documentation() (activity, details, detailsExa
|
||||||
}`
|
}`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a ActivityTypeEditedWindowsProfile) ActivityName() string {
|
type ActivityTypeWipedHost struct {
|
||||||
return "edited_windows_profile"
|
HostID uint `json:"host_id"`
|
||||||
|
HostDisplayName string `json:"host_display_name"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a ActivityTypeEditedWindowsProfile) Documentation() (activity, details, detailsExample string) {
|
func (a ActivityTypeWipedHost) ActivityName() string {
|
||||||
return `Generated when a user edits the Windows profiles of a team (or no team) via the fleetctl CLI.`,
|
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:
|
`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.
|
- "host_id": ID of the host.
|
||||||
- "team_name": The name of the team that the profiles apply to, ` + "`null`" + ` if they apply to devices that are not in a team.`, `{
|
- "host_display_name": Display name of the host.`, `{
|
||||||
"team_id": 123,
|
"host_id": 1,
|
||||||
"team_name": "Workstations"
|
"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
|
InstallProfile(ctx context.Context, hostUUIDs []string, profile mobileconfig.Mobileconfig, uuid string) error
|
||||||
RemoveProfile(ctx context.Context, hostUUIDs []string, identifier string, uuid string) error
|
RemoveProfile(ctx context.Context, hostUUIDs []string, identifier string, uuid string) error
|
||||||
DeviceLock(ctx context.Context, host *Host, 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
|
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
|
BatchSetScripts(ctx context.Context, tmID *uint, scripts []*Script) error
|
||||||
|
|
||||||
// GetHostLockWipeStatus gets the lock/unlock and wipe status for the host.
|
// 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
|
// LockHostViaScript sends a script to lock a host and updates the
|
||||||
// states in host_mdm_actions
|
// 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
|
// UnlockHostViaScript sends a script to unlock a host and updates the
|
||||||
// states in host_mdm_actions
|
// 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
|
// UnlockHostmanually records a request to unlock a host that requires manual
|
||||||
// intervention (such as for macOS). It indicates the an unlock request is
|
// intervention (such as for macOS). It indicates the an unlock request is
|
||||||
// pending.
|
// 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
|
// CleanMacOSMDMLock cleans the lock status and pin for a macOS device
|
||||||
// after it has been unlocked.
|
// after it has been unlocked.
|
||||||
CleanMacOSMDMLock(ctx context.Context, hostUUID string) error
|
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
|
// MDMAppleStore wraps nanomdm's storage and adds methods to deal with
|
||||||
|
|
@ -1337,6 +1351,7 @@ type Datastore interface {
|
||||||
type MDMAppleStore interface {
|
type MDMAppleStore interface {
|
||||||
storage.AllStorage
|
storage.AllStorage
|
||||||
EnqueueDeviceLockCommand(ctx context.Context, host *Host, cmd *mdm.Command, pin string) error
|
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
|
// 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"`
|
HostUUID string `json:"host_uuid" db:"host_uuid"`
|
||||||
// CommandUUID is the unique identifier of the command.
|
// CommandUUID is the unique identifier of the command.
|
||||||
CommandUUID string `json:"command_uuid" db:"command_uuid"`
|
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"`
|
Status string `json:"status" db:"status"`
|
||||||
// UpdatedAt is the last update timestamp of the command result.
|
// UpdatedAt is the last update timestamp of the command result.
|
||||||
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
|
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
|
// windows and linux hosts use a script to unlock
|
||||||
UnlockScript *HostScriptResult
|
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 {
|
func (s *HostLockWipeStatus) IsPendingLock() bool {
|
||||||
|
|
@ -334,8 +339,12 @@ func (s HostLockWipeStatus) IsPendingUnlock() bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s HostLockWipeStatus) IsPendingWipe() bool {
|
func (s HostLockWipeStatus) IsPendingWipe() bool {
|
||||||
// TODO(mna): implement when addressing Wipe story, for now wipe is never pending
|
if s.HostFleetPlatform == "linux" {
|
||||||
return false
|
// 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 {
|
func (s HostLockWipeStatus) IsLocked() bool {
|
||||||
|
|
@ -359,6 +368,20 @@ func (s HostLockWipeStatus) IsUnlocked() bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s HostLockWipeStatus) IsWiped() bool {
|
func (s HostLockWipeStatus) IsWiped() bool {
|
||||||
// TODO(mna): implement when addressing Wipe story, for now never wiped
|
switch s.HostFleetPlatform {
|
||||||
return false
|
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)
|
// Script-based methods (at least for some platforms, MDM-based for others)
|
||||||
LockHost(ctx context.Context, hostID uint) error
|
LockHost(ctx context.Context, hostID uint) error
|
||||||
UnlockHost(ctx context.Context, hostID uint) (unlockPIN string, err 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
|
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)
|
pin := GenerateRandomPin(6)
|
||||||
raw := fmt.Sprintf(`<?xml version="1.0" encoding="UTF-8"?>
|
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">
|
<!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>
|
<string>EraseDevice</string>
|
||||||
<key>PIN</key>
|
<key>PIN</key>
|
||||||
<string>%s</string>
|
<string>%s</string>
|
||||||
|
<key>ObliterationBehavior</key>
|
||||||
|
<string>Default</string>
|
||||||
</dict>
|
</dict>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>`, uuid, pin)
|
</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 {
|
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)
|
require.True(t, mdmStorage.RetrievePushInfoFuncInvoked)
|
||||||
mdmStorage.RetrievePushInfoFuncInvoked = false
|
mdmStorage.RetrievePushInfoFuncInvoked = false
|
||||||
|
|
||||||
host := &fleet.Host{ID: 1, UUID: "A"}
|
host := &fleet.Host{ID: 1, UUID: "A", Platform: "darwin"}
|
||||||
cmdUUID = uuid.New().String()
|
cmdUUID = uuid.New().String()
|
||||||
mdmStorage.EnqueueDeviceLockCommandFunc = func(ctx context.Context, gotHost *fleet.Host, cmd *mdm.Command, pin string) error {
|
mdmStorage.EnqueueDeviceLockCommandFunc = func(ctx context.Context, gotHost *fleet.Host, cmd *mdm.Command, pin string) error {
|
||||||
require.NotNil(t, gotHost)
|
require.NotNil(t, gotHost)
|
||||||
|
|
@ -112,6 +112,7 @@ func TestMDMAppleCommander(t *testing.T) {
|
||||||
require.Equal(t, host.UUID, gotHost.UUID)
|
require.Equal(t, host.UUID, gotHost.UUID)
|
||||||
require.Equal(t, "DeviceLock", cmd.Command.RequestType)
|
require.Equal(t, "DeviceLock", cmd.Command.RequestType)
|
||||||
require.Contains(t, string(cmd.Raw), cmdUUID)
|
require.Contains(t, string(cmd.Raw), cmdUUID)
|
||||||
|
require.Len(t, pin, 6)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
err = cmdr.DeviceLock(ctx, host, cmdUUID)
|
err = cmdr.DeviceLock(ctx, host, cmdUUID)
|
||||||
|
|
@ -120,6 +121,22 @@ func TestMDMAppleCommander(t *testing.T) {
|
||||||
mdmStorage.EnqueueDeviceLockCommandFuncInvoked = false
|
mdmStorage.EnqueueDeviceLockCommandFuncInvoked = false
|
||||||
require.True(t, mdmStorage.RetrievePushInfoFuncInvoked)
|
require.True(t, mdmStorage.RetrievePushInfoFuncInvoked)
|
||||||
mdmStorage.RetrievePushInfoFuncInvoked = false
|
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) {
|
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 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 {
|
type MDMAppleStore struct {
|
||||||
StoreAuthenticateFunc StoreAuthenticateFunc
|
StoreAuthenticateFunc StoreAuthenticateFunc
|
||||||
StoreAuthenticateFuncInvoked bool
|
StoreAuthenticateFuncInvoked bool
|
||||||
|
|
@ -120,6 +122,9 @@ type MDMAppleStore struct {
|
||||||
EnqueueDeviceLockCommandFunc EnqueueDeviceLockCommandFunc
|
EnqueueDeviceLockCommandFunc EnqueueDeviceLockCommandFunc
|
||||||
EnqueueDeviceLockCommandFuncInvoked bool
|
EnqueueDeviceLockCommandFuncInvoked bool
|
||||||
|
|
||||||
|
EnqueueDeviceWipeCommandFunc EnqueueDeviceWipeCommandFunc
|
||||||
|
EnqueueDeviceWipeCommandFuncInvoked bool
|
||||||
|
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -269,3 +274,10 @@ func (fs *MDMAppleStore) EnqueueDeviceLockCommand(ctx context.Context, host *fle
|
||||||
fs.mu.Unlock()
|
fs.mu.Unlock()
|
||||||
return fs.EnqueueDeviceLockCommandFunc(ctx, host, cmd, pin)
|
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 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 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 {
|
type DataStore struct {
|
||||||
HealthCheckFunc HealthCheckFunc
|
HealthCheckFunc HealthCheckFunc
|
||||||
HealthCheckFuncInvoked bool
|
HealthCheckFuncInvoked bool
|
||||||
|
|
@ -2089,6 +2095,15 @@ type DataStore struct {
|
||||||
CleanMacOSMDMLockFunc CleanMacOSMDMLockFunc
|
CleanMacOSMDMLockFunc CleanMacOSMDMLockFunc
|
||||||
CleanMacOSMDMLockFuncInvoked bool
|
CleanMacOSMDMLockFuncInvoked bool
|
||||||
|
|
||||||
|
WipeHostViaScriptFunc WipeHostViaScriptFunc
|
||||||
|
WipeHostViaScriptFuncInvoked bool
|
||||||
|
|
||||||
|
WipeHostViaWindowsMDMFunc WipeHostViaWindowsMDMFunc
|
||||||
|
WipeHostViaWindowsMDMFuncInvoked bool
|
||||||
|
|
||||||
|
UpdateHostLockWipeStatusFromAppleMDMResultFunc UpdateHostLockWipeStatusFromAppleMDMResultFunc
|
||||||
|
UpdateHostLockWipeStatusFromAppleMDMResultFuncInvoked bool
|
||||||
|
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -4955,32 +4970,32 @@ func (s *DataStore) BatchSetScripts(ctx context.Context, tmID *uint, scripts []*
|
||||||
return s.BatchSetScriptsFunc(ctx, tmID, 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.mu.Lock()
|
||||||
s.GetHostLockWipeStatusFuncInvoked = true
|
s.GetHostLockWipeStatusFuncInvoked = true
|
||||||
s.mu.Unlock()
|
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.mu.Lock()
|
||||||
s.LockHostViaScriptFuncInvoked = true
|
s.LockHostViaScriptFuncInvoked = true
|
||||||
s.mu.Unlock()
|
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.mu.Lock()
|
||||||
s.UnlockHostViaScriptFuncInvoked = true
|
s.UnlockHostViaScriptFuncInvoked = true
|
||||||
s.mu.Unlock()
|
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.mu.Lock()
|
||||||
s.UnlockHostManuallyFuncInvoked = true
|
s.UnlockHostManuallyFuncInvoked = true
|
||||||
s.mu.Unlock()
|
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 {
|
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()
|
s.mu.Unlock()
|
||||||
return s.CleanMacOSMDMLockFunc(ctx, hostUUID)
|
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),
|
Detail: apple_mdm.FmtErrorChain(cmdResult.ErrorChain),
|
||||||
OperationType: fleet.MDMOperationTypeRemove,
|
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
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -760,7 +760,7 @@ func TestHostDetailsMDMProfiles(t *testing.T) {
|
||||||
ds.GetHostMDMMacOSSetupFunc = func(ctx context.Context, hostID uint) (*fleet.HostMDMMacOSSetup, error) {
|
ds.GetHostMDMMacOSSetupFunc = func(ctx context.Context, hostID uint) (*fleet.HostMDMMacOSSetup, error) {
|
||||||
return nil, nil
|
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
|
return &fleet.HostLockWipeStatus{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -388,3 +388,11 @@ func (c *Client) MDMUnlockHost(hostID uint) (string, error) {
|
||||||
}
|
}
|
||||||
return response.UnlockPIN, nil
|
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.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]+}/lock", lockHostEndpoint, lockHostRequest{})
|
||||||
ue.POST("/api/_version_/fleet/hosts/{id:[0-9]+}/unlock", unlockHostEndpoint, unlockHostRequest{})
|
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.
|
// Only Fleet MDM specific endpoints should be within the root /mdm/ path.
|
||||||
// NOTE: remember to update
|
// NOTE: remember to update
|
||||||
|
|
|
||||||
|
|
@ -1103,7 +1103,7 @@ func (svc *Service) getHostDetails(ctx context.Context, host *fleet.Host, opts f
|
||||||
}
|
}
|
||||||
host.MDM.MacOSSetup = macOSSetup
|
host.MDM.MacOSSetup = macOSSetup
|
||||||
|
|
||||||
mdmActions, err := svc.ds.GetHostLockWipeStatus(ctx, host.ID, host.FleetPlatform())
|
mdmActions, err := svc.ds.GetHostLockWipeStatus(ctx, host)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, ctxerr.Wrap(ctx, err, "get host mdm lock/wipe status")
|
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("")
|
host.MDM.PendingAction = ptr.String("")
|
||||||
// device status
|
// device status
|
||||||
switch {
|
switch {
|
||||||
case mdmActions.IsLocked():
|
|
||||||
host.MDM.DeviceStatus = ptr.String("locked")
|
|
||||||
case mdmActions.IsWiped():
|
case mdmActions.IsWiped():
|
||||||
host.MDM.DeviceStatus = ptr.String("wiped")
|
host.MDM.DeviceStatus = ptr.String("wiped")
|
||||||
|
case mdmActions.IsLocked():
|
||||||
|
host.MDM.DeviceStatus = ptr.String("locked")
|
||||||
}
|
}
|
||||||
|
|
||||||
// pending action, if any
|
// pending action, if any
|
||||||
|
|
|
||||||
|
|
@ -67,7 +67,7 @@ func TestHostDetails(t *testing.T) {
|
||||||
ds.ListHostBatteriesFunc = func(ctx context.Context, hostID uint) ([]*fleet.HostBattery, error) {
|
ds.ListHostBatteriesFunc = func(ctx context.Context, hostID uint) ([]*fleet.HostBattery, error) {
|
||||||
return dsBats, nil
|
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
|
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.
|
// 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) {
|
ds.ListHostBatteriesFunc = func(ctx context.Context, hostID uint) ([]*fleet.HostBattery, error) {
|
||||||
return nil, nil
|
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
|
return &fleet.HostLockWipeStatus{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -385,7 +385,7 @@ func TestHostDetailsOSSettings(t *testing.T) {
|
||||||
ds.GetHostMDMMacOSSetupFunc = func(ctx context.Context, hid uint) (*fleet.HostMDMMacOSSetup, error) {
|
ds.GetHostMDMMacOSSetupFunc = func(ctx context.Context, hid uint) (*fleet.HostMDMMacOSSetup, error) {
|
||||||
return nil, nil
|
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
|
return &fleet.HostLockWipeStatus{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -497,7 +497,7 @@ func TestHostDetailsOSSettingsWindowsOnly(t *testing.T) {
|
||||||
ds.GetHostMDMWindowsProfilesFunc = func(ctx context.Context, uuid string) ([]fleet.HostMDMWindowsProfile, error) {
|
ds.GetHostMDMWindowsProfilesFunc = func(ctx context.Context, uuid string) ([]fleet.HostMDMWindowsProfile, error) {
|
||||||
return nil, nil
|
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
|
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) {
|
ds.ListHostUpcomingActivitiesFunc = func(ctx context.Context, hostID uint, opt fleet.ListOptions) ([]*fleet.Activity, *fleet.PaginationMetadata, error) {
|
||||||
return nil, nil, nil
|
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
|
return &fleet.HostLockWipeStatus{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1383,7 +1383,7 @@ func TestHostMDMProfileDetail(t *testing.T) {
|
||||||
ds.GetHostMDMMacOSSetupFunc = func(ctx context.Context, hid uint) (*fleet.HostMDMMacOSSetup, error) {
|
ds.GetHostMDMMacOSSetupFunc = func(ctx context.Context, hid uint) (*fleet.HostMDMMacOSSetup, error) {
|
||||||
return nil, nil
|
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
|
return &fleet.HostLockWipeStatus{}, nil
|
||||||
}
|
}
|
||||||
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
|
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)
|
ds := new(mock.Store)
|
||||||
svc, ctx := newTestService(t, ds, nil, nil, &TestServerOpts{License: &fleet.LicenseInfo{Tier: fleet.TierPremium}})
|
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"}
|
teamHost := &fleet.Host{TeamID: ptr.Uint(1), Platform: "darwin"}
|
||||||
globalHost := &fleet.Host{Platform: "darwin"}
|
globalHost := &fleet.Host{Platform: "darwin"}
|
||||||
|
|
||||||
ds.HostByIdentifierFunc = func(ctx context.Context, identifier string) (*fleet.Host, error) {
|
ds.HostByIdentifierFunc = func(ctx context.Context, identifier string) (*fleet.Host, error) {
|
||||||
if identifier == "1" {
|
if identifier == fmt.Sprint(teamHostID) {
|
||||||
return teamHost, nil
|
return teamHost, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1483,14 +1488,14 @@ func TestLockUnlockHostAuth(t *testing.T) {
|
||||||
ds.GetHostMDMMacOSSetupFunc = func(ctx context.Context, hostID uint) (*fleet.HostMDMMacOSSetup, error) {
|
ds.GetHostMDMMacOSSetupFunc = func(ctx context.Context, hostID uint) (*fleet.HostMDMMacOSSetup, error) {
|
||||||
return nil, nil
|
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
|
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
|
return nil
|
||||||
}
|
}
|
||||||
ds.HostLiteFunc = func(ctx context.Context, hostID uint) (*fleet.Host, error) {
|
ds.HostLiteFunc = func(ctx context.Context, hostID uint) (*fleet.Host, error) {
|
||||||
if hostID == 1 {
|
if hostID == teamHostID {
|
||||||
return teamHost, nil
|
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 {
|
ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error {
|
||||||
return nil
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1596,25 +1601,30 @@ func TestLockUnlockHostAuth(t *testing.T) {
|
||||||
}
|
}
|
||||||
ctx := viewer.NewContext(ctx, viewer.Viewer{User: tt.user})
|
ctx := viewer.NewContext(ctx, viewer.Viewer{User: tt.user})
|
||||||
|
|
||||||
err := svc.LockHost(ctx, 2)
|
err := svc.LockHost(ctx, globalHostID)
|
||||||
checkAuthErr(t, tt.shouldFailGlobalWrite, err)
|
checkAuthErr(t, tt.shouldFailGlobalWrite, err)
|
||||||
err = svc.LockHost(ctx, 1)
|
err = svc.LockHost(ctx, teamHostID)
|
||||||
checkAuthErr(t, tt.shouldFailTeamWrite, err)
|
checkAuthErr(t, tt.shouldFailTeamWrite, err)
|
||||||
|
|
||||||
// Pretend we locked the host
|
// Pretend we locked the host
|
||||||
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{HostFleetPlatform: fleetPlatform, LockMDMCommand: &fleet.MDMCommand{}, LockMDMCommandResult: &fleet.MDMCommandResult{Status: fleet.MDMAppleStatusAcknowledged}}, nil
|
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)
|
checkAuthErr(t, tt.shouldFailGlobalWrite, err)
|
||||||
_, err = svc.UnlockHost(ctx, 1)
|
_, err = svc.UnlockHost(ctx, teamHostID)
|
||||||
checkAuthErr(t, tt.shouldFailTeamWrite, err)
|
checkAuthErr(t, tt.shouldFailTeamWrite, err)
|
||||||
|
|
||||||
// Reset so we're now pretending host is unlocked
|
// 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
|
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",
|
"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/lock", nil, http.StatusPaymentRequired)
|
||||||
s.Do("POST", "/api/v1/fleet/hosts/123/unlock", 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() {
|
func (s *integrationTestSuite) TestScriptsEndpointsWithoutLicense() {
|
||||||
|
|
|
||||||
|
|
@ -6867,7 +6867,7 @@ func (s *integrationEnterpriseTestSuite) TestAllSoftwareTitles() {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *integrationEnterpriseTestSuite) TestLockUnlockWindowsLinux() {
|
func (s *integrationEnterpriseTestSuite) TestLockUnlockWipeWindowsLinux() {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
t := s.T()
|
t := s.T()
|
||||||
|
|
||||||
|
|
@ -6889,19 +6889,22 @@ func (s *integrationEnterpriseTestSuite) TestLockUnlockWindowsLinux() {
|
||||||
require.NotNil(t, getHostResp.Host.MDM.PendingAction)
|
require.NotNil(t, getHostResp.Host.MDM.PendingAction)
|
||||||
require.Equal(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)
|
res := s.DoRaw("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/lock", winHost.ID), nil, http.StatusBadRequest)
|
||||||
errMsg := extractServerErrorText(res.Body)
|
errMsg := extractServerErrorText(res.Body)
|
||||||
require.Contains(t, errMsg, "Windows MDM isn't turned on.")
|
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)
|
res = s.DoRaw("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/unlock", winHost.ID), nil, http.StatusBadRequest)
|
||||||
errMsg = extractServerErrorText(res.Body)
|
errMsg = extractServerErrorText(res.Body)
|
||||||
require.Contains(t, errMsg, "Windows MDM isn't turned on.")
|
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)
|
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
|
// 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)
|
require.NoError(t, err)
|
||||||
|
|
||||||
var orbitScriptResp orbitPostScriptResultResponse
|
var orbitScriptResp orbitPostScriptResultResponse
|
||||||
|
|
@ -6923,6 +6926,12 @@ func (s *integrationEnterpriseTestSuite) TestLockUnlockWindowsLinux() {
|
||||||
require.Equal(t, "locked", *getHostResp.Host.MDM.DeviceStatus)
|
require.Equal(t, "locked", *getHostResp.Host.MDM.DeviceStatus)
|
||||||
require.NotNil(t, getHostResp.Host.MDM.PendingAction)
|
require.NotNil(t, getHostResp.Host.MDM.PendingAction)
|
||||||
require.Equal(t, "unlock", *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
|
// 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)
|
checkInstallFleetdCommandSent(mdmDevice, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *integrationMDMTestSuite) TestLockUnlockWindowsLinux() {
|
func (s *integrationMDMTestSuite) TestLockUnlockWipeWindowsLinux() {
|
||||||
t := s.T()
|
t := s.T()
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
// create an MDM-enrolled Windows host
|
// 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)
|
linuxHost := createOrbitEnrolledHost(t, "linux", "lock_unlock_linux", s.ds)
|
||||||
|
|
||||||
for _, host := range []*fleet.Host{winHost, linuxHost} {
|
for _, host := range []*fleet.Host{winHost, linuxHost} {
|
||||||
|
|
@ -11487,7 +11490,7 @@ func (s *integrationMDMTestSuite) TestLockUnlockWindowsLinux() {
|
||||||
require.Contains(t, errMsg, "Host has pending lock request.")
|
require.Contains(t, errMsg, "Host has pending lock request.")
|
||||||
|
|
||||||
// simulate a successful script result for the lock command
|
// 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)
|
require.NoError(t, err)
|
||||||
|
|
||||||
var orbitScriptResp orbitPostScriptResultResponse
|
var orbitScriptResp orbitPostScriptResultResponse
|
||||||
|
|
@ -11504,6 +11507,10 @@ func (s *integrationMDMTestSuite) TestLockUnlockWindowsLinux() {
|
||||||
|
|
||||||
// try to lock the host again
|
// try to lock the host again
|
||||||
s.Do("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/lock", host.ID), nil, http.StatusConflict)
|
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
|
// unlock the host
|
||||||
s.Do("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/unlock", host.ID), nil, http.StatusNoContent)
|
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.")
|
require.Contains(t, errMsg, "Host has pending unlock request.")
|
||||||
|
|
||||||
// simulate a failed script result for the unlock command
|
// 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)
|
require.NoError(t, err)
|
||||||
|
|
||||||
s.DoJSON("POST", "/api/fleet/orbit/scripts/result",
|
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.Equal(t, "locked", *getHostResp.Host.MDM.DeviceStatus)
|
||||||
require.NotNil(t, getHostResp.Host.MDM.PendingAction)
|
require.NotNil(t, getHostResp.Host.MDM.PendingAction)
|
||||||
require.Equal(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() {
|
func (s *integrationMDMTestSuite) TestZCustomConfigurationWebURL() {
|
||||||
t := s.T()
|
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")
|
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)] {
|
if appleMDMPremiumCommands[strings.TrimSpace(cmd.Command.RequestType)] {
|
||||||
lic, err := svc.License(ctx)
|
lic, err := svc.License(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -621,15 +603,6 @@ func (svc *Service) enqueueMicrosoftMDMCommand(ctx context.Context, rawXMLCmd []
|
||||||
return nil, ctxerr.Wrap(ctx, err, "decode SyncML command")
|
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() {
|
if cmdMsg.IsPremium() {
|
||||||
lic, err := svc.License(ctx)
|
lic, err := svc.License(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
|
|
@ -924,3 +924,34 @@ func (svc *Service) UnlockHost(ctx context.Context, hostID uint) (string, error)
|
||||||
|
|
||||||
return "", fleet.ErrMissingLicense
|
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