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
This commit is contained in:
Lucas Manuel Rodriguez 2024-08-30 18:58:10 -03:00 committed by GitHub
parent ee7b05cb26
commit 5f2eaefabd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 61 additions and 5 deletions

View file

@ -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.

View file

@ -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)
}
}

View file

@ -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(