Check for duplicate linux software installers (#44234)

<!-- Add the related story/sub-task/bug number, like Resolves #123, or
remove if NA -->
**Related issue:** Resolves #43959 #44038
Refactored `checkSoftwareConflictsByIdentifier` to a switch statement
with different logic per platform


# Checklist for submitter

If some of the following don't apply, delete the relevant line.

- [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/guides/committing-changes.md#changes-files)
for more information.

- [ ] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements), JS
inline code is prevented especially for url redirects, and untrusted
data interpolated into shell scripts/commands is validated against shell
metacharacters.
- [ ] Timeouts are implemented and retries are limited to avoid infinite
loops
- [ ] If paths of existing endpoints are modified without backwards
compatibility, checked the frontend/CLI for any necessary changes

## Testing

- [x] Added/updated automated tests
- [ ] Where appropriate, [automated tests simulate multiple hosts and
test for host
isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing)
(updates to one hosts's records do not affect another)

- [x] QA'd all new/changed functionality manually

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

## Bug Fixes

- Prevented duplicate software installer entries on Linux.
- Improved conflict detection for software installers across iOS, macOS,
Windows, and Linux platforms to prevent incompatible uploads.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Jonathan Katz
2026-04-27 17:14:47 -04:00
committed by GitHub
parent 2d72337212
commit 899dc5aa57
5 changed files with 315 additions and 47 deletions
+1
View File
@@ -0,0 +1 @@
- Fixed a bug where duplicate software installers for linux could be added.
+22
View File
@@ -1548,6 +1548,28 @@ WHERE
return exists == 1, nil
}
func (ds *Datastore) checkInstallerExistsByName(ctx context.Context, q sqlx.QueryerContext, teamID *uint, name, source, platform string) (bool, error) {
const stmt = `
SELECT 1
FROM
software_titles st
INNER JOIN software_installers ON st.id = software_installers.title_id
AND software_installers.global_or_team_id = ?
WHERE
st.name = ?
AND st.source = ?
AND st.extension_for = ''
AND software_installers.platform = ?
`
var exists int
err := sqlx.GetContext(ctx, q, &exists, stmt, ptr.ValOrZero(teamID), name, source, platform)
if err != nil && !errors.Is(err, sql.ErrNoRows) {
return false, ctxerr.Wrap(ctx, err, "check installer exists by name")
}
return exists == 1, nil
}
func (ds *Datastore) checkInHouseAppExistsForAdamID(ctx context.Context, q sqlx.QueryerContext, teamID *uint, appID fleet.VPPAppID) (exists bool, title string, err error) {
const stmt = `
SELECT st.name
+39 -44
View File
@@ -3699,68 +3699,63 @@ LIMIT 1`
}
func (ds *Datastore) checkSoftwareConflictsByIdentifier(ctx context.Context, payload *fleet.UploadSoftwareInstallerPayload) error {
// if this is an in-house app, check if an installer exists
if payload.Extension == "ipa" {
switch payload.Platform {
// currently, the platform will always be ios for .ipa files
case string(fleet.IOSPlatform), string(fleet.IPadOSPlatform):
// at the point where this method is called, we attempt to create both iOS and iPadOS entries
// for ipa apps, so check for conflicts on either platform.
for platform, source := range map[string]string{
string(fleet.IOSPlatform): "ios_apps",
string(fleet.IPadOSPlatform): "ipados_apps",
} {
exists, err := ds.checkInstallerOrInHouseAppExists(ctx, ds.reader(ctx), payload.TeamID, payload.BundleIdentifier, platform, softwareTypeInstaller)
exists, err := ds.checkVPPAppExistsForTitleIdentifier(ctx, ds.reader(ctx), payload.TeamID, platform, payload.BundleIdentifier, source, "")
if err != nil {
return ctxerr.Wrap(ctx, err, "check if VPP app exists for title identifier")
}
if exists {
return alreadyExists("VPP app", payload.Title)
}
// check if equivalent installers exist, duplicate in-house apps are checked in insertInHouseApp
exists, err = ds.checkInstallerOrInHouseAppExists(ctx, ds.reader(ctx), payload.TeamID, payload.BundleIdentifier, platform, softwareTypeInstaller)
if err != nil {
return ctxerr.Wrap(ctx, err, "check if software installer exists for title identifier")
}
if exists {
return alreadyExists("software installer", payload.Title)
}
exists, err = ds.checkVPPAppExistsForTitleIdentifier(ctx, ds.reader(ctx), payload.TeamID, platform, payload.BundleIdentifier, source, "")
if err != nil {
return ctxerr.Wrap(ctx, err, "check if VPP app exists for title identifier")
}
if exists {
return alreadyExists("VPP app", payload.Title)
}
}
} else {
// check if a VPP app already exists for that software title in the same
// platform and team.
if payload.Platform == string(fleet.MacOSPlatform) || payload.Platform == string(fleet.IOSPlatform) || payload.Platform == string(fleet.IPadOSPlatform) {
exists, err := ds.checkVPPAppExistsForTitleIdentifier(ctx, ds.reader(ctx), payload.TeamID, payload.Platform, payload.BundleIdentifier, payload.Source, "")
if err != nil {
return ctxerr.Wrap(ctx, err, "check if VPP app exists for title identifier")
}
if exists {
return alreadyExists("VPP app", payload.Title)
}
case string(fleet.MacOSPlatform):
exists, err := ds.checkVPPAppExistsForTitleIdentifier(ctx, ds.reader(ctx), payload.TeamID, payload.Platform, payload.BundleIdentifier, payload.Source, "")
if err != nil {
return ctxerr.Wrap(ctx, err, "check if VPP app exists for title identifier")
}
if exists {
return alreadyExists("VPP app", payload.Title)
}
// Check if an in-house app with the same bundle id already exists.
// Also check if equivalent installers exist, since we relaxed the uniqueness constraints to allow
// multiple FMA installer versions.
if payload.BundleIdentifier != "" {
exists, err := ds.checkInstallerOrInHouseAppExists(ctx, ds.reader(ctx), payload.TeamID, payload.BundleIdentifier, payload.Platform, softwareTypeInHouseApp)
if err != nil {
return ctxerr.Wrap(ctx, err, "check if in-house app exists for title identifier")
}
if exists {
return alreadyExists("in-house app", payload.Title)
}
exists, err = ds.checkInstallerOrInHouseAppExists(ctx, ds.reader(ctx), payload.TeamID, payload.BundleIdentifier, payload.Platform, softwareTypeInstaller)
if err != nil {
return ctxerr.Wrap(ctx, err, "check if installer exists for title identifier")
}
if exists {
return alreadyExists("installer", payload.Title)
}
// check only for installers, since in-house apps target iOS/iPadOS so they won't conflict
exists, err = ds.checkInstallerOrInHouseAppExists(ctx, ds.reader(ctx), payload.TeamID, payload.BundleIdentifier, payload.Platform, softwareTypeInstaller)
if err != nil {
return ctxerr.Wrap(ctx, err, "check if installer exists for title identifier")
}
if exists {
return alreadyExists("installer", payload.Title)
}
case "windows", "linux":
// check by name before any software title renaming side effects can happen
exists, err := ds.checkInstallerExistsByName(ctx, ds.reader(ctx), payload.TeamID, payload.Title, payload.Source, payload.Platform)
if err != nil {
return ctxerr.Wrap(ctx, err, "check if installer exists by name")
}
if exists {
return alreadyExists("installer", payload.Title)
}
if payload.Platform == "windows" {
exists, err := ds.checkInstallerOrInHouseAppExists(ctx, ds.reader(ctx), payload.TeamID, payload.Title, payload.Platform, softwareTypeInstaller)
if payload.UpgradeCode != "" {
exists, err := ds.checkInstallerOrInHouseAppExists(ctx, ds.reader(ctx), payload.TeamID, payload.UpgradeCode, payload.Platform, softwareTypeInstaller)
if err != nil {
return ctxerr.Wrap(ctx, err, "check if installer exists for title identifier")
return ctxerr.Wrap(ctx, err, "check if installer exists for upgrade code")
}
if exists {
return alreadyExists("installer", payload.Title)
@@ -61,6 +61,7 @@ func TestSoftwareInstallers(t *testing.T) {
{"CustomToFMAInstallerReplacement", testCustomToFMAInstallerReplacement},
{"GetInstallerByTeamAndURL", testGetInstallerByTeamAndURL},
{"BatchSetFMACancelsPendingOnActiveRow", testBatchSetFMACancelsPendingOnActiveRow},
{"MatchOrCreateSoftwareInstallerDuplicateConflicts", testMatchOrCreateSoftwareInstallerDuplicateConflicts},
}
for _, c := range cases {
@@ -5048,3 +5049,252 @@ func testBatchSetFMACancelsPendingOnActiveRow(t *testing.T, ds *Datastore) {
})
require.Zero(t, pending, "re-submitting the active FMA version must cancel its pending installs")
}
func testMatchOrCreateSoftwareInstallerDuplicateConflicts(t *testing.T, ds *Datastore) {
ctx := context.Background()
user := test.NewUser(t, ds, "Alice", "alice@example.com", true)
team, err := ds.NewTeam(ctx, &fleet.Team{Name: t.Name()})
require.NoError(t, err)
const conflictMsg = "already has an installer available for"
// macOS installer conflicting with a VPP app on the same bundle id.
test.CreateInsertGlobalVPPToken(t, ds)
_, err = ds.InsertVPPAppWithTeam(ctx, &fleet.VPPApp{
VPPAppTeam: fleet.VPPAppTeam{VPPAppID: fleet.VPPAppID{AdamID: "adam_vpp_mac", Platform: fleet.MacOSPlatform}},
Name: "Mac VPP",
BundleIdentifier: "com.example.vpp",
}, &team.ID)
require.NoError(t, err)
_, _, err = ds.MatchOrCreateSoftwareInstaller(ctx, &fleet.UploadSoftwareInstallerPayload{
StorageID: "mac-vpp-clash-storage",
Filename: "vpp-clash.pkg",
Title: "Mac VPP Clash",
BundleIdentifier: "com.example.vpp",
Extension: "pkg",
Source: "apps",
Platform: "darwin",
Version: "1.0",
UserID: user.ID,
ValidatedLabels: &fleet.LabelIdentsWithScope{},
TeamID: &team.ID,
})
require.ErrorContains(t, err, conflictMsg)
// macOS installer sharing a bundle id with an in-house app is allowed:
// in-house apps only target iOS/iPadOS, so they don't conflict with macOS.
_, _, err = ds.MatchOrCreateSoftwareInstaller(ctx, &fleet.UploadSoftwareInstallerPayload{
StorageID: "iha-storage",
Filename: "iha.ipa",
Title: "iOS App",
BundleIdentifier: "com.example.iha",
Extension: "ipa",
Source: "ios_apps",
Platform: "ios",
Version: "1.0",
UserID: user.ID,
ValidatedLabels: &fleet.LabelIdentsWithScope{},
TeamID: &team.ID,
})
require.NoError(t, err)
_, _, err = ds.MatchOrCreateSoftwareInstaller(ctx, &fleet.UploadSoftwareInstallerPayload{
StorageID: "mac-iha-coexist-storage",
Filename: "mac-iha-coexist.pkg",
Title: "Mac IHA Coexist",
BundleIdentifier: "com.example.iha",
Extension: "pkg",
Source: "apps",
Platform: "darwin",
Version: "1.0",
UserID: user.ID,
ValidatedLabels: &fleet.LabelIdentsWithScope{},
TeamID: &team.ID,
})
require.NoError(t, err)
// macOS installer conflicting with the same installer at a newer version.
_, _, err = ds.MatchOrCreateSoftwareInstaller(ctx, &fleet.UploadSoftwareInstallerPayload{
StorageID: "mac-base-storage",
Filename: "mac-app.pkg",
Title: "Mac App",
BundleIdentifier: "com.example.mac",
Extension: "pkg",
Source: "apps",
Platform: "darwin",
Version: "1.0",
UserID: user.ID,
ValidatedLabels: &fleet.LabelIdentsWithScope{},
TeamID: &team.ID,
})
require.NoError(t, err)
_, _, err = ds.MatchOrCreateSoftwareInstaller(ctx, &fleet.UploadSoftwareInstallerPayload{
StorageID: "mac-v2-storage",
Filename: "mac-app-v2.pkg",
Title: "Mac App",
BundleIdentifier: "com.example.mac",
Extension: "pkg",
Source: "apps",
Platform: "darwin",
Version: "2.0",
UserID: user.ID,
ValidatedLabels: &fleet.LabelIdentsWithScope{},
TeamID: &team.ID,
})
require.ErrorContains(t, err, conflictMsg)
// Windows installer conflicting with the same Title at a newer version.
_, _, err = ds.MatchOrCreateSoftwareInstaller(ctx, &fleet.UploadSoftwareInstallerPayload{
StorageID: "win-base-storage",
Filename: "win-app.msi",
Title: "Win App",
Extension: "msi",
Source: "programs",
Platform: "windows",
Version: "1.0",
UserID: user.ID,
ValidatedLabels: &fleet.LabelIdentsWithScope{},
TeamID: &team.ID,
})
require.NoError(t, err)
_, _, err = ds.MatchOrCreateSoftwareInstaller(ctx, &fleet.UploadSoftwareInstallerPayload{
StorageID: "win-v2-storage",
Filename: "win-app-v2.msi",
Title: "Win App",
Extension: "msi",
Source: "programs",
Platform: "windows",
Version: "2.0",
UserID: user.ID,
ValidatedLabels: &fleet.LabelIdentsWithScope{},
TeamID: &team.ID,
})
require.ErrorContains(t, err, conflictMsg)
// Windows installer conflicting on the upgrade code with a different Title.
const winUpgradeCode = "{ABCDEF12-3456-7890-ABCD-EF1234567890}"
_, _, err = ds.MatchOrCreateSoftwareInstaller(ctx, &fleet.UploadSoftwareInstallerPayload{
StorageID: "win-uc-base-storage",
Filename: "win-uc.msi",
Title: "Win UC App",
Extension: "msi",
Source: "programs",
Platform: "windows",
Version: "1.0",
UpgradeCode: winUpgradeCode,
UserID: user.ID,
ValidatedLabels: &fleet.LabelIdentsWithScope{},
TeamID: &team.ID,
})
require.NoError(t, err)
_, _, err = ds.MatchOrCreateSoftwareInstaller(ctx, &fleet.UploadSoftwareInstallerPayload{
StorageID: "win-uc-v2-storage",
Filename: "win-uc-other.msi",
Title: "Win UC App Renamed",
Extension: "msi",
Source: "programs",
Platform: "windows",
Version: "2.0",
UpgradeCode: winUpgradeCode,
UserID: user.ID,
ValidatedLabels: &fleet.LabelIdentsWithScope{},
TeamID: &team.ID,
})
require.ErrorContains(t, err, conflictMsg)
// Windows: existing installer has an upgrade code, new upload has the same
// Title but no upgrade code.
_, _, err = ds.MatchOrCreateSoftwareInstaller(ctx, &fleet.UploadSoftwareInstallerPayload{
StorageID: "win-uc-existing-storage",
Filename: "win-uc-existing.msi",
Title: "Win UC Same Name",
Extension: "msi",
Source: "programs",
Platform: "windows",
Version: "1.0",
UpgradeCode: "{11111111-1111-1111-1111-111111111111}",
UserID: user.ID,
ValidatedLabels: &fleet.LabelIdentsWithScope{},
TeamID: &team.ID,
})
require.NoError(t, err)
_, _, err = ds.MatchOrCreateSoftwareInstaller(ctx, &fleet.UploadSoftwareInstallerPayload{
StorageID: "win-uc-noupgrade-storage",
Filename: "win-uc-custom.msi",
Title: "Win UC Same Name",
Extension: "msi",
Source: "programs",
Platform: "windows",
Version: "2.0",
UserID: user.ID,
ValidatedLabels: &fleet.LabelIdentsWithScope{},
TeamID: &team.ID,
})
require.ErrorContains(t, err, conflictMsg)
// Reverse: existing installer has no upgrade code, new upload has the same
// Title with an upgrade code.
_, _, err = ds.MatchOrCreateSoftwareInstaller(ctx, &fleet.UploadSoftwareInstallerPayload{
StorageID: "win-plain-base-storage",
Filename: "win-plain.msi",
Title: "Win Plain App",
Extension: "msi",
Source: "programs",
Platform: "windows",
Version: "1.0",
UserID: user.ID,
ValidatedLabels: &fleet.LabelIdentsWithScope{},
TeamID: &team.ID,
})
require.NoError(t, err)
_, _, err = ds.MatchOrCreateSoftwareInstaller(ctx, &fleet.UploadSoftwareInstallerPayload{
StorageID: "win-plain-uc-storage",
Filename: "win-plain-uc.msi",
Title: "Win Plain App",
Extension: "msi",
Source: "programs",
Platform: "windows",
Version: "2.0",
UpgradeCode: "{22222222-2222-2222-2222-222222222222}",
UserID: user.ID,
ValidatedLabels: &fleet.LabelIdentsWithScope{},
TeamID: &team.ID,
})
require.ErrorContains(t, err, conflictMsg)
// Linux installer conflicting with the same Title at a newer version.
_, _, err = ds.MatchOrCreateSoftwareInstaller(ctx, &fleet.UploadSoftwareInstallerPayload{
StorageID: "linux-base-storage",
Filename: "linux-app.deb",
Title: "Linux App",
Extension: "deb",
Source: "deb_packages",
Platform: "linux",
Version: "1.0",
UserID: user.ID,
ValidatedLabels: &fleet.LabelIdentsWithScope{},
TeamID: &team.ID,
})
require.NoError(t, err)
_, _, err = ds.MatchOrCreateSoftwareInstaller(ctx, &fleet.UploadSoftwareInstallerPayload{
StorageID: "linux-v2-storage",
Filename: "linux-app-v2.deb",
Title: "Linux App",
Extension: "deb",
Source: "deb_packages",
Platform: "linux",
Version: "2.0",
UserID: user.ID,
ValidatedLabels: &fleet.LabelIdentsWithScope{},
TeamID: &team.ID,
})
require.ErrorContains(t, err, conflictMsg)
}
@@ -12633,7 +12633,7 @@ func (s *integrationEnterpriseTestSuite) TestSoftwareInstallerUploadDownloadAndD
s.lastActivityMatches(fleet.ActivityTypeAddedSoftware{}.ActivityName(), activityData, 0)
// upload again fails
s.uploadSoftwareInstaller(t, payload, http.StatusConflict, "already exists")
s.uploadSoftwareInstaller(t, payload, http.StatusConflict, "already has an installer available")
// update should succeed
s.updateSoftwareInstaller(t, &fleet.UpdateSoftwareInstallerPayload{
@@ -12838,7 +12838,7 @@ func (s *integrationEnterpriseTestSuite) TestSoftwareInstallerUploadDownloadAndD
s.lastActivityOfTypeMatches(fleet.ActivityTypeAddedSoftware{}.ActivityName(), activityData, 0)
// upload again fails
s.uploadSoftwareInstaller(t, payload, http.StatusConflict, "already exists")
s.uploadSoftwareInstaller(t, payload, http.StatusConflict, "already has an installer available")
// download the installer
r := s.Do("GET", fmt.Sprintf("/api/latest/fleet/software/titles/%d/package?alt=media", titleID), nil, http.StatusOK, "team_id", fmt.Sprintf("%d", *payload.TeamID))
@@ -12951,7 +12951,7 @@ func (s *integrationEnterpriseTestSuite) TestSoftwareInstallerUploadDownloadAndD
fmt.Sprintf(`{"software_title": "ruby", "software_package": "ruby.deb", "team_name": null, "team_id": 0, "fleet_name": null, "fleet_id": 0, "self_service": true, "software_title_id": %d}`, titleID), 0)
// upload again fails
s.uploadSoftwareInstaller(t, payload, http.StatusConflict, "already exists")
s.uploadSoftwareInstaller(t, payload, http.StatusConflict, "already has an installer available")
// download the installer
r := s.Do("GET", fmt.Sprintf("/api/latest/fleet/software/titles/%d/package?alt=media", titleID), nil, http.StatusOK, "team_id", fmt.Sprintf("%d", 0))