mirror of
https://github.com/fleetdm/fleet
synced 2026-04-21 13:37:30 +00:00
Fix patch policy bugs (#43420)
<!-- Add the related story/sub-task/bug number, like Resolves #123, or remove if NA --> **Related issue:** Resolves #43389 1. Added verifyPatchPolicy check 2. Fixed nil pointer dereference when calling spec/policies with no fleet_maintained_app_slug key provided 3. Fixed bug where renaming a patch policy in a gitops file caused it to be deleted on the first run, and only added when gitops is run again. # Checklist for submitter If some of the following don't apply, delete the relevant line. - [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/guides/committing-changes.md#changes-files) for more information. ## Testing - [x] Added/updated automated tests - [ ] Where appropriate, [automated tests simulate multiple hosts and test for host isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing) (updates to one hosts's records do not affect another) - [x] QA'd all new/changed functionality manually <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Renaming a patch policy via GitOps now updates the existing policy instead of deleting it. * Fixed nil-pointer errors in policy API operations. * Reject applying patch policies with missing, invalid, or disallowed Fleet Maintained App references (including global/enterprise slugs). * Improved matching for patch policies to avoid unintended deletions when names differ. * Patch policies now preserve intended platform/target behavior during apply/update. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
parent
c9e66b221e
commit
ebd2cb0012
6 changed files with 163 additions and 1 deletions
2
changes/43389-patch-policy-gitops-bugs
Normal file
2
changes/43389-patch-policy-gitops-bugs
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
- Fixed bug where renaming a patch policy in a GitOps file caused it to be deleted initially.
|
||||
- Fixed a nil pointer dereference in the contributor api spec/policies.
|
||||
|
|
@ -1478,6 +1478,11 @@ func (ds *Datastore) ApplyPolicySpecs(ctx context.Context, authorID uint, specs
|
|||
|
||||
// generate new up-to-date patch policy
|
||||
if spec.Type == fleet.PolicyTypePatch {
|
||||
if fmaTitleID == nil {
|
||||
return ctxerr.Wrap(ctx, &fleet.BadRequestError{
|
||||
Message: fmt.Sprintf("fleet_maintained_app_slug must be set for patch policy: %s", spec.Name),
|
||||
})
|
||||
}
|
||||
installer, err := ds.getPatchPolicyInstaller(ctx, ptr.ValOrZero(teamID), *fmaTitleID)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "getting patch policy installer")
|
||||
|
|
@ -1517,6 +1522,7 @@ func (ds *Datastore) ApplyPolicySpecs(ctx context.Context, authorID uint, specs
|
|||
var (
|
||||
shouldRemoveAllPolicyMemberships bool
|
||||
removePolicyStats bool
|
||||
shouldUpdatePatchPolicyName bool
|
||||
)
|
||||
if insertOnDuplicateDidInsertOrUpdate(res) {
|
||||
// Figure out if the query, platform, software installer, VPP app, or script changed.
|
||||
|
|
@ -1548,7 +1554,16 @@ func (ds *Datastore) ApplyPolicySpecs(ctx context.Context, authorID uint, specs
|
|||
if teamID == nil {
|
||||
err = sqlx.GetContext(ctx, tx, &lastID, "SELECT id FROM policies WHERE name = ? AND team_id is NULL", spec.Name)
|
||||
} else {
|
||||
err = sqlx.GetContext(ctx, tx, &lastID, "SELECT id FROM policies WHERE name = ? AND team_id = ?", spec.Name, teamID)
|
||||
// Patch policies are unique by patch_software_title_id so we need to get them by that, and update their name
|
||||
// so that it doesn't get deleted later.
|
||||
if spec.Type == fleet.PolicyTypePatch {
|
||||
err = sqlx.GetContext(ctx, tx, &lastID, "SELECT id FROM policies WHERE patch_software_title_id = ? AND team_id = ?", fmaTitleID, teamID)
|
||||
if _, ok := teamIDToPoliciesByName[teamID][spec.Name]; !ok {
|
||||
shouldUpdatePatchPolicyName = true
|
||||
}
|
||||
} else {
|
||||
err = sqlx.GetContext(ctx, tx, &lastID, "SELECT id FROM policies WHERE name = ? AND team_id = ?", spec.Name, teamID)
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "select policies id")
|
||||
|
|
@ -1595,6 +1610,11 @@ func (ds *Datastore) ApplyPolicySpecs(ctx context.Context, authorID uint, specs
|
|||
return ctxerr.Wrap(ctx, err, "setting needs_full_membership_cleanup flag")
|
||||
}
|
||||
}
|
||||
if shouldUpdatePatchPolicyName {
|
||||
if _, err := tx.ExecContext(ctx, `UPDATE policies SET name = ?, checksum = `+policiesChecksumComputedColumn()+` WHERE id = ?`, spec.Name, policyID); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "setting name for patch policy")
|
||||
}
|
||||
}
|
||||
// Defer cleanup outside the transaction to avoid long-held row locks on
|
||||
// policy_membership.
|
||||
pendingCleanups = append(pendingCleanups, policyCleanupArgs{
|
||||
|
|
|
|||
|
|
@ -7720,6 +7720,108 @@ func testTeamPatchPolicy(t *testing.T, ds *Datastore) {
|
|||
require.Equal(t, "Windows - Maintained2 up to date", p5.Name)
|
||||
require.Equal(t, "windows", p5.Platform)
|
||||
require.Equal(t, "SELECT 1 WHERE NOT EXISTS (SELECT 1 FROM programs WHERE name = 'Maintained2' AND version_compare(version, '1.0') < 0);", p5.Query)
|
||||
|
||||
//////////////////////////////////////////////
|
||||
// ApplyPolicySpecs
|
||||
|
||||
// Test ApplyPolicySpecs with patch policies using a separate team.
|
||||
team2, err := ds.NewTeam(ctx, &fleet.Team{Name: "team2"})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Set up an FMA installer on team2 for the valid slug test.
|
||||
team2Payload := &fleet.UploadSoftwareInstallerPayload{
|
||||
InstallScript: "hello",
|
||||
PreInstallQuery: "SELECT 1",
|
||||
PostInstallScript: "world",
|
||||
StorageID: "storage-team2",
|
||||
Filename: "maintained1-team2",
|
||||
Title: "Maintained1",
|
||||
Version: "1.0",
|
||||
Source: "apps",
|
||||
Platform: "darwin",
|
||||
BundleIdentifier: "fleet.maintained1",
|
||||
UserID: user1.ID,
|
||||
TeamID: &team2.ID,
|
||||
ValidatedLabels: &fleet.LabelIdentsWithScope{},
|
||||
FleetMaintainedAppID: &maintainedApp.ID,
|
||||
}
|
||||
_, _, err = ds.MatchOrCreateSoftwareInstaller(ctx, team2Payload)
|
||||
require.NoError(t, err)
|
||||
|
||||
// ApplyPolicySpecs with type=patch and empty fleet_maintained_app_slug should return an error.
|
||||
err = ds.ApplyPolicySpecs(ctx, user1.ID, []*fleet.PolicySpec{
|
||||
{
|
||||
Name: "patch-no-slug",
|
||||
Query: "SELECT 1;",
|
||||
Team: "team2",
|
||||
Type: fleet.PolicyTypePatch,
|
||||
},
|
||||
})
|
||||
require.Error(t, err)
|
||||
|
||||
// ApplyPolicySpecs with type=patch and a non-existent fleet_maintained_app_slug should return an error.
|
||||
err = ds.ApplyPolicySpecs(ctx, user1.ID, []*fleet.PolicySpec{
|
||||
{
|
||||
Name: "patch-bad-slug",
|
||||
Query: "SELECT 1;",
|
||||
Team: "team2",
|
||||
Type: fleet.PolicyTypePatch,
|
||||
FleetMaintainedAppSlug: "nonexistent-slug",
|
||||
},
|
||||
})
|
||||
require.Error(t, err)
|
||||
|
||||
// ApplyPolicySpecs with type=patch and a valid fleet_maintained_app_slug should succeed.
|
||||
err = ds.ApplyPolicySpecs(ctx, user1.ID, []*fleet.PolicySpec{
|
||||
{
|
||||
Name: "patch-valid-slug",
|
||||
Query: "SELECT 1;",
|
||||
Team: "team2",
|
||||
Type: fleet.PolicyTypePatch,
|
||||
FleetMaintainedAppSlug: "maintained1",
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify the policy was created with the expected auto-generated query and platform.
|
||||
policies, _, err := ds.ListTeamPolicies(ctx, team2.ID, fleet.ListOptions{}, fleet.ListOptions{}, "")
|
||||
require.NoError(t, err)
|
||||
require.Len(t, policies, 1)
|
||||
require.Equal(t, "patch-valid-slug", policies[0].Name)
|
||||
require.Equal(t, fleet.PolicyTypePatch, policies[0].Type)
|
||||
require.Equal(t, "darwin", policies[0].Platform)
|
||||
require.Contains(t, policies[0].Query, "fleet.maintained1")
|
||||
|
||||
// Renaming a patch policy via ApplyPolicySpecs should update it, not delete it.
|
||||
previousID := policies[0].ID
|
||||
var previousChecksum []byte
|
||||
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
|
||||
return sqlx.GetContext(ctx, q, &previousChecksum, `SELECT checksum FROM policies WHERE id = ?`, previousID)
|
||||
})
|
||||
|
||||
err = ds.ApplyPolicySpecs(ctx, user1.ID, []*fleet.PolicySpec{
|
||||
{
|
||||
Name: "patch-renamed",
|
||||
Query: "SELECT 1;",
|
||||
Team: "team2",
|
||||
Type: fleet.PolicyTypePatch,
|
||||
FleetMaintainedAppSlug: "maintained1",
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
policies, _, err = ds.ListTeamPolicies(ctx, team2.ID, fleet.ListOptions{}, fleet.ListOptions{}, "")
|
||||
require.NoError(t, err)
|
||||
require.Len(t, policies, 1)
|
||||
require.Equal(t, previousID, policies[0].ID)
|
||||
require.Equal(t, "patch-renamed", policies[0].Name)
|
||||
require.Equal(t, fleet.PolicyTypePatch, policies[0].Type)
|
||||
|
||||
var newChecksum []byte
|
||||
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
|
||||
return sqlx.GetContext(ctx, q, &newChecksum, `SELECT checksum FROM policies WHERE id = ?`, previousID)
|
||||
})
|
||||
require.NotEqual(t, previousChecksum, newChecksum)
|
||||
}
|
||||
|
||||
func testTeamPolicyAutomationFilter(t *testing.T, ds *Datastore) {
|
||||
|
|
|
|||
|
|
@ -117,6 +117,7 @@ var (
|
|||
errPolicyPatchAndQuerySet = errors.New("If the \"type\" is \"patch\", the \"query\" field is not supported.")
|
||||
errPolicyPatchAndPlatformSet = errors.New("If the \"type\" is \"patch\", the \"platform\" field is not supported.")
|
||||
errPolicyPatchNoTitleID = errors.New("If the \"type\" is \"patch\", the \"patch_software_title_id\" field is required.")
|
||||
errPatchPolicyRequiresTeam = errors.New("If the \"type\" is \"patch\", the \"team\" field is required.")
|
||||
errPolicyQueryUpdated = errors.New("\"query\" can't be updated")
|
||||
errPolicyPlatformUpdated = errors.New("\"platform\" can't be updated")
|
||||
errPolicyConditionalAccessEnabledInvalidPlatform = errors.New("\"conditional_access_enabled\" is only valid on \"darwin\" and \"windows\" policies")
|
||||
|
|
@ -207,6 +208,13 @@ func verifyPolicyPlatforms(platforms string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func verifyPatchPolicy(team string, typ string) error {
|
||||
if typ == PolicyTypePatch && emptyString(team) {
|
||||
return errPatchPolicyRequiresTeam
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func PolicyVerifyConditionalAccess(conditionalAccessEnabled bool, platform string) error {
|
||||
if conditionalAccessEnabled && !strings.Contains(platform, "darwin") && !strings.Contains(platform, "windows") {
|
||||
return errPolicyConditionalAccessEnabledInvalidPlatform
|
||||
|
|
@ -473,6 +481,7 @@ type PolicySpec struct {
|
|||
|
||||
Type string `json:"type"`
|
||||
FleetMaintainedAppSlug string `json:"fleet_maintained_app_slug"`
|
||||
PatchSoftwareTitleID uint `json:"-"`
|
||||
}
|
||||
|
||||
// PolicySoftwareTitle contains software title data for policies.
|
||||
|
|
@ -507,6 +516,9 @@ func (p PolicySpec) Verify() error {
|
|||
if err := PolicyVerifyConditionalAccess(p.ConditionalAccessEnabled, p.Platform); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := verifyPatchPolicy(p.Team, p.Type); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3000,6 +3000,11 @@ func (c *Client) doGitOpsPolicies(config *spec.GitOps, teamSoftwareInstallers []
|
|||
}
|
||||
config.Policies[i].ScriptID = &scriptID
|
||||
}
|
||||
|
||||
// Get patch policy title IDs for the team
|
||||
for i := range config.Policies {
|
||||
config.Policies[i].PatchSoftwareTitleID = softwareTitleIDsBySlug[config.Policies[i].FleetMaintainedAppSlug]
|
||||
}
|
||||
}
|
||||
|
||||
// Get the ids and names of current policies to figure out which ones to delete
|
||||
|
|
@ -3037,6 +3042,7 @@ func (c *Client) doGitOpsPolicies(config *spec.GitOps, teamSoftwareInstallers []
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
var policiesToDelete []uint
|
||||
for _, oldItem := range policies {
|
||||
found := false
|
||||
|
|
@ -3045,6 +3051,13 @@ func (c *Client) doGitOpsPolicies(config *spec.GitOps, teamSoftwareInstallers []
|
|||
found = true
|
||||
break
|
||||
}
|
||||
// patch policies are unique by patch_software_title_id so matching by name doesn't always work
|
||||
if newItem.Type == fleet.PolicyTypePatch && oldItem.PatchSoftware != nil {
|
||||
if oldItem.PatchSoftware.SoftwareTitleID == newItem.PatchSoftwareTitleID {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
policiesToDelete = append(policiesToDelete, oldItem.ID)
|
||||
|
|
|
|||
|
|
@ -27438,6 +27438,19 @@ func (s *integrationEnterpriseTestSuite) TestPatchPolicies() {
|
|||
require.Equal(t, "dynamic", getPolicyResp.Policy.Type)
|
||||
require.Empty(t, getPolicyResp.Policy.PatchSoftware)
|
||||
require.Empty(t, getPolicyResp.Policy.PatchSoftwareTitleID)
|
||||
|
||||
// Should not be able to apply global patch policy
|
||||
spec := &fleet.PolicySpec{
|
||||
Name: "team patch policy",
|
||||
Query: "SELECT 1",
|
||||
Type: fleet.PolicyTypePatch,
|
||||
FleetMaintainedAppSlug: "zoom/windows",
|
||||
}
|
||||
applyResp := fleet.ApplyPolicySpecsResponse{}
|
||||
s.DoJSON("POST", "/api/latest/fleet/spec/policies",
|
||||
fleet.ApplyPolicySpecsRequest{Specs: []*fleet.PolicySpec{spec}},
|
||||
http.StatusBadRequest, &applyResp,
|
||||
)
|
||||
})
|
||||
|
||||
t.Run("patch_policy_lifecycle", func(t *testing.T) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue