Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changes/20730-hide-available-for-install-wrong-team
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
* Fix a bug where a software installer (a package or a VPP app) that has been installed on a host still shows up as "Available for install" and can still be requested to be installed after the host is transferred to a different team without that installer (or after the installer is deleted).
10 changes: 6 additions & 4 deletions server/datastore/mysql/software.go
Original file line number Diff line number Diff line change
Expand Up @@ -2128,7 +2128,7 @@ AND EXISTS (SELECT 1 FROM software s JOIN software_cve scve ON scve.software_id
si.version as package_version,
-- in a future iteration, will be supported for VPP apps
NULL as vpp_app_self_service,
vap.adam_id as vpp_app_adam_id,
vat.adam_id as vpp_app_adam_id,
vap.latest_version as vpp_app_version,
NULLIF(vap.icon_url, '') as vpp_app_icon_url,
COALESCE(hsi.created_at, hvsi.created_at) as last_install_installed_at,
Expand All @@ -2138,13 +2138,15 @@ AND EXISTS (SELECT 1 FROM software s JOIN software_cve scve ON scve.software_id
FROM
software_titles st
LEFT OUTER JOIN
software_installers si ON st.id = si.title_id
software_installers si ON st.id = si.title_id AND si.global_or_team_id = :global_or_team_id
LEFT OUTER JOIN
host_software_installs hsi ON si.id = hsi.software_installer_id AND hsi.host_id = :host_id
LEFT OUTER JOIN
vpp_apps vap ON st.id = vap.title_id
vpp_apps vap ON st.id = vap.title_id AND vap.platform = :host_platform
LEFT OUTER JOIN
vpp_apps_teams vat ON vap.adam_id = vat.adam_id AND vap.platform = vat.platform AND vat.global_or_team_id = :global_or_team_id
LEFT OUTER JOIN
host_vpp_software_installs hvsi ON vap.adam_id = hvsi.adam_id AND vap.platform = hvsi.platform AND hvsi.host_id = :host_id
host_vpp_software_installs hvsi ON vat.adam_id = hvsi.adam_id AND hvsi.host_id = :host_id
LEFT OUTER JOIN
nano_command_results ncr ON ncr.command_uuid = hvsi.command_uuid
WHERE
Expand Down
216 changes: 216 additions & 0 deletions server/datastore/mysql/software_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package mysql

import (
"bytes"
"context"
"database/sql"
"encoding/hex"
Expand Down Expand Up @@ -66,6 +67,8 @@ func TestSoftware(t *testing.T) {
{"ListHostSoftware", testListHostSoftware},
{"ListIOSHostSoftware", testListIOSHostSoftware},
{"SetHostSoftwareInstallResult", testSetHostSoftwareInstallResult},
{"ListHostSoftwareInstallThenTransferTeam", testListHostSoftwareInstallThenTransferTeam},
{"ListHostSoftwareInstallThenDeleteInstallers", testListHostSoftwareInstallThenDeleteInstallers},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
Expand Down Expand Up @@ -4343,3 +4346,216 @@ func testSetHostSoftwareInstallResult(t *testing.T, ds *Datastore) {
require.Error(t, err)
require.True(t, fleet.IsNotFound(err))
}

func testListHostSoftwareInstallThenTransferTeam(t *testing.T, ds *Datastore) {
ctx := context.Background()
user := test.NewUser(t, ds, "user1", "user1@example.com", false)
host := test.NewHost(t, ds, "host1", "", "host1key", "host1uuid", time.Now(), test.WithPlatform("darwin"))
nanoEnroll(t, ds, host, false)
opts := fleet.HostSoftwareTitleListOptions{
ListOptions: fleet.ListOptions{PerPage: 10, IncludeMetadata: true, OrderKey: "name", TestSecondaryOrderKey: "source"},
IncludeAvailableForInstall: true,
}

team1, err := ds.NewTeam(ctx, &fleet.Team{Name: "team 1"})
require.NoError(t, err)
team2, err := ds.NewTeam(ctx, &fleet.Team{Name: "team 2"})
require.NoError(t, err)

err = ds.AddHostsToTeam(ctx, &team1.ID, []uint{host.ID})
require.NoError(t, err)
host.TeamID = &team1.ID

// add a single "externally-installed" software for that host
software := []fleet.Software{
{Name: "a", Version: "0.0.1", Source: "chrome_extensions"},
}
_, err = ds.UpdateHostSoftware(ctx, host.ID, software)
require.NoError(t, err)

// create a software installer for team 1
installerTm1, err := ds.MatchOrCreateSoftwareInstaller(ctx, &fleet.UploadSoftwareInstallerPayload{
InstallScript: "hello",
InstallerFile: bytes.NewReader([]byte("hello")),
StorageID: "storage1",
Filename: "file1",
Title: "file1",
Version: "1.0",
Source: "apps",
TeamID: &team1.ID,
})
require.NoError(t, err)

// install it on the host
hostInstall1, err := ds.InsertSoftwareInstallRequest(ctx, host.ID, installerTm1, false)
require.NoError(t, err)
err = ds.SetHostSoftwareInstallResult(ctx, &fleet.HostSoftwareInstallResultPayload{
HostID: host.ID,
InstallUUID: hostInstall1,
InstallScriptExitCode: ptr.Int(0),
})
require.NoError(t, err)

// add a VPP app for team 1
vppTm1, err := ds.InsertVPPAppWithTeam(ctx,
&fleet.VPPApp{VPPAppID: fleet.VPPAppID{AdamID: "adam_vpp_1", Platform: fleet.MacOSPlatform}, Name: "vpp1",
BundleIdentifier: "com.app.vpp1"}, &team1.ID)
require.NoError(t, err)

// fail to install it on the host
vpp1CmdUUID := createVPPAppInstallRequest(t, ds, host, vppTm1.AdamID, user.ID)
createVPPAppInstallResult(t, ds, host, vpp1CmdUUID, fleet.MDMAppleStatusError)

// add the successful installer to the reported installed software
software = []fleet.Software{
{Name: "a", Version: "0.0.1", Source: "chrome_extensions"},
{Name: "file1", Version: "1.0", Source: "apps"},
}
_, err = ds.UpdateHostSoftware(ctx, host.ID, software)
require.NoError(t, err)

// listing the host's software (including available for install) at this
// point lists "a", "file1" and "vpp1" (because of the install attempt)
sw, meta, err := ds.ListHostSoftware(ctx, host, opts)
require.NoError(t, err)
require.Len(t, sw, 3)
require.EqualValues(t, 3, meta.TotalResults)
require.Equal(t, sw[0].Name, "a")
require.Nil(t, sw[0].AppStoreApp)
require.Nil(t, sw[0].SoftwarePackage)
require.Equal(t, sw[1].Name, "file1")
require.Nil(t, sw[1].AppStoreApp)
require.NotNil(t, sw[1].SoftwarePackage)
require.Equal(t, sw[2].Name, "vpp1")
require.NotNil(t, sw[2].AppStoreApp)
require.Nil(t, sw[2].SoftwarePackage)

// move host to team 2
err = ds.AddHostsToTeam(ctx, &team2.ID, []uint{host.ID})
require.NoError(t, err)
host.TeamID = &team2.ID

// listing the host's software (including available for install) should now
// only list "a" and "file1" (because they are actually installed) and not
// link them to the installer/VPP app. With and without available software
// should result in the same rows (no available software in that new team).
for _, b := range []bool{true, false} {
opts.IncludeAvailableForInstall = b
sw, meta, err = ds.ListHostSoftware(ctx, host, opts)
require.NoError(t, err)
require.Len(t, sw, 2)
require.EqualValues(t, 2, meta.TotalResults)
require.Equal(t, sw[0].Name, "a")
require.Nil(t, sw[0].AppStoreApp)
require.Nil(t, sw[0].SoftwarePackage)
require.Equal(t, sw[1].Name, "file1")
require.Nil(t, sw[1].AppStoreApp)
require.Nil(t, sw[1].SoftwarePackage)
}
}

func testListHostSoftwareInstallThenDeleteInstallers(t *testing.T, ds *Datastore) {
ctx := context.Background()
user := test.NewUser(t, ds, "user1", "user1@example.com", false)
host := test.NewHost(t, ds, "host1", "", "host1key", "host1uuid", time.Now(), test.WithPlatform("darwin"))
nanoEnroll(t, ds, host, false)
opts := fleet.HostSoftwareTitleListOptions{
ListOptions: fleet.ListOptions{PerPage: 10, IncludeMetadata: true, OrderKey: "name", TestSecondaryOrderKey: "source"},
IncludeAvailableForInstall: true,
}

team1, err := ds.NewTeam(ctx, &fleet.Team{Name: "team 1"})
require.NoError(t, err)

err = ds.AddHostsToTeam(ctx, &team1.ID, []uint{host.ID})
require.NoError(t, err)
host.TeamID = &team1.ID

// add a single "externally-installed" software for that host
software := []fleet.Software{
{Name: "a", Version: "0.0.1", Source: "chrome_extensions"},
}
_, err = ds.UpdateHostSoftware(ctx, host.ID, software)
require.NoError(t, err)

// create a software installer for team 1
installerTm1, err := ds.MatchOrCreateSoftwareInstaller(ctx, &fleet.UploadSoftwareInstallerPayload{
InstallScript: "hello",
InstallerFile: bytes.NewReader([]byte("hello")),
StorageID: "storage1",
Filename: "file1",
Title: "file1",
Version: "1.0",
Source: "apps",
TeamID: &team1.ID,
})
require.NoError(t, err)

// fail to install it on the host
hostInstall1, err := ds.InsertSoftwareInstallRequest(ctx, host.ID, installerTm1, false)
require.NoError(t, err)
err = ds.SetHostSoftwareInstallResult(ctx, &fleet.HostSoftwareInstallResultPayload{
HostID: host.ID,
InstallUUID: hostInstall1,
InstallScriptExitCode: ptr.Int(1),
})
require.NoError(t, err)

// add a VPP app for team 1
vppTm1, err := ds.InsertVPPAppWithTeam(ctx,
&fleet.VPPApp{VPPAppID: fleet.VPPAppID{AdamID: "adam_vpp_1", Platform: fleet.MacOSPlatform}, Name: "vpp1",
BundleIdentifier: "com.app.vpp1", LatestVersion: "1.0"}, &team1.ID)
require.NoError(t, err)

// install it on the host
vpp1CmdUUID := createVPPAppInstallRequest(t, ds, host, vppTm1.AdamID, user.ID)
createVPPAppInstallResult(t, ds, host, vpp1CmdUUID, fleet.MDMAppleStatusAcknowledged)

// add the successful VPP app to the reported installed software
software = []fleet.Software{
{Name: "a", Version: "0.0.1", Source: "chrome_extensions"},
{Name: "vpp1", Version: "1.0", Source: "apps", BundleIdentifier: "com.app.vpp1"},
}
_, err = ds.UpdateHostSoftware(ctx, host.ID, software)
require.NoError(t, err)

// listing the host's software (including available for install) at this
// point lists "a", "file1" and "vpp1" (because of the install attempt)
sw, meta, err := ds.ListHostSoftware(ctx, host, opts)
require.NoError(t, err)
require.Len(t, sw, 3)
require.EqualValues(t, 3, meta.TotalResults)
require.Equal(t, sw[0].Name, "a")
require.Nil(t, sw[0].AppStoreApp)
require.Nil(t, sw[0].SoftwarePackage)
require.Equal(t, sw[1].Name, "file1")
require.Nil(t, sw[1].AppStoreApp)
require.NotNil(t, sw[1].SoftwarePackage)
require.Equal(t, sw[2].Name, "vpp1")
require.NotNil(t, sw[2].AppStoreApp)
require.Nil(t, sw[2].SoftwarePackage)

// delete both installers
err = ds.DeleteSoftwareInstaller(ctx, installerTm1)
require.NoError(t, err)
err = ds.DeleteVPPAppFromTeam(ctx, &team1.ID, vppTm1.VPPAppID)
require.NoError(t, err)

// listing the host's software (including available for install) should now
// only list "a" and "vpp1" (because they are actually installed) and not
// link them to the installer/VPP app. With and without available software
// should result in the same rows (no available software anymore).
for _, b := range []bool{true, false} {
opts.IncludeAvailableForInstall = b
sw, meta, err = ds.ListHostSoftware(ctx, host, opts)
require.NoError(t, err)
require.Len(t, sw, 2)
require.EqualValues(t, 2, meta.TotalResults)
require.Equal(t, sw[0].Name, "a")
require.Nil(t, sw[0].AppStoreApp)
require.Nil(t, sw[0].SoftwarePackage)
require.Equal(t, sw[1].Name, "vpp1")
require.Nil(t, sw[1].AppStoreApp)
require.Nil(t, sw[1].SoftwarePackage)
}
}