feat: skip automatic install policy if installer is not scoped to host (#24843)

> Related issue: #24533

- We're still running the policy, but in the handler for the results we
check if the software is in label scope. If not, we set the policy to be
"undetermined" and we do not add an installation request
- Added checks for label scoping to the "install software" and "self
service install" endpoints

# 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] Manual QA for all new/changed functionality
This commit is contained in:
Jahziel Villasana-Espinoza
2024-12-18 10:58:28 -05:00
committed by GitHub
parent 8043ef355c
commit fe8324b48d
8 changed files with 197 additions and 24 deletions
+1
View File
@@ -0,0 +1 @@
- Adds functionality for skipping automatic installs if the software is not scoped to the host via labels.
+23
View File
@@ -765,6 +765,18 @@ func (svc *Service) InstallSoftwareTitle(ctx context.Context, hostID uint, softw
// if we found an installer, use that
if installer != nil {
// check the label scoping for this installer and host
scoped, err := svc.ds.IsSoftwareInstallerLabelScoped(ctx, installer.InstallerID, hostID)
if err != nil {
return ctxerr.Wrap(ctx, err, "checking label scoping during software install attempt")
}
if !scoped {
return &fleet.BadRequestError{
Message: "Couldn't install. Host isn't member of the labels defined for this software title.",
}
}
lastInstallRequest, err := svc.ds.GetHostLastInstallData(ctx, host.ID, installer.InstallerID)
if err != nil {
return ctxerr.Wrapf(ctx, err, "getting last install data for host %d and installer %d", host.ID, installer.InstallerID)
@@ -1593,6 +1605,17 @@ func (svc *Service) SelfServiceInstallSoftwareTitle(ctx context.Context, host *f
}
}
scoped, err := svc.ds.IsSoftwareInstallerLabelScoped(ctx, installer.InstallerID, host.ID)
if err != nil {
return ctxerr.Wrap(ctx, err, "checking label scoping during software install attempt")
}
if !scoped {
return &fleet.BadRequestError{
Message: "Couldn't install. Host isn't member of the labels defined for this software title.",
}
}
ext := filepath.Ext(installer.Name)
requiredPlatform := packageExtensionToPlatform(ext)
if requiredPlatform == "" {
+11 -6
View File
@@ -15,7 +15,7 @@ import (
func TestPreProcessUninstallScript(t *testing.T) {
t.Parallel()
var input = `
input := `
blah$PACKAGE_IDS
pkgids=$PACKAGE_ID
they are $PACKAGE_ID, right $MY_SECRET?
@@ -74,7 +74,6 @@ quotes and braces for (
"com.bar"
)`
assert.Equal(t, expected, payload.UninstallScript)
}
func TestInstallUninstallAuth(t *testing.T) {
@@ -93,7 +92,8 @@ func TestInstallUninstallAuth(t *testing.T) {
}, nil
}
ds.GetSoftwareInstallerMetadataByTeamAndTitleIDFunc = func(ctx context.Context, teamID *uint, titleID uint,
withScriptContents bool) (*fleet.SoftwareInstaller, error) {
withScriptContents bool,
) (*fleet.SoftwareInstaller, error) {
return &fleet.SoftwareInstaller{
Name: "installer.pkg",
Platform: "darwin",
@@ -104,14 +104,16 @@ func TestInstallUninstallAuth(t *testing.T) {
return nil, nil
}
ds.InsertSoftwareInstallRequestFunc = func(ctx context.Context, hostID uint, softwareInstallerID uint, selfService bool, policyID *uint) (string,
error) {
error,
) {
return "request_id", nil
}
ds.GetAnyScriptContentsFunc = func(ctx context.Context, id uint) ([]byte, error) {
return []byte("script"), nil
}
ds.NewHostScriptExecutionRequestFunc = func(ctx context.Context, request *fleet.HostScriptRequestPayload) (*fleet.HostScriptResult,
error) {
error,
) {
return &fleet.HostScriptResult{
ExecutionID: "execution_id",
}, nil
@@ -120,6 +122,10 @@ func TestInstallUninstallAuth(t *testing.T) {
return nil
}
ds.IsSoftwareInstallerLabelScopedFunc = func(ctx context.Context, installerID, hostID uint) (bool, error) {
return true, nil
}
testCases := []struct {
name string
user *fleet.User
@@ -197,7 +203,6 @@ func TestUninstallSoftwareTitle(t *testing.T) {
// Host scripts disabled
host.ScriptsEnabled = ptr.Bool(false)
require.ErrorContains(t, svc.UninstallSoftwareTitle(context.Background(), 1, 10), fleet.RunScriptsOrbitDisabledErrMsg)
}
func checkAuthErr(t *testing.T, shouldFail bool, err error) {
@@ -1605,3 +1605,67 @@ WHERE global_or_team_id = ?
}
return softwarePackages, nil
}
func (ds *Datastore) IsSoftwareInstallerLabelScoped(ctx context.Context, installerID, hostID uint) (bool, error) {
stmt := `
SELECT 1 FROM (
-- no labels
SELECT 0 AS count_installer_labels, 0 AS count_host_labels
WHERE NOT EXISTS (
SELECT 1 FROM software_installer_labels sil WHERE sil.software_installer_id = :installer_id
)
UNION
-- include any
SELECT
COUNT(*) AS count_installer_labels,
COUNT(lm.label_id) AS count_host_labels
FROM
software_installer_labels sil
LEFT OUTER JOIN label_membership lm ON lm.label_id = sil.label_id
AND lm.host_id = :host_id
WHERE
sil.software_installer_id = :installer_id
AND sil.exclude = 0
HAVING
count_installer_labels > 0 AND count_host_labels > 0
UNION
-- exclude any
SELECT
COUNT(*) AS count_installer_labels,
COUNT(lm.label_id) AS count_host_labels
FROM
software_installer_labels sil
LEFT OUTER JOIN label_membership lm ON lm.label_id = sil.label_id
AND lm.host_id = :host_id
WHERE
sil.software_installer_id = :installer_id
AND sil.exclude = 1
HAVING
count_installer_labels > 0 AND count_host_labels = 0
) t
`
namedArgs := map[string]any{
"host_id": hostID,
"installer_id": installerID,
}
stmt, args, err := sqlx.Named(stmt, namedArgs)
if err != nil {
return false, ctxerr.Wrap(ctx, err, "build named query for is software installer label scoped")
}
var res bool
if err := sqlx.GetContext(ctx, ds.reader(ctx), &res, stmt, args...); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return false, nil
}
return false, ctxerr.Wrap(ctx, err, "is software installer label scoped")
}
return res, nil
}
+69 -18
View File
@@ -5258,7 +5258,7 @@ func testListHostSoftwareWithLabelScoping(t *testing.T, ds *Datastore) {
nanoEnroll(t, ds, host, false)
user1 := test.NewUser(t, ds, "Alice", "alice@example.com", true)
// create some software: custom installers and FMA
// create a software installer
tfr1, err := fleet.NewTempFileReader(strings.NewReader("hello"), t.TempDir)
require.NoError(t, err)
installerID1, _, err := ds.MatchOrCreateSoftwareInstaller(ctx, &fleet.UploadSoftwareInstallerPayload{
@@ -5294,6 +5294,11 @@ func testListHostSoftwareWithLabelScoping(t *testing.T, ds *Datastore) {
require.Len(t, software, 1)
require.Equal(t, "file1", software[0].SoftwarePackage.Name)
// installer1 should be in scope since it has no labels
scoped, err := ds.IsSoftwareInstallerLabelScoped(ctx, installerID1, host.ID)
require.NoError(t, err)
require.True(t, scoped)
label1, err := ds.NewLabel(ctx, &fleet.Label{Name: "label1" + t.Name()})
require.NoError(t, err)
@@ -5301,32 +5306,39 @@ func testListHostSoftwareWithLabelScoping(t *testing.T, ds *Datastore) {
require.NoError(t, ds.AddLabelsToHost(ctx, host.ID, []uint{label1.ID}))
// assign the label to the software installer
// TODO(JVE): update this once the DS method exists
updateInstallerLabel := func(siID, labelID uint, exclude bool) {
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
_, err = q.ExecContext(
ctx,
`INSERT INTO software_installer_labels (software_installer_id, label_id, exclude) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE exclude = VALUES(exclude)`,
siID, labelID, exclude,
)
return err
})
}
updateInstallerLabel(installerID1, label1.ID, true)
err = setOrUpdateSoftwareInstallerLabelsDB(ctx, ds.writer(ctx), installerID1, fleet.LabelIdentsWithScope{
LabelScope: fleet.LabelScopeExcludeAny,
ByName: map[string]fleet.LabelIdent{label1.Name: {LabelName: label1.Name, LabelID: label1.ID}},
})
require.NoError(t, err)
// should be empty as the installer label is "exclude any"
software, _, err = ds.ListHostSoftware(ctx, host, opts)
require.NoError(t, err)
require.Empty(t, software)
// installer1 should be out of scope since the label is "exclude any"
scoped, err = ds.IsSoftwareInstallerLabelScoped(ctx, installerID1, host.ID)
require.NoError(t, err)
require.False(t, scoped)
// Update the label to be "include any"
updateInstallerLabel(installerID1, label1.ID, false)
err = setOrUpdateSoftwareInstallerLabelsDB(ctx, ds.writer(ctx), installerID1, fleet.LabelIdentsWithScope{
LabelScope: fleet.LabelScopeIncludeAny,
ByName: map[string]fleet.LabelIdent{label1.Name: {LabelName: label1.Name, LabelID: label1.ID}},
})
require.NoError(t, err)
software, _, err = ds.ListHostSoftware(ctx, host, opts)
require.NoError(t, err)
require.Len(t, software, 1)
require.Equal(t, "file1", software[0].SoftwarePackage.Name)
// Now installer1 is in scope again: label is "include any"
scoped, err = ds.IsSoftwareInstallerLabelScoped(ctx, installerID1, host.ID)
require.NoError(t, err)
require.True(t, scoped)
// Add an installer. No label yet.
installerID2, _, err := ds.MatchOrCreateSoftwareInstaller(ctx, &fleet.UploadSoftwareInstallerPayload{
InstallScript: "hello",
@@ -5358,8 +5370,14 @@ func testListHostSoftwareWithLabelScoping(t *testing.T, ds *Datastore) {
label3, err := ds.NewLabel(ctx, &fleet.Label{Name: "label3" + t.Name()})
require.NoError(t, err)
updateInstallerLabel(installerID2, label2.ID, true)
updateInstallerLabel(installerID2, label3.ID, true)
err = setOrUpdateSoftwareInstallerLabelsDB(ctx, ds.writer(ctx), installerID2, fleet.LabelIdentsWithScope{
LabelScope: fleet.LabelScopeExcludeAny,
ByName: map[string]fleet.LabelIdent{
label2.Name: {LabelName: label2.Name, LabelID: label2.ID},
label3.Name: {LabelName: label3.Name, LabelID: label3.ID},
},
})
require.NoError(t, err)
// Now host has label1, label2
require.NoError(t, ds.AddLabelsToHost(ctx, host.ID, []uint{label2.ID}))
@@ -5369,6 +5387,16 @@ func testListHostSoftwareWithLabelScoping(t *testing.T, ds *Datastore) {
require.NoError(t, err)
require.Len(t, software, 1)
// installer1 is still in scope
scoped, err = ds.IsSoftwareInstallerLabelScoped(ctx, installerID1, host.ID)
require.NoError(t, err)
require.True(t, scoped)
// installer2 is out of scope, because host has label2
scoped, err = ds.IsSoftwareInstallerLabelScoped(ctx, installerID2, host.ID)
require.NoError(t, err)
require.False(t, scoped)
// Add an installer. No label yet.
installerID3, _, err := ds.MatchOrCreateSoftwareInstaller(ctx, &fleet.UploadSoftwareInstallerPayload{
InstallScript: "hello",
@@ -5392,18 +5420,41 @@ func testListHostSoftwareWithLabelScoping(t *testing.T, ds *Datastore) {
label4, err := ds.NewLabel(ctx, &fleet.Label{Name: "label4" + t.Name()})
require.NoError(t, err)
updateInstallerLabel(installerID3, label4.ID, true)
err = setOrUpdateSoftwareInstallerLabelsDB(ctx, ds.writer(ctx), installerID3, fleet.LabelIdentsWithScope{
LabelScope: fleet.LabelScopeExcludeAny,
ByName: map[string]fleet.LabelIdent{label4.Name: {LabelName: label4.Name, LabelID: label4.ID}},
})
require.NoError(t, err)
// We should have [installerID1, installerID3]
software, _, err = ds.ListHostSoftware(ctx, host, opts)
require.NoError(t, err)
require.Len(t, software, 2)
// installer1 is still in scope
scoped, err = ds.IsSoftwareInstallerLabelScoped(ctx, installerID1, host.ID)
require.NoError(t, err)
require.True(t, scoped)
// installer3 is in scope, because label is "exclude any" and host doesn't have the label
scoped, err = ds.IsSoftwareInstallerLabelScoped(ctx, installerID2, host.ID)
require.NoError(t, err)
require.False(t, scoped)
// Now include hosts with label4. No host has this label, so we shouldn't see installerID3 anymore.
updateInstallerLabel(installerID3, label4.ID, false)
err = setOrUpdateSoftwareInstallerLabelsDB(ctx, ds.writer(ctx), installerID3, fleet.LabelIdentsWithScope{
LabelScope: fleet.LabelScopeIncludeAny,
ByName: map[string]fleet.LabelIdent{label4.Name: {LabelName: label4.Name, LabelID: label4.ID}},
})
require.NoError(t, err)
// We should have [installerID1]
software, _, err = ds.ListHostSoftware(ctx, host, opts)
require.NoError(t, err)
require.Len(t, software, 1)
// installer1 is still in scope
scoped, err = ds.IsSoftwareInstallerLabelScoped(ctx, installerID1, host.ID)
require.NoError(t, err)
require.True(t, scoped)
}
+4
View File
@@ -609,6 +609,10 @@ type Datastore interface {
ListHostSoftware(ctx context.Context, host *Host, opts HostSoftwareTitleListOptions) ([]*HostSoftwareWithInstaller, *PaginationMetadata, error)
// IsSoftwareInstallerLabelScoped returns whether or not the given installerID is scoped to the
// given host ID by labels.
IsSoftwareInstallerLabelScoped(ctx context.Context, installerID, hostID uint) (bool, error)
// SetHostSoftwareInstallResult records the result of a software installation
// attempt on the host.
SetHostSoftwareInstallResult(ctx context.Context, result *HostSoftwareInstallResultPayload) error
+12
View File
@@ -451,6 +451,8 @@ type ListCVEsFunc func(ctx context.Context, maxAge time.Duration) ([]fleet.CVEMe
type ListHostSoftwareFunc func(ctx context.Context, host *fleet.Host, opts fleet.HostSoftwareTitleListOptions) ([]*fleet.HostSoftwareWithInstaller, *fleet.PaginationMetadata, error)
type IsSoftwareInstallerLabelScopedFunc func(ctx context.Context, installerID uint, hostID uint) (bool, error)
type SetHostSoftwareInstallResultFunc func(ctx context.Context, result *fleet.HostSoftwareInstallResultPayload) error
type UploadedSoftwareExistsFunc func(ctx context.Context, bundleIdentifier string, teamID *uint) (bool, error)
@@ -1823,6 +1825,9 @@ type DataStore struct {
ListHostSoftwareFunc ListHostSoftwareFunc
ListHostSoftwareFuncInvoked bool
IsSoftwareInstallerLabelScopedFunc IsSoftwareInstallerLabelScopedFunc
IsSoftwareInstallerLabelScopedFuncInvoked bool
SetHostSoftwareInstallResultFunc SetHostSoftwareInstallResultFunc
SetHostSoftwareInstallResultFuncInvoked bool
@@ -4420,6 +4425,13 @@ func (s *DataStore) ListHostSoftware(ctx context.Context, host *fleet.Host, opts
return s.ListHostSoftwareFunc(ctx, host, opts)
}
func (s *DataStore) IsSoftwareInstallerLabelScoped(ctx context.Context, installerID uint, hostID uint) (bool, error) {
s.mu.Lock()
s.IsSoftwareInstallerLabelScopedFuncInvoked = true
s.mu.Unlock()
return s.IsSoftwareInstallerLabelScopedFunc(ctx, installerID, hostID)
}
func (s *DataStore) SetHostSoftwareInstallResult(ctx context.Context, result *fleet.HostSoftwareInstallResultPayload) error {
s.mu.Lock()
s.SetHostSoftwareInstallResultFuncInvoked = true
+13
View File
@@ -1010,6 +1010,8 @@ func (svc *Service) SubmitDistributedQueryResults(
logging.WithErr(ctx, err)
}
// NOTE: if the installers for the policies here are not scoped to the host via labels, we update the policy status here to stop it from showing up as "failed" in the
// host details.
if err := svc.processSoftwareForNewlyFailingPolicies(ctx, host.ID, host.TeamID, host.Platform, host.OrbitNodeKey, policyResults); err != nil {
logging.WithErr(ctx, err)
}
@@ -1795,6 +1797,17 @@ func (svc *Service) processSoftwareForNewlyFailingPolicies(
level.Debug(logger).Log("msg", "installer platform does not match host platform")
continue
}
scoped, err := svc.ds.IsSoftwareInstallerLabelScoped(ctx, failingPolicyWithInstaller.InstallerID, hostID)
if err != nil {
return ctxerr.Wrap(ctx, err, "checking if software installer is label scoped to host")
}
if !scoped {
// NOTE: we update the policy status here to stop it from showing up as "failed" in the
// host details.
incomingPolicyResults[failingPolicyWithInstaller.ID] = nil
level.Debug(logger).Log("msg", "not marking policy as failed since software is out of scope for host")
continue
}
hostLastInstall, err := svc.ds.GetHostLastInstallData(ctx, hostID, installerMetadata.InstallerID)
if err != nil {
return ctxerr.Wrap(ctx, err, "get host last install data")