Skip to content

Commit

Permalink
Prevent installing on pending host+installer (#21722)
Browse files Browse the repository at this point in the history
#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
  • Loading branch information
lucasmrod authored Aug 30, 2024
1 parent ee7b05c commit 5f2eaef
Show file tree
Hide file tree
Showing 3 changed files with 61 additions and 5 deletions.
1 change: 1 addition & 0 deletions changes/21428-prevent-install-when-already-pending
Original file line number Diff line number Diff line change
@@ -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.
18 changes: 18 additions & 0 deletions ee/server/service/software_installers.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
Expand Down
47 changes: 42 additions & 5 deletions server/service/integration_enterprise_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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
Expand All @@ -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()
}

Expand Down Expand Up @@ -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",
Expand All @@ -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
Expand Down Expand Up @@ -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(
Expand Down

0 comments on commit 5f2eaef

Please sign in to comment.