From 5f2eaefabd167303827c8b114fa906c7e8aa0b31 Mon Sep 17 00:00:00 2001 From: Lucas Manuel Rodriguez Date: Fri, 30 Aug 2024 18:58:10 -0300 Subject: [PATCH] Prevent installing on pending host+installer (#21722) #21428 Figma: https://www.figma.com/design/4pfUOYy7IyMIrjMH2fuCdU/%2319551-Policy-automations%3A-install-software?node-id=5871-12100&t=pKh926u8a30iYFBA-4 - [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] Added/updated tests - [X] Manual QA for all new/changed functionality --- ...21428-prevent-install-when-already-pending | 1 + ee/server/service/software_installers.go | 18 +++++++ server/service/integration_enterprise_test.go | 47 +++++++++++++++++-- 3 files changed, 61 insertions(+), 5 deletions(-) create mode 100644 changes/21428-prevent-install-when-already-pending diff --git a/changes/21428-prevent-install-when-already-pending b/changes/21428-prevent-install-when-already-pending new file mode 100644 index 000000000000..d01006d6f91d --- /dev/null +++ b/changes/21428-prevent-install-when-already-pending @@ -0,0 +1 @@ +* Added validation to `POST /api/_version_/fleet/hosts/{host_id}/software/install/{software_title_id}` to prevent installing on a host that already has a pending installation for that software title. diff --git a/ee/server/service/software_installers.go b/ee/server/service/software_installers.go index eca416612941..d4969b71999f 100644 --- a/ee/server/service/software_installers.go +++ b/ee/server/service/software_installers.go @@ -385,6 +385,24 @@ func (svc *Service) InstallSoftwareTitle(ctx context.Context, hostID uint, softw // if we found an installer, use that if installer != nil { + 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) + } + if lastInstallRequest != nil && lastInstallRequest.Status != nil && *lastInstallRequest.Status == fleet.SoftwareInstallerPending { + return &fleet.BadRequestError{ + Message: "Couldn't install software. Host has a pending install request.", + InternalErr: ctxerr.WrapWithData( + ctx, err, "host already has a pending install for this installer", + map[string]any{ + "host_id": host.ID, + "software_installer_id": installer.InstallerID, + "team_id": host.TeamID, + "title_id": softwareTitleID, + }, + ), + } + } return svc.installSoftwareTitleUsingInstaller(ctx, host, installer) } } diff --git a/server/service/integration_enterprise_test.go b/server/service/integration_enterprise_test.go index 99904d6a0490..bba5df5c89f4 100644 --- a/server/service/integration_enterprise_test.go +++ b/server/service/integration_enterprise_test.go @@ -11117,7 +11117,7 @@ func (s *integrationEnterpriseTestSuite) TestHostSoftwareInstallResult() { host := createOrbitEnrolledHost(t, "linux", "", s.ds) - // create a software installer and some host install requests + // Create software installers and corresponding host install requests. payload := &fleet.UploadSoftwareInstallerPayload{ InstallScript: "install script", PreInstallQuery: "pre install query", @@ -11127,6 +11127,24 @@ func (s *integrationEnterpriseTestSuite) TestHostSoftwareInstallResult() { } s.uploadSoftwareInstaller(payload, http.StatusOK, "") titleID := getSoftwareTitleID(t, s.ds, payload.Title, "deb_packages") + payload2 := &fleet.UploadSoftwareInstallerPayload{ + InstallScript: "install script 2", + PreInstallQuery: "pre install query 2", + PostInstallScript: "post install script 2", + Filename: "vim.deb", + Title: "vim", + } + s.uploadSoftwareInstaller(payload2, http.StatusOK, "") + titleID2 := getSoftwareTitleID(t, s.ds, payload2.Title, "deb_packages") + payload3 := &fleet.UploadSoftwareInstallerPayload{ + InstallScript: "install script 3", + PreInstallQuery: "pre install query 3", + PostInstallScript: "post install script 3", + Filename: "emacs.deb", + Title: "emacs", + } + s.uploadSoftwareInstaller(payload3, http.StatusOK, "") + titleID3 := getSoftwareTitleID(t, s.ds, payload3.Title, "deb_packages") latestInstallUUID := func() string { var id string @@ -11138,9 +11156,10 @@ func (s *integrationEnterpriseTestSuite) TestHostSoftwareInstallResult() { // create some install requests for the host installUUIDs := make([]string, 3) + titleIDs := []uint{titleID, titleID2, titleID3} for i := 0; i < len(installUUIDs); i++ { resp := installSoftwareResponse{} - s.DoJSON("POST", fmt.Sprintf("/api/v1/fleet/hosts/%d/software/install/%d", host.ID, titleID), nil, http.StatusAccepted, &resp) + s.DoJSON("POST", fmt.Sprintf("/api/v1/fleet/hosts/%d/software/install/%d", host.ID, titleIDs[i]), nil, http.StatusAccepted, &resp) installUUIDs[i] = latestInstallUUID() } @@ -11203,7 +11222,14 @@ func (s *integrationEnterpriseTestSuite) TestHostSoftwareInstallResult() { Status: fleet.SoftwareInstallerFailed, PreInstallQueryOutput: ptr.String(fleet.SoftwareInstallerQueryFailCopy), }) - wantAct.InstallUUID = installUUIDs[1] + wantAct = fleet.ActivityTypeInstalledSoftware{ + HostID: host.ID, + HostDisplayName: host.DisplayName(), + SoftwareTitle: payload2.Title, + SoftwarePackage: payload2.Filename, + InstallUUID: installUUIDs[1], + Status: string(fleet.SoftwareInstallerFailed), + } s.lastActivityOfTypeMatches(wantAct.ActivityName(), string(jsonMustMarshal(t, wantAct)), 0) s.Do("POST", "/api/fleet/orbit/software_install/result", @@ -11225,8 +11251,14 @@ func (s *integrationEnterpriseTestSuite) TestHostSoftwareInstallResult() { Output: ptr.String(fmt.Sprintf(fleet.SoftwareInstallerInstallSuccessCopy, "success")), PostInstallScriptOutput: ptr.String(fmt.Sprintf(fleet.SoftwareInstallerPostInstallSuccessCopy, "ok")), }) - wantAct.InstallUUID = installUUIDs[2] - wantAct.Status = string(fleet.SoftwareInstallerInstalled) + wantAct = fleet.ActivityTypeInstalledSoftware{ + HostID: host.ID, + HostDisplayName: host.DisplayName(), + SoftwareTitle: payload3.Title, + SoftwarePackage: payload3.Filename, + InstallUUID: installUUIDs[2], + Status: string(fleet.SoftwareInstallerInstalled), + } lastActID := s.lastActivityOfTypeMatches(wantAct.ActivityName(), string(jsonMustMarshal(t, wantAct)), 0) // non-existing installation uuid @@ -13073,6 +13105,11 @@ func (s *integrationEnterpriseTestSuite) TestPolicyAutomationsSoftwareInstallers require.Equal(t, fleet.SoftwareInstallerPending, *host1LastInstall.Status) prevExecutionID := host1LastInstall.ExecutionID + // Request a manual installation on the host for the same installer, which should fail. + var installResp installSoftwareResponse + s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/software/install/%d", + host1Team1.ID, dummyInstallerPkgTitleID), nil, http.StatusBadRequest, &installResp) + // Submit same results as before, which should not trigger a installation because the policy is already failing. distributedResp = submitDistributedQueryResultsResponse{} s.DoJSONWithoutAuth("POST", "/api/osquery/distributed/write", genDistributedReqWithPolicyResults(