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:
@@ -0,0 +1 @@
|
||||
- Adds functionality for skipping automatic installs if the software is not scoped to the host via labels.
|
||||
@@ -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 == "" {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user