Fix many issues with VPP apps select queries and host counts (#21039)

Fix for #21013 and other fixes found while testing VPP apps with
iOS/iPadOS/macOS devices.

Regarding the change in ListSoftwareTitles with `TeamID=nil`, from Noah:
https://github.com/fleetdm/fleet/issues/19447#issuecomment-2270006803
This commit is contained in:
Lucas Manuel Rodriguez
2024-08-06 18:00:52 -03:00
committed by GitHub
parent 285822e1bb
commit fb4292ce08
4 changed files with 391 additions and 114 deletions
+3 -3
View File
@@ -640,7 +640,7 @@ func (ds *Datastore) insertNewInstalledHostSoftwareDB(
UPDATE software s
JOIN software_titles st
ON s.bundle_identifier = st.bundle_identifier AND
IF(s.source IN ('ios_apps', 'ipados_apps'), s.source = st.source, 1)
IF(s.source IN ('apps', 'ios_apps', 'ipados_apps'), s.source = st.source, 1)
SET s.title_id = st.id
WHERE s.title_id IS NULL
OR s.title_id != st.id
@@ -1667,7 +1667,7 @@ FROM (
NOT EXISTS (
SELECT 1 FROM software_titles st
WHERE s.bundle_identifier = st.bundle_identifier AND
IF(s.source IN ('ios_apps', 'ipados_apps'), s.source = st.source, 1)
IF(s.source IN ('apps', 'ios_apps', 'ipados_apps'), s.source = st.source, 1)
)
AND COALESCE(bundle_identifier, '') != ''
@@ -1718,7 +1718,7 @@ AND COALESCE(s.bundle_identifier, '') = '';
UPDATE software s
JOIN software_titles st
ON s.bundle_identifier = st.bundle_identifier AND
IF(s.source IN ('ios_apps', 'ipados_apps'), s.source = st.source, 1)
IF(s.source IN ('apps', 'ios_apps', 'ipados_apps'), s.source = st.source, 1)
SET s.title_id = st.id
WHERE s.title_id IS NULL
OR s.title_id != st.id;
+47 -25
View File
@@ -13,18 +13,24 @@ import (
)
func (ds *Datastore) SoftwareTitleByID(ctx context.Context, id uint, teamID *uint, tmFilter fleet.TeamFilter) (*fleet.SoftwareTitle, error) {
var teamFilter string // used to filter software titles host counts by team
var (
teamFilter string // used to filter software titles host counts by team
softwareInstallerGlobalOrTeamIDFilter string
vppAppsTeamsGlobalOrTeamIDFilter string
)
if teamID != nil {
teamFilter = fmt.Sprintf("sthc.team_id = %d AND sthc.global_stats = 0", *teamID)
softwareInstallerGlobalOrTeamIDFilter = fmt.Sprintf("si.global_or_team_id = %d", *teamID)
vppAppsTeamsGlobalOrTeamIDFilter = fmt.Sprintf("vat.global_or_team_id = %d", *teamID)
} else {
teamFilter = ds.whereFilterGlobalOrTeamIDByTeams(tmFilter, "sthc")
softwareInstallerGlobalOrTeamIDFilter = "TRUE"
vppAppsTeamsGlobalOrTeamIDFilter = "TRUE"
}
var tmID uint // used to filter software installers by team
if teamID != nil {
tmID = *teamID
}
// Select software title but filter out if the software has zero host counts
// and it's not an installer or VPP app.
selectSoftwareTitleStmt := fmt.Sprintf(`
SELECT
st.id,
@@ -32,27 +38,27 @@ SELECT
st.source,
st.browser,
st.bundle_identifier,
COALESCE(SUM(sthc.hosts_count), 0) as hosts_count,
MAX(sthc.updated_at) as counts_updated_at,
COALESCE(SUM(sthc.hosts_count), 0) AS hosts_count,
MAX(sthc.updated_at) AS counts_updated_at,
COUNT(si.id) as software_installers_count,
COUNT(vat.adam_id) as vpp_apps_count
COUNT(vat.adam_id) AS vpp_apps_count
FROM software_titles st
LEFT JOIN software_titles_host_counts sthc ON sthc.software_title_id = st.id
LEFT JOIN software_installers si ON si.title_id = st.id AND si.global_or_team_id = ?
LEFT JOIN software_titles_host_counts sthc ON sthc.software_title_id = st.id AND sthc.hosts_count > 0 AND (%s)
LEFT JOIN software_installers si ON si.title_id = st.id AND %s
LEFT JOIN vpp_apps vap ON vap.title_id = st.id
LEFT JOIN vpp_apps_teams vat ON vat.global_or_team_id = ? AND vat.adam_id = vap.adam_id AND vat.platform = vap.platform
LEFT JOIN vpp_apps_teams vat ON vat.adam_id = vap.adam_id AND vat.platform = vap.platform AND %s
WHERE st.id = ? AND
((sthc.hosts_count > 0 AND %s) OR vat.adam_id IS NOT NULL OR si.id IS NOT NULL)
(sthc.hosts_count > 0 OR vat.adam_id IS NOT NULL OR si.id IS NOT NULL)
GROUP BY
st.id,
st.name,
st.source,
st.browser,
st.bundle_identifier
`, teamFilter,
`, teamFilter, softwareInstallerGlobalOrTeamIDFilter, vppAppsTeamsGlobalOrTeamIDFilter,
)
var title fleet.SoftwareTitle
if err := sqlx.GetContext(ctx, ds.reader(ctx), &title, selectSoftwareTitleStmt, tmID, tmID, id); err != nil {
if err := sqlx.GetContext(ctx, ds.reader(ctx), &title, selectSoftwareTitleStmt, id); err != nil {
if err == sql.ErrNoRows {
return nil, notFound("SoftwareTitle").WithID(id)
}
@@ -262,10 +268,10 @@ SELECT
vap.latest_version as vpp_app_version,
vap.icon_url as vpp_app_icon_url
FROM software_titles st
LEFT JOIN software_installers si ON si.title_id = st.id AND si.global_or_team_id = ?
LEFT JOIN software_installers si ON si.title_id = st.id AND %s
LEFT JOIN vpp_apps vap ON vap.title_id = st.id
LEFT JOIN vpp_apps_teams vat ON vat.global_or_team_id = ? AND vat.adam_id = vap.adam_id AND vat.platform = vap.platform
LEFT JOIN software_titles_host_counts sthc ON sthc.software_title_id = st.id AND sthc.team_id = ?
LEFT JOIN vpp_apps_teams vat ON vat.adam_id = vap.adam_id AND vat.platform = vap.platform AND %s
LEFT JOIN software_titles_host_counts sthc ON sthc.software_title_id = st.id AND (%s)
-- placeholder for JOIN on software/software_cve
%s
-- placeholder for optional extra WHERE filter
@@ -279,9 +285,24 @@ GROUP BY st.id, package_self_service, package_name, package_version, vpp_app_sel
cveJoinType = "INNER"
}
args := []any{0, 0, 0}
if opt.TeamID != nil {
args[0], args[1], args[2] = *opt.TeamID, *opt.TeamID, *opt.TeamID
countsJoin := "TRUE"
softwareInstallersJoinCond := "TRUE"
vppAppsTeamsJoinCond := "TRUE"
includeVPPAppsAndSoftwareInstallers := "TRUE"
switch {
case opt.TeamID == nil:
countsJoin = "sthc.team_id = 0 AND sthc.global_stats = 1"
// When opt.TeamID is nil (aka "All teams") we do not include VPP-apps/installers
// that are not installed on any host.
includeVPPAppsAndSoftwareInstallers = "FALSE"
case *opt.TeamID == 0:
countsJoin = "sthc.team_id = 0 AND sthc.global_stats = 0"
softwareInstallersJoinCond = fmt.Sprintf("si.global_or_team_id = %d", *opt.TeamID)
vppAppsTeamsJoinCond = fmt.Sprintf("vat.global_or_team_id = %d", *opt.TeamID)
case *opt.TeamID > 0:
countsJoin = fmt.Sprintf("sthc.team_id = %d AND sthc.global_stats = 0", *opt.TeamID)
softwareInstallersJoinCond = fmt.Sprintf("si.global_or_team_id = %d", *opt.TeamID)
vppAppsTeamsJoinCond = fmt.Sprintf("vat.global_or_team_id = %d", *opt.TeamID)
}
additionalWhere := "TRUE"
@@ -299,6 +320,7 @@ GROUP BY st.id, package_self_service, package_name, package_version, vpp_app_sel
`, cveJoinType)
}
var args []any
if match != "" {
additionalWhere = " (st.name LIKE ? OR scve.cve LIKE ?)"
match = likePattern(match)
@@ -306,9 +328,9 @@ GROUP BY st.id, package_self_service, package_name, package_version, vpp_app_sel
}
// default to "a software installer or VPP app exists", and see next condition.
defaultFilter := `
(si.id IS NOT NULL OR vat.adam_id IS NOT NULL)
`
defaultFilter := fmt.Sprintf(`
((si.id IS NOT NULL OR vat.adam_id IS NOT NULL) AND %s)
`, includeVPPAppsAndSoftwareInstallers)
// add software installed for hosts if any of this is true:
//
@@ -321,7 +343,7 @@ GROUP BY st.id, package_self_service, package_name, package_version, vpp_app_sel
defaultFilter += ` AND si.self_service = 1 `
}
stmt = fmt.Sprintf(stmt, softwareJoin, additionalWhere, defaultFilter)
stmt = fmt.Sprintf(stmt, softwareInstallersJoinCond, vppAppsTeamsJoinCond, countsJoin, softwareJoin, additionalWhere, defaultFilter)
return stmt, args
}
+338 -77
View File
@@ -28,6 +28,7 @@ func TestSoftwareTitles(t *testing.T) {
{"TeamFilterSoftwareTitles", testTeamFilterSoftwareTitles},
{"ListSoftwareTitlesInstallersOnly", testListSoftwareTitlesInstallersOnly},
{"ListSoftwareTitlesAvailableForInstallFilter", testListSoftwareTitlesAvailableForInstallFilter},
{"ListSoftwareTitlesAllTeams", testListSoftwareTitlesAllTeams},
{"UploadedSoftwareExists", testUploadedSoftwareExists},
}
for _, c := range cases {
@@ -259,6 +260,10 @@ func testSoftwareSyncHostsSoftwareTitles(t *testing.T, ds *Datastore) {
}
func testOrderSoftwareTitles(t *testing.T, ds *Datastore) {
//
// All tests below are in hosts in "No team".
//
ctx := context.Background()
host1 := test.NewHost(t, ds, "host1", "", "host1key", "host1uuid", time.Now())
@@ -316,8 +321,10 @@ func testOrderSoftwareTitles(t *testing.T, ds *Datastore) {
_, err = ds.InsertSoftwareInstallRequest(ctx, host1.ID, installer2, false)
require.NoError(t, err)
// create a VPP app not installed anywhere
_, err = ds.InsertVPPAppWithTeam(ctx, &fleet.VPPApp{Name: "vpp1", BundleIdentifier: "com.app.vpp1",
VPPAppID: fleet.VPPAppID{AdamID: "adam_vpp_app_1", Platform: fleet.IPadOSPlatform}}, nil)
_, err = ds.InsertVPPAppWithTeam(ctx, &fleet.VPPApp{
Name: "vpp1", BundleIdentifier: "com.app.vpp1",
VPPAppID: fleet.VPPAppID{AdamID: "adam_vpp_app_1", Platform: fleet.IPadOSPlatform},
}, nil)
require.NoError(t, err)
require.NoError(t, ds.SyncHostsSoftware(ctx, time.Now()))
@@ -325,10 +332,13 @@ func testOrderSoftwareTitles(t *testing.T, ds *Datastore) {
require.NoError(t, ds.SyncHostsSoftwareTitles(ctx, time.Now()))
// primary sort is "hosts_count DESC", followed by "name ASC, source ASC, browser ASC"
titles, _, _, err := ds.ListSoftwareTitles(ctx, fleet.SoftwareTitleListOptions{ListOptions: fleet.ListOptions{
OrderKey: "hosts_count",
OrderDirection: fleet.OrderDescending,
}}, fleet.TeamFilter{User: &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)}})
titles, _, _, err := ds.ListSoftwareTitles(ctx, fleet.SoftwareTitleListOptions{
ListOptions: fleet.ListOptions{
OrderKey: "hosts_count",
OrderDirection: fleet.OrderDescending,
},
TeamID: ptr.Uint(0),
}, fleet.TeamFilter{User: &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)}})
require.NoError(t, err)
require.Len(t, titles, 10)
i := 0
@@ -385,10 +395,13 @@ func testOrderSoftwareTitles(t *testing.T, ds *Datastore) {
require.NotNil(t, titles[i].AppStoreApp)
// primary sort is "hosts_count ASC", followed by "name ASC, source ASC, browser ASC"
titles, _, _, err = ds.ListSoftwareTitles(ctx, fleet.SoftwareTitleListOptions{ListOptions: fleet.ListOptions{
OrderKey: "hosts_count",
OrderDirection: fleet.OrderAscending,
}}, fleet.TeamFilter{User: &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)}})
titles, _, _, err = ds.ListSoftwareTitles(ctx, fleet.SoftwareTitleListOptions{
ListOptions: fleet.ListOptions{
OrderKey: "hosts_count",
OrderDirection: fleet.OrderAscending,
},
TeamID: ptr.Uint(0),
}, fleet.TeamFilter{User: &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)}})
require.NoError(t, err)
require.Len(t, titles, 10)
i = 0
@@ -425,10 +438,13 @@ func testOrderSoftwareTitles(t *testing.T, ds *Datastore) {
require.Equal(t, "deb_packages", titles[i].Source)
// primary sort is "name ASC", followed by "host_count DESC, source ASC, browser ASC"
titles, _, _, err = ds.ListSoftwareTitles(ctx, fleet.SoftwareTitleListOptions{ListOptions: fleet.ListOptions{
OrderKey: "name",
OrderDirection: fleet.OrderAscending,
}}, fleet.TeamFilter{User: &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)}})
titles, _, _, err = ds.ListSoftwareTitles(ctx, fleet.SoftwareTitleListOptions{
ListOptions: fleet.ListOptions{
OrderKey: "name",
OrderDirection: fleet.OrderAscending,
},
TeamID: ptr.Uint(0),
}, fleet.TeamFilter{User: &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)}})
require.NoError(t, err)
require.Len(t, titles, 10)
i = 0
@@ -465,10 +481,13 @@ func testOrderSoftwareTitles(t *testing.T, ds *Datastore) {
assert.Equal(t, "ipados_apps", titles[i].Source)
// primary sort is "name DESC", followed by "host_count DESC, source ASC, browser ASC"
titles, _, _, err = ds.ListSoftwareTitles(ctx, fleet.SoftwareTitleListOptions{ListOptions: fleet.ListOptions{
OrderKey: "name",
OrderDirection: fleet.OrderDescending,
}}, fleet.TeamFilter{User: &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)}})
titles, _, _, err = ds.ListSoftwareTitles(ctx, fleet.SoftwareTitleListOptions{
ListOptions: fleet.ListOptions{
OrderKey: "name",
OrderDirection: fleet.OrderDescending,
},
TeamID: ptr.Uint(0),
}, fleet.TeamFilter{User: &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)}})
require.NoError(t, err)
require.Len(t, titles, 10)
i = 0
@@ -505,11 +524,14 @@ func testOrderSoftwareTitles(t *testing.T, ds *Datastore) {
require.Equal(t, "apps", titles[i].Source)
// using a match query
titles, _, _, err = ds.ListSoftwareTitles(ctx, fleet.SoftwareTitleListOptions{ListOptions: fleet.ListOptions{
OrderKey: "name",
OrderDirection: fleet.OrderDescending,
MatchQuery: "ba",
}}, fleet.TeamFilter{User: &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)}})
titles, _, _, err = ds.ListSoftwareTitles(ctx, fleet.SoftwareTitleListOptions{
ListOptions: fleet.ListOptions{
OrderKey: "name",
OrderDirection: fleet.OrderDescending,
MatchQuery: "ba",
},
TeamID: ptr.Uint(0),
}, fleet.TeamFilter{User: &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)}})
require.NoError(t, err)
require.Len(t, titles, 4)
require.Equal(t, "baz", titles[0].Name)
@@ -524,11 +546,14 @@ func testOrderSoftwareTitles(t *testing.T, ds *Datastore) {
require.Equal(t, "apps", titles[3].Source)
// using another (installer-only) match query
titles, _, _, err = ds.ListSoftwareTitles(ctx, fleet.SoftwareTitleListOptions{ListOptions: fleet.ListOptions{
OrderKey: "name",
OrderDirection: fleet.OrderDescending,
MatchQuery: "insta",
}}, fleet.TeamFilter{User: &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)}})
titles, _, _, err = ds.ListSoftwareTitles(ctx, fleet.SoftwareTitleListOptions{
ListOptions: fleet.ListOptions{
OrderKey: "name",
OrderDirection: fleet.OrderDescending,
MatchQuery: "insta",
},
TeamID: ptr.Uint(0),
}, fleet.TeamFilter{User: &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)}})
require.NoError(t, err)
require.Len(t, titles, 2)
require.Equal(t, "installer2", titles[0].Name)
@@ -537,10 +562,14 @@ func testOrderSoftwareTitles(t *testing.T, ds *Datastore) {
require.Equal(t, "apps", titles[1].Source)
// filter on self-service only
titles, _, _, err = ds.ListSoftwareTitles(ctx, fleet.SoftwareTitleListOptions{ListOptions: fleet.ListOptions{
OrderKey: "name",
OrderDirection: fleet.OrderDescending,
}, SelfServiceOnly: true}, fleet.TeamFilter{User: &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)}})
titles, _, _, err = ds.ListSoftwareTitles(ctx, fleet.SoftwareTitleListOptions{
ListOptions: fleet.ListOptions{
OrderKey: "name",
OrderDirection: fleet.OrderDescending,
},
TeamID: ptr.Uint(0),
SelfServiceOnly: true,
}, fleet.TeamFilter{User: &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)}})
require.NoError(t, err)
require.Len(t, titles, 1)
require.Equal(t, "installer1", titles[0].Name)
@@ -615,32 +644,39 @@ func testTeamFilterSoftwareTitles(t *testing.T, ds *Datastore) {
})
require.NoError(t, err)
require.NotZero(t, installer2)
// create a VPP app for team2
_, err = ds.InsertVPPAppWithTeam(ctx, &fleet.VPPApp{Name: "vpp2", BundleIdentifier: "com.app.vpp2",
VPPAppID: fleet.VPPAppID{AdamID: "adam_vpp_app_2", Platform: fleet.IOSPlatform}}, &team2.ID)
_, err = ds.InsertVPPAppWithTeam(ctx, &fleet.VPPApp{
Name: "vpp2", BundleIdentifier: "com.app.vpp2",
VPPAppID: fleet.VPPAppID{AdamID: "adam_vpp_app_2", Platform: fleet.IOSPlatform},
}, &team2.ID)
require.NoError(t, err)
// create a VPP app for No Team
_, err = ds.InsertVPPAppWithTeam(ctx, &fleet.VPPApp{Name: "vpp3", BundleIdentifier: "com.app.vpp3",
VPPAppID: fleet.VPPAppID{AdamID: "adam_vpp_app_3", Platform: fleet.MacOSPlatform}}, ptr.Uint(0))
// create a VPP app for "No team"
_, err = ds.InsertVPPAppWithTeam(ctx, &fleet.VPPApp{
Name: "vpp3", BundleIdentifier: "com.app.vpp3",
VPPAppID: fleet.VPPAppID{AdamID: "adam_vpp_app_3", Platform: fleet.MacOSPlatform},
}, ptr.Uint(0))
require.NoError(t, err)
require.NoError(t, ds.SyncHostsSoftware(ctx, time.Now()))
require.NoError(t, ds.ReconcileSoftwareTitles(ctx))
require.NoError(t, ds.SyncHostsSoftwareTitles(ctx, time.Now()))
// Testing the global user (for no team)
// Testing the global user (for "All teams")
// Should not return VPP apps or software installers (because they are not installed yet).
globalTeamFilter := fleet.TeamFilter{User: userGlobalAdmin, IncludeObserver: true}
titles, count, _, err := ds.ListSoftwareTitles(
context.Background(), fleet.SoftwareTitleListOptions{ListOptions: fleet.ListOptions{}}, globalTeamFilter,
context.Background(), fleet.SoftwareTitleListOptions{
ListOptions: fleet.ListOptions{},
TeamID: nil,
}, globalTeamFilter,
)
sortTitlesByName(titles)
// software installers are associated with a team, so they don't show up in
// this request for no team, but other titles do because software titles are
// not associated with a team.
require.NoError(t, err)
require.Len(t, titles, 3)
require.Equal(t, 3, count)
require.Len(t, titles, 2)
require.Equal(t, 2, count)
require.Equal(t, "bar", titles[0].Name)
require.Equal(t, "deb_packages", titles[0].Source)
require.Equal(t, "foo", titles[1].Name)
@@ -653,19 +689,36 @@ func testTeamFilterSoftwareTitles(t *testing.T, ds *Datastore) {
assert.Equal(t, uint(2), titles[1].HostsCount)
require.Nil(t, titles[1].SoftwarePackage)
require.Nil(t, titles[1].AppStoreApp)
require.Equal(t, uint(0), titles[2].VersionsCount)
require.Nil(t, titles[2].SoftwarePackage)
require.Equal(t, "vpp3", titles[2].Name)
barTitle := titles[0]
fooTitle := titles[1]
title, err := ds.SoftwareTitleByID(context.Background(), titles[0].ID, nil, globalTeamFilter)
// Testing the global user (for "No team")
// should only return vpp3 because it's the only app in the "No team".
titles, count, _, err = ds.ListSoftwareTitles(
context.Background(), fleet.SoftwareTitleListOptions{
ListOptions: fleet.ListOptions{},
TeamID: ptr.Uint(0),
}, globalTeamFilter,
)
sortTitlesByName(titles)
require.NoError(t, err)
require.Len(t, titles, 1)
require.Equal(t, 1, count)
require.Equal(t, uint(0), titles[0].VersionsCount)
require.Nil(t, titles[0].SoftwarePackage)
require.Equal(t, "vpp3", titles[0].Name)
// Get title of bar software.
title, err := ds.SoftwareTitleByID(context.Background(), barTitle.ID, nil, globalTeamFilter)
require.NoError(t, err)
require.Zero(t, title.SoftwareInstallersCount)
require.Zero(t, title.VPPAppsCount)
// ListSoftwareTitles does not populate version host counts, so we do that manually
titles[0].Versions[0].HostsCount = ptr.Uint(1)
barTitle.Versions[0].HostsCount = ptr.Uint(1)
assert.Equal(
t,
titles[0],
barTitle,
fleet.SoftwareTitleListResult{
ID: title.ID,
Name: title.Name,
@@ -679,11 +732,11 @@ func testTeamFilterSoftwareTitles(t *testing.T, ds *Datastore) {
)
// Testing with team filter -- this team does not contain this software title
_, err = ds.SoftwareTitleByID(context.Background(), titles[0].ID, &team1.ID, globalTeamFilter)
_, err = ds.SoftwareTitleByID(context.Background(), barTitle.ID, &team1.ID, globalTeamFilter)
assert.ErrorIs(t, err, sql.ErrNoRows)
// Testing with team filter -- this team does contain this software title
title, err = ds.SoftwareTitleByID(context.Background(), titles[1].ID, &team1.ID, globalTeamFilter)
title, err = ds.SoftwareTitleByID(context.Background(), fooTitle.ID, &team1.ID, globalTeamFilter)
require.NoError(t, err)
require.Zero(t, title.SoftwareInstallersCount)
require.Zero(t, title.VPPAppsCount)
@@ -808,14 +861,19 @@ func testListSoftwareTitlesInstallersOnly(t *testing.T, ds *Datastore) {
require.NoError(t, err)
require.NotZero(t, installer2)
// create a VPP app not installed on a host
_, err = ds.InsertVPPAppWithTeam(ctx, &fleet.VPPApp{Name: "vpp1", BundleIdentifier: "com.app,vpp1",
VPPAppID: fleet.VPPAppID{AdamID: "adam_vpp_app_1", Platform: fleet.MacOSPlatform}}, nil)
_, err = ds.InsertVPPAppWithTeam(ctx, &fleet.VPPApp{
Name: "vpp1", BundleIdentifier: "com.app,vpp1",
VPPAppID: fleet.VPPAppID{AdamID: "adam_vpp_app_1", Platform: fleet.MacOSPlatform},
}, nil)
require.NoError(t, err)
titles, counts, _, err := ds.ListSoftwareTitles(ctx, fleet.SoftwareTitleListOptions{ListOptions: fleet.ListOptions{
OrderKey: "name",
OrderDirection: fleet.OrderAscending,
}}, fleet.TeamFilter{User: &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)}})
titles, counts, _, err := ds.ListSoftwareTitles(ctx, fleet.SoftwareTitleListOptions{
ListOptions: fleet.ListOptions{
OrderKey: "name",
OrderDirection: fleet.OrderAscending,
},
TeamID: ptr.Uint(0),
}, fleet.TeamFilter{User: &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)}})
require.NoError(t, err)
require.EqualValues(t, 3, counts)
require.Len(t, titles, 3)
@@ -834,11 +892,14 @@ func testListSoftwareTitlesInstallersOnly(t *testing.T, ds *Datastore) {
require.NoError(t, ds.SyncHostsSoftwareTitles(ctx, time.Now()))
// match installer1 name
titles, counts, _, err = ds.ListSoftwareTitles(ctx, fleet.SoftwareTitleListOptions{ListOptions: fleet.ListOptions{
OrderKey: "name",
OrderDirection: fleet.OrderAscending,
MatchQuery: "installer1",
}}, fleet.TeamFilter{User: &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)}})
titles, counts, _, err = ds.ListSoftwareTitles(ctx, fleet.SoftwareTitleListOptions{
ListOptions: fleet.ListOptions{
OrderKey: "name",
OrderDirection: fleet.OrderAscending,
MatchQuery: "installer1",
},
TeamID: ptr.Uint(0),
}, fleet.TeamFilter{User: &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)}})
require.NoError(t, err)
require.EqualValues(t, 1, counts)
require.Len(t, titles, 1)
@@ -847,11 +908,15 @@ func testListSoftwareTitlesInstallersOnly(t *testing.T, ds *Datastore) {
require.True(t, titles[0].CountsUpdatedAt.IsZero())
// vulnerable only returns nothing
titles, counts, _, err = ds.ListSoftwareTitles(ctx, fleet.SoftwareTitleListOptions{ListOptions: fleet.ListOptions{
OrderKey: "name",
OrderDirection: fleet.OrderAscending,
MatchQuery: "installer1",
}, VulnerableOnly: true}, fleet.TeamFilter{User: &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)}})
titles, counts, _, err = ds.ListSoftwareTitles(ctx, fleet.SoftwareTitleListOptions{
ListOptions: fleet.ListOptions{
OrderKey: "name",
OrderDirection: fleet.OrderAscending,
MatchQuery: "installer1",
},
TeamID: ptr.Uint(0),
VulnerableOnly: true,
}, fleet.TeamFilter{User: &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)}})
require.NoError(t, err)
require.EqualValues(t, 0, counts)
require.Len(t, titles, 0)
@@ -865,6 +930,7 @@ func testListSoftwareTitlesInstallersOnly(t *testing.T, ds *Datastore) {
OrderDirection: fleet.OrderAscending,
},
AvailableForInstall: true,
TeamID: ptr.Uint(0),
},
fleet.TeamFilter{User: &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)}},
)
@@ -877,7 +943,7 @@ func testListSoftwareTitlesInstallersOnly(t *testing.T, ds *Datastore) {
func testListSoftwareTitlesAvailableForInstallFilter(t *testing.T, ds *Datastore) {
ctx := context.Background()
// create a 2 software installers
// create 2 software installers
installer1, err := ds.MatchOrCreateSoftwareInstaller(ctx, &fleet.UploadSoftwareInstallerPayload{
Title: "installer1",
Source: "apps",
@@ -896,17 +962,25 @@ func testListSoftwareTitlesAvailableForInstallFilter(t *testing.T, ds *Datastore
require.NotZero(t, installer2)
// create a 4 VPP apps
_, err = ds.InsertVPPAppWithTeam(ctx, &fleet.VPPApp{Name: "vpp1", BundleIdentifier: "com.example.vpp1",
VPPAppID: fleet.VPPAppID{AdamID: "adam_vpp_app_1", Platform: fleet.MacOSPlatform}}, nil)
_, err = ds.InsertVPPAppWithTeam(ctx, &fleet.VPPApp{
Name: "vpp1", BundleIdentifier: "com.example.vpp1",
VPPAppID: fleet.VPPAppID{AdamID: "adam_vpp_app_1", Platform: fleet.MacOSPlatform},
}, nil)
require.NoError(t, err)
_, err = ds.InsertVPPAppWithTeam(ctx, &fleet.VPPApp{Name: "vpp2", BundleIdentifier: "com.example.vpp2",
VPPAppID: fleet.VPPAppID{AdamID: "adam_vpp_app_2", Platform: fleet.IPadOSPlatform}}, nil)
_, err = ds.InsertVPPAppWithTeam(ctx, &fleet.VPPApp{
Name: "vpp2", BundleIdentifier: "com.example.vpp2",
VPPAppID: fleet.VPPAppID{AdamID: "adam_vpp_app_2", Platform: fleet.IPadOSPlatform},
}, nil)
require.NoError(t, err)
_, err = ds.InsertVPPAppWithTeam(ctx, &fleet.VPPApp{Name: "vpp2", BundleIdentifier: "com.example.vpp2",
VPPAppID: fleet.VPPAppID{AdamID: "adam_vpp_app_2", Platform: fleet.MacOSPlatform}}, nil)
_, err = ds.InsertVPPAppWithTeam(ctx, &fleet.VPPApp{
Name: "vpp2", BundleIdentifier: "com.example.vpp2",
VPPAppID: fleet.VPPAppID{AdamID: "adam_vpp_app_2", Platform: fleet.MacOSPlatform},
}, nil)
require.NoError(t, err)
_, err = ds.InsertVPPAppWithTeam(ctx, &fleet.VPPApp{Name: "vpp2", BundleIdentifier: "com.example.vpp2",
VPPAppID: fleet.VPPAppID{AdamID: "adam_vpp_app_2", Platform: fleet.IOSPlatform}}, nil)
_, err = ds.InsertVPPAppWithTeam(ctx, &fleet.VPPApp{
Name: "vpp2", BundleIdentifier: "com.example.vpp2",
VPPAppID: fleet.VPPAppID{AdamID: "adam_vpp_app_2", Platform: fleet.IOSPlatform},
}, nil)
require.NoError(t, err)
host := test.NewHost(t, ds, "host", "", "hostkey", "hostuuid", time.Now())
@@ -929,6 +1003,7 @@ func testListSoftwareTitlesAvailableForInstallFilter(t *testing.T, ds *Datastore
OrderKey: "name",
OrderDirection: fleet.OrderAscending,
},
TeamID: ptr.Uint(0),
},
fleet.TeamFilter{User: &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)}},
)
@@ -963,6 +1038,7 @@ func testListSoftwareTitlesAvailableForInstallFilter(t *testing.T, ds *Datastore
OrderDirection: fleet.OrderAscending,
},
AvailableForInstall: true,
TeamID: ptr.Uint(0),
},
fleet.TeamFilter{User: &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)}},
)
@@ -984,6 +1060,191 @@ func testListSoftwareTitlesAvailableForInstallFilter(t *testing.T, ds *Datastore
}, names)
}
func testListSoftwareTitlesAllTeams(t *testing.T, ds *Datastore) {
ctx := context.Background()
team1, err := ds.NewTeam(ctx, &fleet.Team{Name: "team1"})
require.NoError(t, err)
team2, err := ds.NewTeam(ctx, &fleet.Team{Name: "team2"})
require.NoError(t, err)
// Create a macOS software foobar installer on "No team".
macOSInstallerNoTeam, err := ds.MatchOrCreateSoftwareInstaller(ctx, &fleet.UploadSoftwareInstallerPayload{
Title: "foobar",
BundleIdentifier: "com.foo.bar",
Source: "apps",
InstallScript: "echo",
Filename: "foobar.pkg",
TeamID: nil,
})
require.NoError(t, err)
// Create an iOS Canva installer on "team1".
require.NotZero(t, macOSInstallerNoTeam)
_, err = ds.InsertVPPAppWithTeam(ctx, &fleet.VPPApp{
Name: "Canva", BundleIdentifier: "com.example.canva",
VPPAppID: fleet.VPPAppID{AdamID: "adam_vpp_app_canva", Platform: fleet.IOSPlatform},
}, &team1.ID)
require.NoError(t, err)
// Create a macOS Canva installer on "team1".
require.NotZero(t, macOSInstallerNoTeam)
_, err = ds.InsertVPPAppWithTeam(ctx, &fleet.VPPApp{
Name: "Canva", BundleIdentifier: "com.example.canva",
VPPAppID: fleet.VPPAppID{AdamID: "adam_vpp_app_canva", Platform: fleet.MacOSPlatform},
}, &team1.ID)
require.NoError(t, err)
// Create an iPadOS Canva installer on "team2".
require.NotZero(t, macOSInstallerNoTeam)
_, err = ds.InsertVPPAppWithTeam(ctx, &fleet.VPPApp{
Name: "Canva", BundleIdentifier: "com.example.canva",
VPPAppID: fleet.VPPAppID{AdamID: "adam_vpp_app_canva", Platform: fleet.IPadOSPlatform},
}, &team2.ID)
require.NoError(t, err)
// Add a macOS host on "No team" with some software.
host := test.NewHost(t, ds, "host", "", "hostkey", "hostuuid", time.Now())
software := []fleet.Software{
{Name: "foo", Version: "0.0.1", Source: "chrome_extensions"},
{Name: "foo", Version: "0.0.3", Source: "chrome_extensions"},
{Name: "bar", Version: "0.0.3", Source: "deb_packages"},
}
_, err = ds.UpdateHostSoftware(ctx, host.ID, software)
require.NoError(t, err)
// Simulate vulnerabilities cron
require.NoError(t, ds.SyncHostsSoftware(ctx, time.Now()))
require.NoError(t, ds.ReconcileSoftwareTitles(ctx))
require.NoError(t, ds.SyncHostsSoftwareTitles(ctx, time.Now()))
// List software titles for "All teams", should only return the host software titles
// and no installers/VPP-apps because none is installed yet.
titles, counts, _, err := ds.ListSoftwareTitles(
ctx,
fleet.SoftwareTitleListOptions{
ListOptions: fleet.ListOptions{
OrderKey: "name",
OrderDirection: fleet.OrderAscending,
},
TeamID: nil,
},
fleet.TeamFilter{User: &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)}},
)
require.NoError(t, err)
assert.EqualValues(t, 2, counts)
assert.Len(t, titles, 2)
type nameSource struct {
name string
source string
}
names := make([]nameSource, 0, len(titles))
for _, title := range titles {
names = append(names, nameSource{name: title.Name, source: title.Source})
}
assert.ElementsMatch(t, []nameSource{
{name: "bar", source: "deb_packages"},
{name: "foo", source: "chrome_extensions"},
}, names)
// List software for "No team". Should list the host's software + the macOS installer.
titles, counts, _, err = ds.ListSoftwareTitles(
ctx,
fleet.SoftwareTitleListOptions{
ListOptions: fleet.ListOptions{
OrderKey: "name",
OrderDirection: fleet.OrderAscending,
},
TeamID: ptr.Uint(0),
},
fleet.TeamFilter{User: &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)}},
)
require.NoError(t, err)
assert.EqualValues(t, 3, counts)
assert.Len(t, titles, 3)
names = make([]nameSource, 0, len(titles))
for _, title := range titles {
names = append(names, nameSource{name: title.Name, source: title.Source})
}
assert.ElementsMatch(t, []nameSource{
{name: "bar", source: "deb_packages"},
{name: "foo", source: "chrome_extensions"},
{name: "foobar", source: "apps"},
}, names)
// List software for "team1". Should list Canva for iOS and macOS.
titles, counts, _, err = ds.ListSoftwareTitles(
ctx,
fleet.SoftwareTitleListOptions{
ListOptions: fleet.ListOptions{
OrderKey: "name",
OrderDirection: fleet.OrderAscending,
},
TeamID: &team1.ID,
},
fleet.TeamFilter{User: &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)}},
)
require.NoError(t, err)
assert.EqualValues(t, 2, counts)
assert.Len(t, titles, 2)
names = make([]nameSource, 0, len(titles))
for _, title := range titles {
names = append(names, nameSource{name: title.Name, source: title.Source})
}
assert.ElementsMatch(t, []nameSource{
{name: "Canva", source: "ios_apps"},
{name: "Canva", source: "apps"},
}, names)
// List software for "team2". Should list Canva for iPadOS.
titles, counts, _, err = ds.ListSoftwareTitles(
ctx,
fleet.SoftwareTitleListOptions{
ListOptions: fleet.ListOptions{
OrderKey: "name",
OrderDirection: fleet.OrderAscending,
},
TeamID: &team2.ID,
},
fleet.TeamFilter{User: &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)}},
)
require.NoError(t, err)
assert.EqualValues(t, 1, counts)
assert.Len(t, titles, 1)
names = make([]nameSource, 0, len(titles))
for _, title := range titles {
names = append(names, nameSource{name: title.Name, source: title.Source})
}
assert.ElementsMatch(t, []nameSource{
{name: "Canva", source: "ipados_apps"},
}, names)
// List software available for install on "No team". Should list "foobar" package only.
titles, counts, _, err = ds.ListSoftwareTitles(
ctx,
fleet.SoftwareTitleListOptions{
ListOptions: fleet.ListOptions{
OrderKey: "name",
OrderDirection: fleet.OrderAscending,
},
AvailableForInstall: true,
TeamID: ptr.Uint(0),
},
fleet.TeamFilter{User: &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)}},
)
require.NoError(t, err)
require.EqualValues(t, 1, counts)
require.Len(t, titles, 1)
names = make([]nameSource, 0, len(titles))
for _, title := range titles {
names = append(names, nameSource{name: title.Name, source: title.Source})
}
assert.ElementsMatch(t, []nameSource{
{name: "foobar", source: "apps"},
}, names)
}
func testUploadedSoftwareExists(t *testing.T, ds *Datastore) {
ctx := context.Background()
@@ -8299,7 +8299,7 @@ func (s *integrationEnterpriseTestSuite) TestAllSoftwareTitles() {
require.NotNil(t, resp.SoftwareTitles[0].SoftwarePackage.SelfService)
require.True(t, *resp.SoftwareTitles[0].SoftwarePackage.SelfService)
// no team but self-service returns the emacs software (technically impossible via the UI)
// "All teams" returns no software because the self-service software it's not installed (host_counts == 0).
resp = listSoftwareTitlesResponse{}
s.DoJSON(
"GET", "/api/latest/fleet/software/titles",
@@ -8308,15 +8308,9 @@ func (s *integrationEnterpriseTestSuite) TestAllSoftwareTitles() {
"self_service", "true",
)
require.Len(t, resp.SoftwareTitles, 2)
require.NotNil(t, resp.SoftwareTitles[0].SoftwarePackage)
require.NotNil(t, resp.SoftwareTitles[0].SoftwarePackage.SelfService)
require.True(t, *resp.SoftwareTitles[0].SoftwarePackage.SelfService)
require.NotNil(t, resp.SoftwareTitles[1].SoftwarePackage)
require.NotNil(t, resp.SoftwareTitles[1].SoftwarePackage.SelfService)
require.True(t, *resp.SoftwareTitles[1].SoftwarePackage.SelfService)
require.Empty(t, resp.SoftwareTitles, 0)
// team 0 returns the emacs software
// "No team" returns the emacs software
resp = listSoftwareTitlesResponse{}
s.DoJSON(
"GET", "/api/latest/fleet/software/titles",