diff --git a/changes/37802-fix-windows-fma-list b/changes/37802-fix-windows-fma-list new file mode 100644 index 0000000000..6858134b40 --- /dev/null +++ b/changes/37802-fix-windows-fma-list @@ -0,0 +1 @@ +- Fixed a bug where Fleet maintained apps for Windows won't show as available in the list when they actually are. diff --git a/server/datastore/mysql/maintained_apps.go b/server/datastore/mysql/maintained_apps.go index 493277dc4b..c1754a4672 100644 --- a/server/datastore/mysql/maintained_apps.go +++ b/server/datastore/mysql/maintained_apps.go @@ -47,7 +47,7 @@ ON DUPLICATE KEY UPDATE const teamFMATitlesJoin = ` team_titles.id software_title_id FROM fleet_maintained_apps fma LEFT JOIN ( - SELECT DISTINCT st.id, st.unique_identifier + SELECT DISTINCT st.id, st.unique_identifier, st.name, si.platform FROM software_titles st LEFT JOIN software_installers si @@ -63,7 +63,16 @@ const teamFMATitlesJoin = ` AND vat.platform = va.platform AND vat.global_or_team_id = ? WHERE si.id IS NOT NULL OR vat.id IS NOT NULL - ) team_titles ON team_titles.unique_identifier = fma.unique_identifier` + ) team_titles + ON team_titles.unique_identifier = fma.unique_identifier + -- pattern match fma name to a similar title name, since upgrade_code is not surfaced in fma table + OR ( + team_titles.platform = fma.platform + AND fma.platform = 'windows' + -- Box Drive is the only FMA at the point of writing this where unique_identifier is shorter than name + AND team_titles.name LIKE CONCAT(LEAST(fma.name, fma.unique_identifier), '%') + ) +` func (ds *Datastore) GetMaintainedAppByID(ctx context.Context, appID uint, teamID *uint) (*fleet.MaintainedApp, error) { stmt := `SELECT fma.id, fma.name, fma.platform, fma.unique_identifier, fma.slug, ` diff --git a/server/datastore/mysql/maintained_apps_test.go b/server/datastore/mysql/maintained_apps_test.go index 4ade51c2c0..80048873bb 100644 --- a/server/datastore/mysql/maintained_apps_test.go +++ b/server/datastore/mysql/maintained_apps_test.go @@ -24,6 +24,7 @@ func TestMaintainedApps(t *testing.T) { {"ListAndGetAvailableApps", testListAndGetAvailableApps}, {"SyncAndRemoveApps", testSyncAndRemoveApps}, {"GetMaintainedAppBySlug", testGetMaintainedAppBySlug}, + {"ListAvailableAppsWindows", testListAvailableAppsWindows}, } for _, c := range cases { @@ -542,3 +543,89 @@ func testGetMaintainedAppBySlug(t *testing.T, ds *Datastore) { TitleID: nil, }, gotApp) } + +func testListAvailableAppsWindows(t *testing.T, ds *Datastore) { + ctx := context.Background() + + team1, err := ds.NewTeam(ctx, &fleet.Team{Name: "Team 1"}) + require.NoError(t, err) + user := test.NewUser(t, ds, "Alice", "alice@example.com", true) + + maintained1, err := ds.UpsertMaintainedApp(ctx, &fleet.MaintainedApp{ + Name: "Maintained1", + Slug: "maintained1", + Platform: "windows", + UniqueIdentifier: "Maintained1 (MSI)", + }) + require.NoError(t, err) + maintained2, err := ds.UpsertMaintainedApp(ctx, &fleet.MaintainedApp{ + Name: "Maintained2", + Slug: "maintained2", + Platform: "darwin", + UniqueIdentifier: "com.foo", + }) + require.NoError(t, err) + + expectedApps := []fleet.MaintainedApp{ + { + ID: maintained1.ID, + Name: maintained1.Name, + Platform: maintained1.Platform, + Slug: "maintained1", + }, + { + ID: maintained2.ID, + Name: maintained2.Name, + Platform: maintained2.Platform, + Slug: "maintained2", + }, + } + apps, _, err := ds.ListAvailableFleetMaintainedApps(ctx, &team1.ID, fleet.ListOptions{IncludeMetadata: true}) + require.NoError(t, err) + require.Len(t, apps, 2) + require.Nil(t, apps[0].TitleID) + require.Nil(t, apps[1].TitleID) + require.Equal(t, expectedApps, apps) + + // upload an installer that will create a title with a similar name, but with + // an upgrade code so that unique identifier doesn't match + _, titleID, err := ds.MatchOrCreateSoftwareInstaller(ctx, &fleet.UploadSoftwareInstallerPayload{ + Title: "Maintained1 (MSI)", + UpgradeCode: "{UPGRADE-CODE}", + Source: "programs", + StorageID: "storageid1", + Filename: "maintained1.msi", + Extension: "msi", + Platform: "windows", + Version: "1.0", + UserID: user.ID, + TeamID: &team1.ID, + ValidatedLabels: &fleet.LabelIdentsWithScope{}, + FleetMaintainedAppID: ptr.Uint(maintained1.ID), + }) + require.NoError(t, err) + // create a pkg installer that should not match by similar name + _, _, err = ds.MatchOrCreateSoftwareInstaller(ctx, &fleet.UploadSoftwareInstallerPayload{ + Title: "Maintained2 ", + BundleIdentifier: "Maintained2.ShallNotBeMatched", + Source: "apps", + StorageID: "storageid2", + Filename: "maintained2.pkg", + Extension: "pkg", + Platform: "darwin", + Version: "1.0", + UserID: user.ID, + TeamID: &team1.ID, + ValidatedLabels: &fleet.LabelIdentsWithScope{}, + }) + require.NoError(t, err) + + // the windows app should be found using using name, because the existing software title has an upgrade code + apps, _, err = ds.ListAvailableFleetMaintainedApps(ctx, &team1.ID, fleet.ListOptions{IncludeMetadata: true}) + require.NoError(t, err) + require.Len(t, apps, 2) + require.NotNil(t, apps[0].TitleID) + require.Equal(t, titleID, *apps[0].TitleID) + // the darwin app should not be matched by name + require.Nil(t, apps[1].TitleID) +}