From fb4292ce08f0d67069cb7597b60da2d9695af37d Mon Sep 17 00:00:00 2001 From: Lucas Manuel Rodriguez Date: Tue, 6 Aug 2024 18:00:52 -0300 Subject: [PATCH] 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 --- server/datastore/mysql/software.go | 6 +- server/datastore/mysql/software_titles.go | 72 +-- .../datastore/mysql/software_titles_test.go | 415 ++++++++++++++---- server/service/integration_enterprise_test.go | 12 +- 4 files changed, 391 insertions(+), 114 deletions(-) diff --git a/server/datastore/mysql/software.go b/server/datastore/mysql/software.go index b91048a973..afc32d5610 100644 --- a/server/datastore/mysql/software.go +++ b/server/datastore/mysql/software.go @@ -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; diff --git a/server/datastore/mysql/software_titles.go b/server/datastore/mysql/software_titles.go index 2c214616dc..354d9f4c8b 100644 --- a/server/datastore/mysql/software_titles.go +++ b/server/datastore/mysql/software_titles.go @@ -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 } diff --git a/server/datastore/mysql/software_titles_test.go b/server/datastore/mysql/software_titles_test.go index 4bb20acfd8..0f8738d143 100644 --- a/server/datastore/mysql/software_titles_test.go +++ b/server/datastore/mysql/software_titles_test.go @@ -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() diff --git a/server/service/integration_enterprise_test.go b/server/service/integration_enterprise_test.go index 79e61b96d1..389cc4a039 100644 --- a/server/service/integration_enterprise_test.go +++ b/server/service/integration_enterprise_test.go @@ -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",