Skip to content

Commit

Permalink
Added error messages when scripts are disabled. (#18174)
Browse files Browse the repository at this point in the history
#17148

Added error messages to lock/unlock/wipe when scripts are disabled.

# Checklist for submitter
- [x] Added/updated tests
- [x] Manual QA for all new/changed functionality
  • Loading branch information
getvictor committed Apr 10, 2024
1 parent 24d0f70 commit 3859c97
Show file tree
Hide file tree
Showing 8 changed files with 213 additions and 5 deletions.
77 changes: 72 additions & 5 deletions cmd/fleetctl/mdm_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"fmt"
"os"
"slices"
"testing"
"time"

Expand Down Expand Up @@ -487,6 +488,21 @@ func TestMDMLockCommand(t *testing.T) {
return h.MDMInfo, nil
}

ds.GetHostOrbitInfoFunc = func(ctx context.Context, hostID uint) (*fleet.HostOrbitInfo, error) {
hostIDMod := hostID % 3
switch hostIDMod {
case 0:
return nil, &notFoundError{}
case 1:
return &fleet.HostOrbitInfo{}, nil
case 2:
return &fleet.HostOrbitInfo{ScriptsEnabled: ptr.Bool(true)}, nil
default:
t.Errorf("unexpected hostIDMod %v", hostIDMod)
return nil, nil
}
}

appCfgAllMDM, appCfgWinMDM, appCfgMacMDM, appCfgNoMDM := setupAppConigs()

successfulOutput := func(ident string) string {
Expand Down Expand Up @@ -730,6 +746,21 @@ func TestMDMUnlockCommand(t *testing.T) {
return h.MDMInfo, nil
}

ds.GetHostOrbitInfoFunc = func(ctx context.Context, hostID uint) (*fleet.HostOrbitInfo, error) {
hostIDMod := hostID % 3
switch hostIDMod {
case 0:
return nil, &notFoundError{}
case 1:
return &fleet.HostOrbitInfo{}, nil
case 2:
return &fleet.HostOrbitInfo{ScriptsEnabled: ptr.Bool(true)}, nil
default:
t.Errorf("unexpected hostIDMod %v", hostIDMod)
return nil, nil
}
}

appCfgAllMDM, appCfgWinMDM, appCfgMacMDM, appCfgNoMDM := setupAppConigs()

successfulOutput := func(ident string) string {
Expand Down Expand Up @@ -793,11 +824,6 @@ func TestMDMWipeCommand(t *testing.T) {
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",
Expand Down Expand Up @@ -892,13 +918,32 @@ func TestMDMWipeCommand(t *testing.T) {
MDMInfo: &fleet.HostMDM{Enrolled: true, Name: fleet.WellKnownMDMFleet},
MDM: fleet.MDMHostData{Name: fleet.WellKnownMDMFleet, EnrollmentStatus: ptr.String("On (manual")},
}
linuxEnrolled := &fleet.Host{
ID: 18,
UUID: "linux-enrolled",
Platform: "linux",
}
linuxEnrolled2 := &fleet.Host{
ID: 19,
UUID: "linux-enrolled",
Platform: "linux",
}
linuxEnrolled3 := &fleet.Host{
ID: 20,
UUID: "linux-enrolled",
Platform: "linux",
}

linuxHostIDs := []uint{linuxEnrolled.ID, linuxEnrolled2.ID, linuxEnrolled3.ID}

hostByUUID := make(map[string]*fleet.Host)
hostsByID := make(map[uint]*fleet.Host)
for _, h := range []*fleet.Host{
winEnrolled,
macEnrolled,
linuxEnrolled,
linuxEnrolled2,
linuxEnrolled3,
macNotEnrolled,
winNotEnrolled,
macPending,
Expand Down Expand Up @@ -1039,6 +1084,26 @@ func TestMDMWipeCommand(t *testing.T) {
return h.MDMInfo, nil
}

// This function should only run on linux
ds.GetHostOrbitInfoFunc = func(ctx context.Context, hostID uint) (*fleet.HostOrbitInfo, error) {
if !slices.Contains(linuxHostIDs, hostID) {
t.Errorf("GetHostOrbitInfo should not be called for non-linux host %v", hostID)
return nil, nil
}
hostIDMod := hostID % 3
switch hostIDMod {
case 0:
return nil, &notFoundError{}
case 1:
return &fleet.HostOrbitInfo{}, nil
case 2:
return &fleet.HostOrbitInfo{ScriptsEnabled: ptr.Bool(true)}, nil
default:
t.Errorf("unexpected hostIDMod %v", hostIDMod)
return nil, nil
}
}

appCfgAllMDM, appCfgWinMDM, appCfgMacMDM, appCfgNoMDM := setupAppConigs()
appCfgScriptsDisabled := &fleet.AppConfig{ServerSettings: fleet.ServerSettings{ScriptsDisabled: true}}

Expand All @@ -1055,6 +1120,8 @@ func TestMDMWipeCommand(t *testing.T) {
{appCfgAllMDM, "valid windows", []string{"--host", winEnrolled.UUID}, ""},
{appCfgAllMDM, "valid macos", []string{"--host", macEnrolled.UUID}, ""},
{appCfgNoMDM, "valid linux", []string{"--host", linuxEnrolled.UUID}, ""},
{appCfgNoMDM, "valid linux 2", []string{"--host", linuxEnrolled2.UUID}, ""},
{appCfgNoMDM, "valid linux 3", []string{"--host", linuxEnrolled3.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.`},
Expand Down
42 changes: 42 additions & 0 deletions ee/server/service/hosts.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,20 @@ func (svc *Service) LockHost(ctx context.Context, hostID uint) error {
if appCfg.ServerSettings.ScriptsDisabled {
return ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("host_id", "Can't lock host because running scripts is disabled in organization settings."))
}
hostOrbitInfo, err := svc.ds.GetHostOrbitInfo(ctx, host.ID)
switch {
case err != nil:
// If not found, then do nothing. We do not know if this host has scripts enabled or not
if !fleet.IsNotFound(err) {
return ctxerr.Wrap(ctx, err, "get host orbit info")
}
case hostOrbitInfo.ScriptsEnabled != nil && !*hostOrbitInfo.ScriptsEnabled:
return ctxerr.Wrap(
ctx, fleet.NewInvalidArgumentError(
"host_id", "Couldn't lock host. To lock, deploy the fleetd agent with --enable-scripts and refetch host vitals.",
),
)
}

default:
return ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("host_id", fmt.Sprintf("Unsupported host platform: %s", host.Platform)))
Expand Down Expand Up @@ -166,6 +180,20 @@ func (svc *Service) UnlockHost(ctx context.Context, hostID uint) (string, error)
if appCfg.ServerSettings.ScriptsDisabled {
return "", ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("host_id", "Can't unlock host because running scripts is disabled in organization settings."))
}
hostOrbitInfo, err := svc.ds.GetHostOrbitInfo(ctx, host.ID)
switch {
case err != nil:
// If not found, then do nothing. We do not know if this host has scripts enabled or not
if !fleet.IsNotFound(err) {
return "", ctxerr.Wrap(ctx, err, "get host orbit info")
}
case hostOrbitInfo.ScriptsEnabled != nil && !*hostOrbitInfo.ScriptsEnabled:
return "", ctxerr.Wrap(
ctx, fleet.NewInvalidArgumentError(
"host_id", "Couldn't unlock host. To unlock, deploy the fleetd agent with --enable-scripts and refetch host vitals.",
),
)
}

default:
return "", ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("host_id", fmt.Sprintf("Unsupported host platform: %s", host.Platform)))
Expand Down Expand Up @@ -248,6 +276,20 @@ func (svc *Service) WipeHost(ctx context.Context, hostID uint) error {
if appCfg.ServerSettings.ScriptsDisabled {
return ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("host_id", "Can't wipe host because running scripts is disabled in organization settings."))
}
hostOrbitInfo, err := svc.ds.GetHostOrbitInfo(ctx, host.ID)
switch {
case err != nil:
// If not found, then do nothing. We do not know if this host has scripts enabled or not
if !fleet.IsNotFound(err) {
return ctxerr.Wrap(ctx, err, "get host orbit info")
}
case hostOrbitInfo.ScriptsEnabled != nil && !*hostOrbitInfo.ScriptsEnabled:
return ctxerr.Wrap(
ctx, fleet.NewInvalidArgumentError(
"host_id", "Couldn't wipe host. To wipe, deploy the fleetd agent with --enable-scripts and refetch host vitals.",
),
)
}

default:
return ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("host_id", fmt.Sprintf("Unsupported host platform: %s", host.Platform)))
Expand Down
19 changes: 19 additions & 0 deletions server/datastore/mysql/hosts.go
Original file line number Diff line number Diff line change
Expand Up @@ -3708,6 +3708,25 @@ func (ds *Datastore) SetOrUpdateHostOrbitInfo(
)
}

func (ds *Datastore) GetHostOrbitInfo(ctx context.Context, hostID uint) (*fleet.HostOrbitInfo, error) {
var orbit fleet.HostOrbitInfo
err := sqlx.GetContext(
ctx, ds.reader(ctx), &orbit, `
SELECT
scripts_enabled
FROM
host_orbit_info
WHERE host_id = ?`, hostID,
)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, ctxerr.Wrap(ctx, notFound("HostOrbitInfo").WithID(hostID))
}
return nil, ctxerr.Wrapf(ctx, err, "select host_orbit_info for host_id %d", hostID)
}
return &orbit, nil
}

func (ds *Datastore) getOrInsertMDMSolution(ctx context.Context, serverURL string, mdmName string) (mdmID uint, err error) {
readStmt := &parameterizedStmt{
Statement: `SELECT id FROM mobile_device_management_solutions WHERE name = ? AND server_url = ?`,
Expand Down
39 changes: 39 additions & 0 deletions server/datastore/mysql/hosts_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@ func TestHosts(t *testing.T) {
{"ListHostsWithPagination", testListHostsWithPagination},
{"LastRestarted", testLastRestarted},
{"HostHealth", testHostHealth},
{"GetHostOrbitInfo", testGetHostOrbitInfo},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
Expand Down Expand Up @@ -8756,3 +8757,41 @@ func testHostHealth(t *testing.T, ds *Datastore) {
require.Empty(t, hh.VulnerableSoftware)
require.Equal(t, h.TeamID, hh.TeamID)
}

func testGetHostOrbitInfo(t *testing.T, ds *Datastore) {
host, err := ds.NewHost(
context.Background(), &fleet.Host{
DetailUpdatedAt: time.Now(),
LabelUpdatedAt: time.Now(),
PolicyUpdatedAt: time.Now(),
SeenTime: time.Now(),
NodeKey: ptr.String("1"),
UUID: "1",
Hostname: "foo.local",
PrimaryIP: "192.168.1.1",
PrimaryMac: "30-65-EC-6F-C4-58",
},
)
require.NoError(t, err)
require.NotNil(t, host)

_, err = ds.GetHostOrbitInfo(context.Background(), host.ID)
require.True(t, fleet.IsNotFound(err))

orbitVersion := "1.1.0"
err = ds.SetOrUpdateHostOrbitInfo(
context.Background(), host.ID, orbitVersion, sql.NullString{Valid: false}, sql.NullBool{Valid: false},
)
require.NoError(t, err)
hostOrbitInfo, err := ds.GetHostOrbitInfo(context.Background(), host.ID)
require.NoError(t, err)
assert.Nil(t, hostOrbitInfo.ScriptsEnabled)

err = ds.SetOrUpdateHostOrbitInfo(
context.Background(), host.ID, orbitVersion, sql.NullString{Valid: false}, sql.NullBool{Bool: true, Valid: true},
)
require.NoError(t, err)
hostOrbitInfo, err = ds.GetHostOrbitInfo(context.Background(), host.ID)
require.NoError(t, err)
assert.True(t, *hostOrbitInfo.ScriptsEnabled)
}
2 changes: 2 additions & 0 deletions server/fleet/datastore.go
Original file line number Diff line number Diff line change
Expand Up @@ -841,6 +841,8 @@ type Datastore interface {
ctx context.Context, hostID uint, version string, desktopVersion sql.NullString, scriptsEnabled sql.NullBool,
) error

GetHostOrbitInfo(ctx context.Context, hostID uint) (*HostOrbitInfo, error)

ReplaceHostDeviceMapping(ctx context.Context, id uint, mappings []*HostDeviceMapping, source string) error

// ReplaceHostBatteries creates or updates the battery mappings of a host.
Expand Down
5 changes: 5 additions & 0 deletions server/fleet/hosts.go
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,11 @@ type Host struct {
Policies *[]*HostPolicy `json:"policies,omitempty" csv:"-"`
}

// HostOrbitInfo maps to the host_orbit_info table in the database, which maps to the orbit_info agent table.
type HostOrbitInfo struct {
ScriptsEnabled *bool `json:"scripts_enabled" db:"scripts_enabled"`
}

// HostHealth contains a subset of Host data that indicates how healthy a Host is. For fields with
// the same name, see the comments/docs for the Host field above.
type HostHealth struct {
Expand Down
12 changes: 12 additions & 0 deletions server/mock/datastore_mock.go
Original file line number Diff line number Diff line change
Expand Up @@ -589,6 +589,8 @@ type GetHostMDMProfileRetryCountByCommandUUIDFunc func(ctx context.Context, host

type SetOrUpdateHostOrbitInfoFunc func(ctx context.Context, hostID uint, version string, desktopVersion sql.NullString, scriptsEnabled sql.NullBool) error

type GetHostOrbitInfoFunc func(ctx context.Context, hostID uint) (*fleet.HostOrbitInfo, error)

type ReplaceHostDeviceMappingFunc func(ctx context.Context, id uint, mappings []*fleet.HostDeviceMapping, source string) error

type ReplaceHostBatteriesFunc func(ctx context.Context, id uint, mappings []*fleet.HostBattery) error
Expand Down Expand Up @@ -1755,6 +1757,9 @@ type DataStore struct {
SetOrUpdateHostOrbitInfoFunc SetOrUpdateHostOrbitInfoFunc
SetOrUpdateHostOrbitInfoFuncInvoked bool

GetHostOrbitInfoFunc GetHostOrbitInfoFunc
GetHostOrbitInfoFuncInvoked bool

ReplaceHostDeviceMappingFunc ReplaceHostDeviceMappingFunc
ReplaceHostDeviceMappingFuncInvoked bool

Expand Down Expand Up @@ -4218,6 +4223,13 @@ func (s *DataStore) SetOrUpdateHostOrbitInfo(ctx context.Context, hostID uint, v
return s.SetOrUpdateHostOrbitInfoFunc(ctx, hostID, version, desktopVersion, scriptsEnabled)
}

func (s *DataStore) GetHostOrbitInfo(ctx context.Context, hostID uint) (*fleet.HostOrbitInfo, error) {
s.mu.Lock()
s.GetHostOrbitInfoFuncInvoked = true
s.mu.Unlock()
return s.GetHostOrbitInfoFunc(ctx, hostID)
}

func (s *DataStore) ReplaceHostDeviceMapping(ctx context.Context, id uint, mappings []*fleet.HostDeviceMapping, source string) error {
s.mu.Lock()
s.ReplaceHostDeviceMappingFuncInvoked = true
Expand Down
22 changes: 22 additions & 0 deletions server/service/integration_enterprise_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7244,6 +7244,28 @@ func (s *integrationEnterpriseTestSuite) TestLockUnlockWipeWindowsLinux() {
errMsg = extractServerErrorText(res.Body)
require.Contains(t, errMsg, "Windows MDM isn't turned on.")

// Disable scripts on Linux host
err := s.ds.SetOrUpdateHostOrbitInfo(
context.Background(), linuxHost.ID, "1.22.0", sql.NullString{}, sql.NullBool{Bool: false, Valid: true},
)
require.NoError(t, err)
// try to lock/unlock/wipe the Linux host. Fails because scripts are not enabled.
res = s.DoRaw("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/lock", linuxHost.ID), nil, http.StatusUnprocessableEntity)
errMsg = extractServerErrorText(res.Body)
require.Contains(t, errMsg, "Couldn't lock host. To lock, deploy the fleetd agent with --enable-scripts")
res = s.DoRaw("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/unlock", linuxHost.ID), nil, http.StatusUnprocessableEntity)
errMsg = extractServerErrorText(res.Body)
require.Contains(t, errMsg, "Couldn't unlock host. To unlock, deploy the fleetd agent with --enable-scripts")
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, "Couldn't wipe host. To wipe, deploy the fleetd agent with --enable-scripts")

// Enable scripts on Linux host
err = s.ds.SetOrUpdateHostOrbitInfo(
context.Background(), linuxHost.ID, "1.22.0", sql.NullString{}, sql.NullBool{Bool: true, Valid: true},
)
require.NoError(t, err)

// 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)

Expand Down

0 comments on commit 3859c97

Please sign in to comment.