From 5dc7f00fcdcfc928bf68eca59aa211f1079114b3 Mon Sep 17 00:00:00 2001 From: Lucas Manuel Rodriguez Date: Tue, 17 Mar 2026 16:30:38 -0300 Subject: [PATCH] Add `patch` policies to software filtered policies (#41839) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bug found while developing a policy related feature. Screenshot 2026-03-17 at 12 30
55 PM --- server/datastore/mysql/policies.go | 2 +- server/datastore/mysql/policies_test.go | 40 ++++++++- server/fleet/policies.go | 2 +- server/service/integration_enterprise_test.go | 86 +++++++++++++++++++ 4 files changed, 125 insertions(+), 5 deletions(-) diff --git a/server/datastore/mysql/policies.go b/server/datastore/mysql/policies.go index 0909205f62..7d5817fa08 100644 --- a/server/datastore/mysql/policies.go +++ b/server/datastore/mysql/policies.go @@ -2685,7 +2685,7 @@ func (ds *Datastore) createAutomationClause(ctx context.Context, automationFilte switch automationFilter { case "software": - return " AND (p.software_installer_id IS NOT NULL OR p.vpp_apps_teams_id IS NOT NULL)", nil, nil + return " AND (p.software_installer_id IS NOT NULL OR p.vpp_apps_teams_id IS NOT NULL OR p.type = 'patch')", nil, nil case "scripts": return " AND p.script_id IS NOT NULL", nil, nil case "calendar": diff --git a/server/datastore/mysql/policies_test.go b/server/datastore/mysql/policies_test.go index 1462899571..12afb6167e 100644 --- a/server/datastore/mysql/policies_test.go +++ b/server/datastore/mysql/policies_test.go @@ -7662,6 +7662,29 @@ func testTeamPolicyAutomationFilter(t *testing.T, ds *Datastore) { }) require.NoError(t, err) + // Add an FMA. + fma, err := ds.UpsertMaintainedApp(ctx, &fleet.MaintainedApp{ID: 1}) + require.NoError(t, err) + + payload := &fleet.UploadSoftwareInstallerPayload{ + InstallScript: "hello", + PreInstallQuery: "SELECT 1", + PostInstallScript: "world", + StorageID: "storage1", + Filename: "maintained1", + Title: "Maintained1", + Version: "1.0", + Source: "apps", + Platform: "darwin", + BundleIdentifier: "fleet.maintained1", + UserID: user1.ID, + TeamID: nil, + ValidatedLabels: &fleet.LabelIdentsWithScope{}, + FleetMaintainedAppID: &fma.ID, + } + _, titleID2, err := ds.MatchOrCreateSoftwareInstaller(context.Background(), payload) + require.NoError(t, err) + test.CreateInsertGlobalVPPToken(t, ds) // create team1 app @@ -7721,6 +7744,14 @@ func testTeamPolicyAutomationFilter(t *testing.T, ds *Datastore) { }) require.NoError(t, err) + teamPatchPolicy, err := ds.NewTeamPolicy(ctx, 0, nil, fleet.PolicyPayload{ + Name: "query 8", + Query: "SELECT 1;", + Type: fleet.PolicyTypePatch, + PatchSoftwareTitleID: &titleID2, + }) + require.NoError(t, err) + // TODO: test ticket integration policies? config := fleet.TeamConfig{} @@ -7735,7 +7766,7 @@ func testTeamPolicyAutomationFilter(t *testing.T, ds *Datastore) { }, "") require.NoError(t, err) - require.Len(t, merged, 7) + require.Len(t, merged, 8) assert.Equal(t, gpol.ID, merged[0].ID) assert.Equal(t, teamInstallerPolicy.ID, merged[1].ID) assert.Equal(t, teamAppStorePolicy.ID, merged[2].ID) @@ -7743,6 +7774,7 @@ func testTeamPolicyAutomationFilter(t *testing.T, ds *Datastore) { assert.Equal(t, teamCalendarPolicy.ID, merged[4].ID) assert.Equal(t, teamConditionalPolicy.ID, merged[5].ID) assert.Equal(t, teamWebhookPolicy.ID, merged[6].ID) + assert.Equal(t, teamPatchPolicy.ID, merged[7].ID) // Test filters merged, err = ds.ListMergedTeamPolicies(ctx, 0, fleet.ListOptions{ @@ -7750,9 +7782,10 @@ func testTeamPolicyAutomationFilter(t *testing.T, ds *Datastore) { OrderDirection: fleet.OrderAscending, }, "software") require.NoError(t, err) - require.Len(t, merged, 2) + require.Len(t, merged, 3) assert.Equal(t, teamInstallerPolicy.ID, merged[0].ID) assert.Equal(t, teamAppStorePolicy.ID, merged[1].ID) + assert.Equal(t, teamPatchPolicy.ID, merged[2].ID) merged, err = ds.ListMergedTeamPolicies(ctx, 0, fleet.ListOptions{ OrderKey: "name", @@ -7792,7 +7825,8 @@ func testTeamPolicyAutomationFilter(t *testing.T, ds *Datastore) { OrderDirection: fleet.OrderAscending, }, fleet.ListOptions{}, "software") require.NoError(t, err) - require.Len(t, policies, 2) + require.Len(t, policies, 3) assert.Equal(t, teamInstallerPolicy.ID, policies[0].ID) assert.Equal(t, teamAppStorePolicy.ID, policies[1].ID) + assert.Equal(t, teamPatchPolicy.ID, policies[2].ID) } diff --git a/server/fleet/policies.go b/server/fleet/policies.go index 9ebd9c3437..4adcabf7ed 100644 --- a/server/fleet/policies.go +++ b/server/fleet/policies.go @@ -59,7 +59,7 @@ type PolicyPayload struct { // Type is the policy type. It is 'dynamic' by default and 'patch' for patch policies. Type string - // PatchSoftwareTitleID is the title id of the Fleet maintained app chcked by a patch policy. + // PatchSoftwareTitleID is the title id of the Fleet maintained app checked by a patch policy. // // Only applies to team policies with the patch type. PatchSoftwareTitleID *uint diff --git a/server/service/integration_enterprise_test.go b/server/service/integration_enterprise_test.go index 7638e5b305..818a08506d 100644 --- a/server/service/integration_enterprise_test.go +++ b/server/service/integration_enterprise_test.go @@ -1343,6 +1343,92 @@ func (s *integrationEnterpriseTestSuite) TestTeamPolicies() { require.Len(t, ts.Policies, 0) } +func (s *integrationEnterpriseTestSuite) TestListTeamPoliciesAutomationTypeSoftware() { + t := s.T() + ctx := context.Background() + + // Create a team + team, err := s.ds.NewTeam(ctx, &fleet.Team{ + Name: "team_automation_type_" + t.Name(), + }) + require.NoError(t, err) + + // Upload a software installer for the software install policy + payload := &fleet.UploadSoftwareInstallerPayload{ + InstallScript: "some install script", + Filename: "ruby.deb", + TeamID: &team.ID, + } + s.uploadSoftwareInstaller(t, payload, http.StatusOK, "") + rubyTitleID := getSoftwareTitleID(t, s.ds, "ruby", "deb_packages") + + // Upload a second software installer for the patch policy (supported for FMAs) + payload2 := &fleet.UploadSoftwareInstallerPayload{ + InstallScript: "some install script", + Filename: "dummy_installer.pkg", + TeamID: &team.ID, + } + s.uploadSoftwareInstaller(t, payload2, http.StatusOK, "") + dummyTitleID := getSoftwareTitleID(t, s.ds, "DummyApp", "apps") + // Create a Fleet Maintained App and associate it with the dummy installer + mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error { + res, err := q.ExecContext(ctx, `INSERT INTO fleet_maintained_apps (name, slug, platform, unique_identifier) + VALUES ('DummyApp', 'dummy-autofilter/darwin', 'darwin', 'com.example.dummy-autofilter')`) + require.NoError(t, err) + id, err := res.LastInsertId() + require.NoError(t, err) + _, err = q.ExecContext(ctx, `UPDATE software_installers SET fleet_maintained_app_id = ? WHERE global_or_team_id = ? AND title_id = ?`, id, team.ID, dummyTitleID) + require.NoError(t, err) + return nil + }) + + // Create a regular policy (no automation) + regularPolicy := teamPolicyResponse{} + s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/fleets/%d/policies", team.ID), teamPolicyRequest{ + Name: "regular policy", + Query: "SELECT 1;", + }, http.StatusOK, ®ularPolicy) + + // Create a software install policy + installPolicy := teamPolicyResponse{} + s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/fleets/%d/policies", team.ID), teamPolicyRequest{ + Name: "install software policy", + Query: "SELECT 1 FROM some_table;", + SoftwareTitleID: &rubyTitleID, + }, http.StatusOK, &installPolicy) + require.NotNil(t, installPolicy.Policy.InstallSoftware) + + // Create a patch policy + patchPolicy := teamPolicyResponse{} + s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/fleets/%d/policies", team.ID), teamPolicyRequest{ + Type: ptr.String("patch"), + PatchSoftwareTitleID: &dummyTitleID, + }, http.StatusOK, &patchPolicy) + require.NotNil(t, patchPolicy.Policy.PatchSoftware) + require.Equal(t, fleet.PolicyTypePatch, patchPolicy.Policy.Type) + + // List all policies (no filter) - should return all 3 + listResp := listTeamPoliciesResponse{} + s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/fleets/%d/policies", team.ID), nil, http.StatusOK, &listResp) + require.Len(t, listResp.Policies, 3) + + // List with automation_type=software - should return install policy and patch policy only + listResp = listTeamPoliciesResponse{} + s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/fleets/%d/policies", team.ID), nil, http.StatusOK, &listResp, "automation_type", "software") + require.Len(t, listResp.Policies, 2) + policyIDs := []uint{listResp.Policies[0].ID, listResp.Policies[1].ID} + assert.Contains(t, policyIDs, installPolicy.Policy.ID) + assert.Contains(t, policyIDs, patchPolicy.Policy.ID) + + // List with merge_inherited and automation_type=software + listResp = listTeamPoliciesResponse{} + s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/fleets/%d/policies", team.ID), nil, http.StatusOK, &listResp, "merge_inherited", "true", "automation_type", "software") + require.Len(t, listResp.Policies, 2) + policyIDs = []uint{listResp.Policies[0].ID, listResp.Policies[1].ID} + assert.Contains(t, policyIDs, installPolicy.Policy.ID) + assert.Contains(t, policyIDs, patchPolicy.Policy.ID) +} + func (s *integrationEnterpriseTestSuite) TestNoTeamPolicies() { t := s.T() ctx := context.Background()