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
35 changes: 18 additions & 17 deletions ee/server/service/software_installers.go
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,24 @@ func (svc *Service) UpdateSoftwareInstaller(ctx context.Context, payload *fleet.
return nil, ctxerr.Wrap(ctx, err, "getting software title by id")
}

dirty := make(map[string]bool)

payload.Categories = server.RemoveDuplicatesFromSlice(payload.Categories)
catIDs, err := svc.ds.GetSoftwareCategoryIDs(ctx, payload.Categories)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "getting software category ids")
}

if len(catIDs) != len(payload.Categories) {
return nil, &fleet.BadRequestError{
Message: "some or all of the categories provided don't exist",
InternalErr: fmt.Errorf("categories provided: %v", payload.Categories),
}
}

payload.CategoryIDs = catIDs
dirty["Categories"] = true

// Handle in house apps separately
if software.InHouseAppCount == 1 {
return svc.updateInHouseAppInstaller(ctx, payload, vc, teamName, software)
Expand All @@ -349,7 +367,6 @@ func (svc *Service) UpdateSoftwareInstaller(ctx context.Context, payload *fleet.
}

payload.InstallerID = existingInstaller.InstallerID
dirty := make(map[string]bool)

if software.DisplayName != payload.DisplayName {
dirty["DisplayName"] = true
Expand All @@ -368,22 +385,6 @@ func (svc *Service) UpdateSoftwareInstaller(ctx context.Context, payload *fleet.
}
payload.ValidatedLabels = validatedLabels

payload.Categories = server.RemoveDuplicatesFromSlice(payload.Categories)
catIDs, err := svc.ds.GetSoftwareCategoryIDs(ctx, payload.Categories)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "getting software category ids")
}

if len(catIDs) != len(payload.Categories) {
return nil, &fleet.BadRequestError{
Message: "some or all of the categories provided don't exist",
InternalErr: fmt.Errorf("categories provided: %v", payload.Categories),
}
}

payload.CategoryIDs = catIDs
dirty["Categories"] = true

// activity team ID must be null if no team, not zero
var actTeamID *uint
if payload.TeamID != nil && *payload.TeamID != 0 {
Expand Down
23 changes: 18 additions & 5 deletions server/datastore/mysql/in_house_apps.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,11 @@ func (ds *Datastore) insertInHouseApp(ctx context.Context, payload *fleet.InHous
err = ds.withRetryTxx(ctx, func(tx sqlx.ExtContext) error {
row := tx.QueryRowxContext(ctx, selectStmt, globalOrTeamID, payload.BundleID, payload.Filename)
if err := row.Scan(&count); err != nil {
return ctxerr.Wrap(ctx, err, "insertInHouseApp")
return err
}
if count > 0 {
// ios or ipados version of this installer exists
err = alreadyExists("insertInHouseApp", payload.Filename)
err = alreadyExists("In-house app", payload.Filename)
}

argsIos := []any{
Expand Down Expand Up @@ -76,12 +76,12 @@ func (ds *Datastore) insertInHouseApp(ctx context.Context, payload *fleet.InHous

_, err := ds.insertInHouseAppDB(ctx, tx, payload, argsIpad)
if err != nil {
return ctxerr.Wrap(ctx, err, "insertInHouseApp")
return err
}

installerID, err = ds.insertInHouseAppDB(ctx, tx, payload, argsIos)
if err != nil {
return ctxerr.Wrap(ctx, err, "insertInHouseApp")
return err
}

return nil
Expand Down Expand Up @@ -130,7 +130,7 @@ func (ds *Datastore) insertInHouseAppDB(ctx context.Context, tx sqlx.ExtContext,
res, err := tx.ExecContext(ctx, stmt, args...)
if err != nil {
if IsDuplicate(err) {
err = alreadyExists("insertInHouseAppDB", payload.Filename)
err = alreadyExists("In-house app", payload.Filename)
}
return 0, ctxerr.Wrap(ctx, err, "insertInHouseAppDB")
}
Expand All @@ -143,6 +143,13 @@ func (ds *Datastore) insertInHouseAppDB(ctx context.Context, tx sqlx.ExtContext,
if err := setOrUpdateSoftwareInstallerLabelsDB(ctx, tx, installerID, *payload.ValidatedLabels, softwareTypeInHouseApp); err != nil {
return 0, ctxerr.Wrap(ctx, err, "insertInHouseAppDB")
}

if payload.CategoryIDs != nil {
if err := setOrUpdateSoftwareInstallerCategoriesDB(ctx, tx, installerID, payload.CategoryIDs, softwareTypeInHouseApp); err != nil {
return 0, ctxerr.Wrap(ctx, err, "upsert in house apps categories")
}
}

return installerID, nil
}

Expand Down Expand Up @@ -281,6 +288,12 @@ func (ds *Datastore) SaveInHouseAppUpdates(ctx context.Context, payload *fleet.U
}
}

if payload.CategoryIDs != nil {
if err := setOrUpdateSoftwareInstallerCategoriesDB(ctx, tx, payload.InstallerID, payload.CategoryIDs, softwareTypeInHouseApp); err != nil {
return ctxerr.Wrap(ctx, err, "upsert in house app categories")
}
}

return nil
})
if err != nil {
Expand Down
92 changes: 92 additions & 0 deletions server/datastore/mysql/in_house_apps_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ func TestInHouseApps(t *testing.T) {
}{
{"TestInHouseAppsCrud", testInHouseAppsCrud},
{"MultipleTeams", testInHouseAppsMultipleTeams},
{"Categories", testInHouseAppsCategories},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
Expand Down Expand Up @@ -306,6 +307,97 @@ func testInHouseAppsMultipleTeams(t *testing.T, ds *Datastore) {

}

func testInHouseAppsCategories(t *testing.T, ds *Datastore) {
ctx := context.Background()

host1 := test.NewHost(t, ds, "host1", "1", "host1key", "host1uuid", time.Now())
user1 := test.NewUser(t, ds, "Alice", "alice@example.com", true)

team1, err := ds.NewTeam(ctx, &fleet.Team{Name: "team 1"})
require.NoError(t, err)
require.NoError(t, ds.AddHostsToTeam(ctx, fleet.NewAddHostsToTeamParams(&team1.ID, []uint{host1.ID})))

nanoEnroll(t, ds, host1, false)

payload1 := fleet.UploadSoftwareInstallerPayload{
TeamID: &team1.ID,
UserID: user1.ID,
BundleIdentifier: "com.foo",
Filename: "foo.ipa",
StorageID: "id1234",
Extension: "ipa",
SelfService: false,
ValidatedLabels: &fleet.LabelIdentsWithScope{},
CategoryIDs: []uint{1, 2},
}

// Software categories are missing from test schema
ExecAdhocSQL(t, ds, func(tx sqlx.ExtContext) error {
_, err := tx.ExecContext(ctx, `
INSERT INTO software_categories
VALUES (1,'Productivity'), (2,'Browsers'),(3,'Communication'),(4,'Developer tools')`)
return err
})

// Add installers for both teams
installerID, _, err := ds.MatchOrCreateSoftwareInstaller(ctx, &payload1)
require.NoError(t, err)

var count int
err = sqlx.GetContext(ctx, ds.reader(ctx), &count, `SELECT COUNT(id) FROM in_house_app_software_categories WHERE in_house_app_id = ?`, installerID)
require.NoError(t, err)
require.Equal(t, 2, count)

err = sqlx.GetContext(ctx, ds.reader(ctx), &count, `SELECT COUNT(id) FROM software_titles`)
require.NoError(t, err)
require.Equal(t, 2, count)

err = sqlx.GetContext(ctx, ds.reader(ctx), &count, `SELECT COUNT(id) FROM in_house_apps`)
require.NoError(t, err)
require.Equal(t, 2, count)

// Test with empty categories

payload2 := fleet.UploadSoftwareInstallerPayload{
TeamID: &team1.ID,
UserID: user1.ID,
BundleIdentifier: "com.bar",
Filename: "bar.ipa",
StorageID: "id5678",
Extension: "ipa",
ValidatedLabels: &fleet.LabelIdentsWithScope{},
CategoryIDs: nil, // empty slice should work the same
}

secondInstallerID, secondTitleID, err := ds.MatchOrCreateSoftwareInstaller(ctx, &payload2)
require.NoError(t, err)

// Check that this has no categories
err = sqlx.GetContext(ctx, ds.reader(ctx), &count, `SELECT COUNT(id) FROM in_house_app_software_categories WHERE in_house_app_id = ?`, secondInstallerID)
require.NoError(t, err)
require.Equal(t, 0, count)

// Update software categories
updatePayload := fleet.UpdateSoftwareInstallerPayload{
TeamID: payload2.TeamID,
TitleID: secondTitleID,
InstallerID: secondInstallerID,
Filename: payload2.Filename,
StorageID: payload2.StorageID,
ValidatedLabels: payload2.ValidatedLabels,
CategoryIDs: []uint{1, 2},
SelfService: ptr.Bool(true),
}

err = ds.SaveInHouseAppUpdates(ctx, &updatePayload)
require.NoError(t, err)

// Categories
err = sqlx.GetContext(ctx, ds.reader(ctx), &count, `SELECT COUNT(id) FROM in_house_app_software_categories WHERE in_house_app_id = ?`, secondInstallerID)
require.NoError(t, err)
require.Equal(t, 2, count)
}

func createInHouseAppInstallRequest(t *testing.T, ds *Datastore, hostID uint, appID uint, titleID uint, user *fleet.User) string {
ctx := context.Background()
ctx = viewer.NewContext(ctx, viewer.Viewer{User: user})
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package tables

import (
"database/sql"
"fmt"
)

func init() {
MigrationClient.AddMigration(Up_20251110172137, Down_20251110172137)
}

func Up_20251110172137(tx *sql.Tx) error {

createLabelMappingTableStmt := `
CREATE TABLE in_house_app_software_categories (
id int unsigned NOT NULL AUTO_INCREMENT,
software_category_id int unsigned NOT NULL,
in_house_app_id int unsigned NOT NULL,
created_at datetime(6) DEFAULT CURRENT_TIMESTAMP(6),
PRIMARY KEY (id),
UNIQUE KEY idx_unique_in_house_app_id_software_category_id (in_house_app_id,software_category_id),
CONSTRAINT in_house_app_software_categories_ibfk_1 FOREIGN KEY (in_house_app_id) REFERENCES in_house_apps (id) ON DELETE CASCADE,
CONSTRAINT in_house_app_software_categories_ibfk_2 FOREIGN KEY (software_category_id) REFERENCES software_categories (id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
`

if _, err := tx.Exec(createLabelMappingTableStmt); err != nil {
return fmt.Errorf("create in_house_app_software_categories table: %w", err)
}
return nil
}

func Down_20251110172137(tx *sql.Tx) error {
return nil
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package tables

import "testing"

func TestUp_20251110172137(t *testing.T) {
db := applyUpToPrev(t)

// New table

// Apply current migration.
applyNext(t, db)
}
18 changes: 16 additions & 2 deletions server/datastore/mysql/schema.sql

Large diffs are not rendered by default.

17 changes: 15 additions & 2 deletions server/datastore/mysql/software.go
Original file line number Diff line number Diff line change
Expand Up @@ -5631,15 +5631,28 @@ FROM
JOIN vpp_app_team_software_categories vatsc ON vatsc.vpp_app_team_id = vat.id
JOIN software_categories sc ON vatsc.software_category_id = sc.id
WHERE
st.id IN (?) AND vat.global_or_team_id = ?;
st.id IN (?) AND vat.global_or_team_id = ?

UNION

SELECT
st.id AS title_id,
sc.name AS software_category_name
FROM
in_house_apps iha
JOIN software_titles st ON st.id = iha.title_id
JOIN in_house_app_software_categories ihasc ON ihasc.in_house_app_id = iha.id
JOIN software_categories sc ON ihasc.software_category_id = sc.id
WHERE
st.id IN (?) AND iha.global_or_team_id = ?;
`

var tmID uint
if teamID != nil {
tmID = *teamID
}

stmt, args, err := sqlx.In(stmt, softwareTitleIDs, tmID, softwareTitleIDs, tmID)
stmt, args, err := sqlx.In(stmt, softwareTitleIDs, tmID, softwareTitleIDs, tmID, softwareTitleIDs, tmID)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "sqlx.In for get categories for software installers")
}
Expand Down
3 changes: 2 additions & 1 deletion server/datastore/mysql/software_installers.go
Original file line number Diff line number Diff line change
Expand Up @@ -198,15 +198,16 @@ func (ds *Datastore) MatchOrCreateSoftwareInstaller(ctx context.Context, payload
}

// Insert in house app instead of software installer
// And add both iOS and ipadOS titles per https://github.com/fleetdm/fleet/issues/34283
if payload.Extension == "ipa" {
// Insert both iOS and ipadOS titles per https://github.com/fleetdm/fleet/issues/34283
installerID, titleID, err := ds.insertInHouseApp(ctx, &fleet.InHouseAppPayload{
TeamID: payload.TeamID,
Filename: payload.Filename,
BundleID: payload.BundleIdentifier,
StorageID: payload.StorageID,
Platform: payload.Platform,
ValidatedLabels: payload.ValidatedLabels,
CategoryIDs: payload.CategoryIDs,
Version: payload.Version,
SelfService: payload.SelfService,
})
Expand Down
1 change: 1 addition & 0 deletions server/fleet/in_house_apps.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ type InHouseAppPayload struct {
StorageID string
Platform string
ValidatedLabels *LabelIdentsWithScope
CategoryIDs []uint
Version string
SelfService bool
}
Loading
Loading