From e4d8fcc3a5ead406c29ec9a857bfafe21e765374 Mon Sep 17 00:00:00 2001 From: Roberto Dip Date: Tue, 23 Jul 2024 14:56:24 -0500 Subject: [PATCH] fix: adjust host filters for VPP software (#20663) this allows to filter hosts with pending, failed and installed VPP apps. # Checklist for submitter If some of the following don't apply, delete the relevant line. - [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 --- server/datastore/mysql/hosts.go | 30 ++++++++++++----- server/datastore/mysql/software_installers.go | 33 +++++++++++++++++++ server/service/integration_mdm_test.go | 28 ++++++++++++++++ 3 files changed, 83 insertions(+), 8 deletions(-) diff --git a/server/datastore/mysql/hosts.go b/server/datastore/mysql/hosts.go index a37dd0ac8b..1afd8e93de 100644 --- a/server/datastore/mysql/hosts.go +++ b/server/datastore/mysql/hosts.go @@ -1038,17 +1038,31 @@ func (ds *Datastore) applyHostFilters( // software (version) ID filter is mutually exclusive with software title ID // so we're reusing the same filter to avoid adding unnecessary conditions. if opt.SoftwareStatusFilter != nil { - // get the installer id meta, err := ds.GetSoftwareInstallerMetadataByTeamAndTitleID(ctx, opt.TeamFilter, *opt.SoftwareTitleIDFilter, false) - if err != nil { + switch { + case fleet.IsNotFound(err): + vppApp, err := ds.GetVPPAppByTeamAndTitleID(ctx, opt.TeamFilter, *opt.SoftwareTitleIDFilter, false) + if err != nil { + return "", nil, ctxerr.Wrap(ctx, err, "get vpp app by team and title id") + } + vppAppJoin, vppAppParams, err := ds.vppAppJoin(vppApp.AdamID, *opt.SoftwareStatusFilter) + if err != nil { + return "", nil, ctxerr.Wrap(ctx, err, "vpp app join") + } + softwareStatusJoin = vppAppJoin + joinParams = append(joinParams, vppAppParams...) + + case err != nil: return "", nil, ctxerr.Wrap(ctx, err, "get software installer metadata by team and title id") + default: + installerJoin, installerParams, err := ds.softwareInstallerJoin(meta.InstallerID, *opt.SoftwareStatusFilter) + if err != nil { + return "", nil, ctxerr.Wrap(ctx, err, "software installer join") + } + softwareStatusJoin = installerJoin + joinParams = append(joinParams, installerParams...) + } - installerJoin, installerParams, err := ds.softwareInstallerJoin(meta.InstallerID, *opt.SoftwareStatusFilter) - if err != nil { - return "", nil, ctxerr.Wrap(ctx, err, "software installer join") - } - softwareStatusJoin = installerJoin - joinParams = append(joinParams, installerParams...) } else { softwareFilter = "EXISTS (SELECT 1 FROM host_software hs INNER JOIN software sw ON hs.software_id = sw.id WHERE hs.host_id = h.id AND sw.title_id = ?)" whereParams = append(whereParams, *opt.SoftwareTitleIDFilter) diff --git a/server/datastore/mysql/software_installers.go b/server/datastore/mysql/software_installers.go index 59e96b5db6..637f28eca7 100644 --- a/server/datastore/mysql/software_installers.go +++ b/server/datastore/mysql/software_installers.go @@ -416,6 +416,39 @@ WHERE return &dest, nil } +func (ds *Datastore) vppAppJoin(adamID string, status fleet.SoftwareInstallerStatus) (string, []interface{}, error) { + stmt := fmt.Sprintf(`JOIN ( +SELECT + host_id +FROM + host_vpp_software_installs hvsi +LEFT OUTER JOIN + nano_command_results ncr ON ncr.command_uuid = hvsi.command_uuid +WHERE + adam_id = :adam_id + AND hvsi.id IN( + SELECT + max(id) -- ensure we use only the most recent install attempt for each host + FROM host_vpp_software_installs + WHERE + adam_id = :adam_id + GROUP BY + host_id, adam_id) + AND (%s) = :status) hss ON hss.host_id = h.id +`, vppAppHostStatusNamedQuery("hvsi", "ncr", "")) + + return sqlx.Named(stmt, map[string]interface{}{ + "status": status, + "adam_id": adamID, + "software_status_installed": fleet.SoftwareInstallerInstalled, + "software_status_failed": fleet.SoftwareInstallerFailed, + "software_status_pending": fleet.SoftwareInstallerPending, + "mdm_status_acknowledged": fleet.MDMAppleStatusAcknowledged, + "mdm_status_error": fleet.MDMAppleStatusError, + "mdm_status_format_error": fleet.MDMAppleStatusCommandFormatError, + }) +} + func (ds *Datastore) softwareInstallerJoin(installerID uint, status fleet.SoftwareInstallerStatus) (string, []interface{}, error) { stmt := fmt.Sprintf(`JOIN ( SELECT diff --git a/server/service/integration_mdm_test.go b/server/service/integration_mdm_test.go index 1d9661da8f..9611d7dd79 100644 --- a/server/service/integration_mdm_test.go +++ b/server/service/integration_mdm_test.go @@ -9820,6 +9820,15 @@ func (s *integrationMDMTestSuite) TestVPPApps() { installResp = installSoftwareResponse{} s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/software/install/%d", mdmHost.ID, errTitleID), &installSoftwareRequest{}, http.StatusAccepted, &installResp) + // Check if the host is listed as pending + var listResp listHostsResponse + s.DoJSON("GET", "/api/latest/fleet/hosts", nil, http.StatusOK, &listResp, "software_status", "pending", "team_id", strconv.Itoa(int(team.ID)), "software_title_id", strconv.Itoa(int(errTitleID))) + require.Len(t, listResp.Hosts, 1) + require.Equal(t, listResp.Hosts[0].ID, mdmHost.ID) + var countResp countHostsResponse + s.DoJSON("GET", "/api/latest/fleet/hosts/count", nil, http.StatusOK, &countResp, "software_status", "pending", "team_id", strconv.Itoa(int(team.ID)), "software_title_id", strconv.Itoa(int(errTitleID))) + require.Equal(t, 1, countResp.Count) + // Simulate failed installation on the host cmd, err := mdmDevice.Idle() var cmdUUID string @@ -9835,6 +9844,14 @@ func (s *integrationMDMTestSuite) TestVPPApps() { } } + listResp = listHostsResponse{} + s.DoJSON("GET", "/api/latest/fleet/hosts", nil, http.StatusOK, &listResp, "software_status", "failed", "team_id", strconv.Itoa(int(team.ID)), "software_title_id", strconv.Itoa(int(errTitleID))) + require.Len(t, listResp.Hosts, 1) + require.Equal(t, listResp.Hosts[0].ID, mdmHost.ID) + countResp = countHostsResponse{} + s.DoJSON("GET", "/api/latest/fleet/hosts/count", nil, http.StatusOK, &countResp, "software_status", "failed", "team_id", strconv.Itoa(int(team.ID)), "software_title_id", strconv.Itoa(int(errTitleID))) + require.Equal(t, 1, countResp.Count) + s.lastActivityMatches( fleet.ActivityInstalledAppStoreApp{}.ActivityName(), fmt.Sprintf( @@ -9852,6 +9869,10 @@ func (s *integrationMDMTestSuite) TestVPPApps() { // Trigger install to the host installResp = installSoftwareResponse{} s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/software/install/%d", mdmHost.ID, titleID), &installSoftwareRequest{}, http.StatusAccepted, &installResp) + require.Equal(t, listResp.Hosts[0].ID, mdmHost.ID) + countResp = countHostsResponse{} + s.DoJSON("GET", "/api/latest/fleet/hosts/count", nil, http.StatusOK, &countResp, "software_status", "pending", "team_id", strconv.Itoa(int(team.ID)), "software_title_id", strconv.Itoa(int(titleID))) + require.Equal(t, 1, countResp.Count) // Simulate successful installation on the host cmd, err = mdmDevice.Idle() @@ -9867,6 +9888,13 @@ func (s *integrationMDMTestSuite) TestVPPApps() { } } + listResp = listHostsResponse{} + s.DoJSON("GET", "/api/latest/fleet/hosts", nil, http.StatusOK, &listResp, "software_status", "installed", "team_id", strconv.Itoa(int(team.ID)), "software_title_id", strconv.Itoa(int(titleID))) + require.Len(t, listResp.Hosts, 1) + countResp = countHostsResponse{} + s.DoJSON("GET", "/api/latest/fleet/hosts/count", nil, http.StatusOK, &countResp, "software_status", "installed", "team_id", strconv.Itoa(int(team.ID)), "software_title_id", strconv.Itoa(int(titleID))) + require.Equal(t, 1, countResp.Count) + s.lastActivityMatches( fleet.ActivityInstalledAppStoreApp{}.ActivityName(), fmt.Sprintf(