Skip to content

Commit 1725eff

Browse files
authored
Allow software uninstalls, script-based lock/unlock/wipe, while scripts are globally disabled (#24815)
For #22875. # Checklist for submitter If some of the following don't apply, delete the relevant line. <!-- Note that API documentation changes are now addressed by the product design team. --> - [x] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. See [Changes files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/Committing-Changes.md#changes-files) for more information. - [x] Input data is properly validated, `SELECT *` is avoided, SQL injection is prevented (using placeholders for values in statements) - [x] Added/updated tests - [x] If database migrations are included, checked table schema to confirm autoupdate - For database migrations: - [x] Checked schema for all modified table for columns that will auto-update timestamps during migration. - [x] Confirmed that updating the timestamps is acceptable, and will not cause unwanted side effects. - [x] Ensured the correct collation is explicitly set for character columns (`COLLATE utf8mb4_unicode_ci`). - [x] Manual QA for all new/changed functionality
1 parent 16d309a commit 1725eff

23 files changed

+248
-145
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
* Allowed software uninstalls and script-based host lock/unlock/wipe to run while global scripts are disabled.

cmd/fleetctl/mdm.go

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -265,16 +265,6 @@ func mdmWipeCommand() *cli.Command {
265265
return err
266266
}
267267

268-
config, err := client.GetAppConfig()
269-
if err != nil {
270-
return err
271-
}
272-
273-
// linux hosts need scripts to be enabled in the org settings to wipe.
274-
if host.Platform == "linux" && config.ServerSettings.ScriptsDisabled {
275-
return errors.New("Can't wipe host because running scripts is disabled in organization settings.")
276-
}
277-
278268
if err := client.MDMWipeHost(host.ID); err != nil {
279269
return fmt.Errorf("Failed to wipe host: %w", err)
280270
}

cmd/fleetctl/mdm_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1246,7 +1246,7 @@ func TestMDMWipeCommand(t *testing.T) {
12461246
{appCfgAllMDM, "valid windows but host is locked", []string{"--host", winEnrolledLocked.host.UUID}, "Host cannot be wiped until it is unlocked."},
12471247
{appCfgAllMDM, "valid macos but host is locked", []string{"--host", macEnrolledLocked.host.UUID}, "Host cannot be wiped until it is unlocked."},
12481248
{appCfgAllMDM, "valid macos but host is locked", []string{"--host", macEnrolledLocked.host.UUID}, "Host cannot be wiped until it is unlocked."},
1249-
{appCfgScriptsDisabled, "valid linux but script are disabled", []string{"--host", linuxEnrolled.host.UUID}, "Can't wipe host because running scripts is disabled in organization settings."},
1249+
{appCfgScriptsDisabled, "valid linux and scripts are disabled", []string{"--host", linuxEnrolled.host.UUID}, ""},
12501250
}
12511251

12521252
successfulOutput := func(ident string) string {

cmd/fleetctl/scripts_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -338,7 +338,7 @@ Fleet records the last 10,000 characters to prevent downtime.
338338
}
339339
return &h, nil
340340
}
341-
ds.ListPendingHostScriptExecutionsFunc = func(ctx context.Context, hid uint) ([]*fleet.HostScriptResult, error) {
341+
ds.ListPendingHostScriptExecutionsFunc = func(ctx context.Context, hid uint, onlyShowInternal bool) ([]*fleet.HostScriptResult, error) {
342342
require.Equal(t, uint(42), hid)
343343
if c.expectPending {
344344
return []*fleet.HostScriptResult{{HostID: uint(42)}}, nil

ee/server/service/hosts.go

Lines changed: 3 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -91,18 +91,6 @@ func (svc *Service) LockHost(ctx context.Context, hostID uint, viewPIN bool) (un
9191
return "", ctxerr.Wrap(ctx, err, "check windows MDM enabled")
9292
}
9393
}
94-
// on windows and linux, a script is used to lock the host so scripts must
95-
// be enabled
96-
appCfg, err := svc.ds.AppConfig(ctx)
97-
if err != nil {
98-
return "", ctxerr.Wrap(ctx, err, "get app config")
99-
}
100-
if appCfg.ServerSettings.ScriptsDisabled {
101-
return "", ctxerr.Wrap(
102-
ctx,
103-
fleet.NewInvalidArgumentError("host_id", "Can't lock host because running scripts is disabled in organization settings."),
104-
)
105-
}
10694
hostOrbitInfo, err := svc.ds.GetHostOrbitInfo(ctx, host.ID)
10795
switch {
10896
case err != nil:
@@ -181,8 +169,8 @@ func (svc *Service) UnlockHost(ctx context.Context, hostID uint) (string, error)
181169
// is currently locked.
182170

183171
case "windows", "linux":
184-
// on windows and linux, a script is used to lock the host so scripts must
185-
// be enabled
172+
// on Windows and Linux, a script is used to unlock the host so scripts must
173+
// be enabled on the host
186174
if host.FleetPlatform() == "windows" {
187175
if err := svc.VerifyMDMWindowsConfigured(ctx); err != nil {
188176
if errors.Is(err, fleet.ErrMDMNotConfigured) {
@@ -191,13 +179,6 @@ func (svc *Service) UnlockHost(ctx context.Context, hostID uint) (string, error)
191179
return "", ctxerr.Wrap(ctx, err, "check windows MDM enabled")
192180
}
193181
}
194-
appCfg, err := svc.ds.AppConfig(ctx)
195-
if err != nil {
196-
return "", ctxerr.Wrap(ctx, err, "get app config")
197-
}
198-
if appCfg.ServerSettings.ScriptsDisabled {
199-
return "", ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("host_id", "Can't unlock host because running scripts is disabled in organization settings."))
200-
}
201182
hostOrbitInfo, err := svc.ds.GetHostOrbitInfo(ctx, host.ID)
202183
switch {
203184
case err != nil:
@@ -286,14 +267,7 @@ func (svc *Service) WipeHost(ctx context.Context, hostID uint) error {
286267
requireMDM = true
287268

288269
case "linux":
289-
// on linux, a script is used to wipe the host so scripts must be enabled
290-
appCfg, err := svc.ds.AppConfig(ctx)
291-
if err != nil {
292-
return ctxerr.Wrap(ctx, err, "get app config")
293-
}
294-
if appCfg.ServerSettings.ScriptsDisabled {
295-
return ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("host_id", "Can't wipe host because running scripts is disabled in organization settings."))
296-
}
270+
// on linux, a script is used to wipe the host so scripts must be enabled on the host
297271
hostOrbitInfo, err := svc.ds.GetHostOrbitInfo(ctx, host.ID)
298272
switch {
299273
case err != nil:

ee/server/service/software_installers.go

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1026,18 +1026,6 @@ func (svc *Service) installSoftwareTitleUsingInstaller(ctx context.Context, host
10261026
}
10271027

10281028
func (svc *Service) UninstallSoftwareTitle(ctx context.Context, hostID uint, softwareTitleID uint) error {
1029-
// First check if scripts are disabled globally. If so, no need for further processing.
1030-
cfg, err := svc.ds.AppConfig(ctx)
1031-
if err != nil {
1032-
svc.authz.SkipAuthorization(ctx)
1033-
return err
1034-
}
1035-
1036-
if cfg.ServerSettings.ScriptsDisabled {
1037-
svc.authz.SkipAuthorization(ctx)
1038-
return fleet.NewUserMessageError(errors.New(fleet.RunScriptScriptsDisabledGloballyErrMsg), http.StatusForbidden)
1039-
}
1040-
10411029
// we need to use ds.Host because ds.HostLite doesn't return the orbit node key
10421030
host, err := svc.ds.Host(ctx, hostID)
10431031
if err != nil {
@@ -1138,7 +1126,7 @@ func (svc *Service) UninstallSoftwareTitle(ctx context.Context, hostID uint, sof
11381126
if host.TeamID != nil {
11391127
teamID = *host.TeamID
11401128
}
1141-
// create the script execution request, the host will be notified of the
1129+
// create the script execution request; the host will be notified of the
11421130
// script execution request via the orbit config's Notifications mechanism.
11431131
request := fleet.HostScriptRequestPayload{
11441132
HostID: host.ID,
@@ -1149,7 +1137,7 @@ func (svc *Service) UninstallSoftwareTitle(ctx context.Context, hostID uint, sof
11491137
if ctxUser := authz.UserFromContext(ctx); ctxUser != nil {
11501138
request.UserID = &ctxUser.ID
11511139
}
1152-
scriptResult, err := svc.ds.NewHostScriptExecutionRequest(ctx, &request)
1140+
scriptResult, err := svc.ds.NewInternalScriptExecutionRequest(ctx, &request)
11531141
if err != nil {
11541142
return ctxerr.Wrap(ctx, err, "create script execution request")
11551143
}

ee/server/service/software_installers_test.go

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,9 @@ func TestInstallUninstallAuth(t *testing.T) {
8282
svc := newTestService(t, ds)
8383

8484
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
85-
return &fleet.AppConfig{}, nil
85+
return &fleet.AppConfig{
86+
ServerSettings: fleet.ServerSettings{ScriptsDisabled: true}, // global scripts being disabled shouldn't impact (un)installs
87+
}, nil
8688
}
8789
ds.HostFunc = func(ctx context.Context, id uint) (*fleet.Host, error) {
8890
return &fleet.Host{
@@ -111,9 +113,8 @@ func TestInstallUninstallAuth(t *testing.T) {
111113
ds.GetAnyScriptContentsFunc = func(ctx context.Context, id uint) ([]byte, error) {
112114
return []byte("script"), nil
113115
}
114-
ds.NewHostScriptExecutionRequestFunc = func(ctx context.Context, request *fleet.HostScriptRequestPayload) (*fleet.HostScriptResult,
115-
error,
116-
) {
116+
ds.NewInternalScriptExecutionRequestFunc = func(ctx context.Context, request *fleet.HostScriptRequestPayload) (*fleet.HostScriptResult,
117+
error) {
117118
return &fleet.HostScriptResult{
118119
ExecutionID: "execution_id",
119120
}, nil
@@ -187,18 +188,14 @@ func TestUninstallSoftwareTitle(t *testing.T) {
187188
return host, nil
188189
}
189190

190-
// Scripts disabled
191+
// Global scripts disabled (doesn't matter)
191192
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
192193
return &fleet.AppConfig{
193194
ServerSettings: fleet.ServerSettings{
194195
ScriptsDisabled: true,
195196
},
196197
}, nil
197198
}
198-
require.ErrorContains(t, svc.UninstallSoftwareTitle(context.Background(), 1, 10), fleet.RunScriptScriptsDisabledGloballyErrMsg)
199-
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
200-
return &fleet.AppConfig{}, nil
201-
}
202199

203200
// Host scripts disabled
204201
host.ScriptsEnabled = ptr.Bool(false)

frontend/pages/admin/OrgSettingsPage/cards/Advanced/Advanced.tsx

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -347,15 +347,18 @@ const Advanced = ({
347347
parseTarget
348348
tooltipContent={
349349
<>
350-
Disabling scripts will block access to run scripts. Scripts{" "}
351-
<br /> may still be added and removed in the UI and API. <br />
350+
Disabling script execution will block access to run scripts.
351+
<br />
352+
Scripts may still be added and removed in the UI and API.
353+
<br />
352354
<em>
353-
(Default: <strong>Off</strong>)
355+
(Default: <b>Off</b>)
354356
</em>
355357
</>
356358
}
359+
helpText="Features that run scripts under-the-hood (e.g. software install, lock/wipe) will still be available."
357360
>
358-
Disable scripts
361+
Disable script execution features
359362
</Checkbox>
360363
<Checkbox
361364
onChange={onInputChange}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package tables
2+
3+
import (
4+
"database/sql"
5+
"fmt"
6+
)
7+
8+
func init() {
9+
MigrationClient.AddMigration(Up_20241224000000, Down_20241224000000)
10+
}
11+
12+
func Up_20241224000000(tx *sql.Tx) error {
13+
// alter the host_script_results table to add an is_internal flag (for scripts that still execute
14+
// when scripts are disabled globally)
15+
_, err := tx.Exec(`ALTER TABLE host_script_results ADD COLUMN is_internal BOOLEAN DEFAULT FALSE`)
16+
if err != nil {
17+
return fmt.Errorf("add is_internal to host_script_results: %w", err)
18+
}
19+
20+
return nil
21+
}
22+
23+
func Down_20241224000000(tx *sql.Tx) error {
24+
return nil
25+
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package tables
2+
3+
import (
4+
"testing"
5+
6+
"github.com/google/uuid"
7+
"github.com/stretchr/testify/require"
8+
)
9+
10+
func TestUp_20241224000000(t *testing.T) {
11+
db := applyUpToPrev(t)
12+
13+
const (
14+
insertScriptResultStmt = `INSERT INTO host_script_results (
15+
host_id, execution_id, script_content_id, script_id, output
16+
) VALUES (?, ?, ?, ?, '')`
17+
18+
insertScriptStmt = `INSERT INTO scripts (
19+
team_id, global_or_team_id, name, script_content_id
20+
) VALUES (?, ?, ?, ?)`
21+
22+
loadResultStmt = `SELECT
23+
id, host_id, execution_id, script_id, is_internal
24+
FROM host_script_results WHERE id = ?`
25+
)
26+
27+
type script struct {
28+
id, globalOrTeamID int64
29+
name, scriptContents string
30+
teamID *int64
31+
}
32+
type scriptResult struct {
33+
id, hostID int64
34+
executionID string
35+
scriptID *int64
36+
isInternal bool
37+
}
38+
39+
// create a global script
40+
globalScript := script{
41+
globalOrTeamID: 0,
42+
teamID: nil,
43+
name: "global-script",
44+
scriptContents: "b",
45+
}
46+
47+
scriptContentsID := execNoErrLastID(t, db, "INSERT INTO script_contents(contents, md5_checksum) VALUES (?, 'a')", globalScript.scriptContents)
48+
49+
res, err := db.Exec(insertScriptStmt, globalScript.teamID, globalScript.globalOrTeamID, globalScript.name, scriptContentsID)
50+
require.NoError(t, err)
51+
globalScript.id, _ = res.LastInsertId()
52+
53+
// create a host script result for that global script
54+
globalScriptResult := scriptResult{
55+
hostID: 123,
56+
executionID: uuid.New().String(),
57+
scriptID: &globalScript.id,
58+
}
59+
res, err = db.Exec(insertScriptResultStmt, globalScriptResult.hostID, globalScriptResult.executionID, scriptContentsID, globalScriptResult.scriptID)
60+
require.NoError(t, err)
61+
globalScriptResult.id, _ = res.LastInsertId()
62+
63+
// Apply current migration.
64+
applyNext(t, db)
65+
66+
// the global host script result should not be set as internal
67+
var result scriptResult
68+
err = db.QueryRow(loadResultStmt, globalScriptResult.id).Scan(&result.id, &result.hostID, &result.executionID, &result.scriptID, &result.isInternal)
69+
require.NoError(t, err)
70+
require.False(t, result.isInternal)
71+
require.Equal(t, globalScriptResult, result)
72+
}

0 commit comments

Comments
 (0)