diff --git a/ee/server/service/software_title_icons.go b/ee/server/service/software_title_icons.go index 425a106e2b..eb4809b687 100644 --- a/ee/server/service/software_title_icons.go +++ b/ee/server/service/software_title_icons.go @@ -51,6 +51,7 @@ func (svc *Service) UploadSoftwareTitleIcon(ctx context.Context, payload *fleet. } var softwareInstaller *fleet.SoftwareInstaller var vppApp *fleet.VPPAppStoreApp + var inHouseApp *fleet.SoftwareInstaller vc, ok := viewer.FromContext(ctx) if !ok { @@ -67,9 +68,15 @@ func (svc *Service) UploadSoftwareTitleIcon(ctx context.Context, payload *fleet. if err != nil && !fleet.IsNotFound(err) { return fleet.SoftwareTitleIcon{}, ctxerr.Wrap(ctx, err, "getting VPP app") } + if vppApp == nil { + inHouseApp, err = svc.ds.GetInHouseAppMetadataByTeamAndTitleID(ctx, &payload.TeamID, payload.TitleID) + if err != nil && !fleet.IsNotFound(err) { + return fleet.SoftwareTitleIcon{}, ctxerr.Wrap(ctx, err, "getting in-house app") + } + } } - if softwareInstaller == nil && vppApp == nil { - return fleet.SoftwareTitleIcon{}, &fleet.BadRequestError{Message: fmt.Sprintf("Software title has no software installer or VPP app: %d", payload.TitleID)} + if softwareInstaller == nil && vppApp == nil && inHouseApp == nil { + return fleet.SoftwareTitleIcon{}, &fleet.BadRequestError{Message: fmt.Sprintf("Software title has no software installer, VPP app, or in-house app: %d", payload.TitleID)} } icon, err := svc.ds.GetSoftwareTitleIcon(ctx, payload.TeamID, payload.TitleID) @@ -228,5 +235,23 @@ func generateEditActivityForSoftwareTitleIcon(ctx context.Context, svc *Service, return nil } - return ctxerr.New(ctx, "no software installer or VPP app found for software title icon") + if activityDetailsForSoftwareTitleIcon.InHouseAppID != nil { + if err := svc.NewActivity(ctx, user, fleet.ActivityTypeEditedSoftware{ + SoftwareTitle: activityDetailsForSoftwareTitleIcon.SoftwareTitle, + SoftwarePackage: activityDetailsForSoftwareTitleIcon.Filename, + TeamName: activityDetailsForSoftwareTitleIcon.TeamName, + TeamID: &activityDetailsForSoftwareTitleIcon.TeamID, + SelfService: activityDetailsForSoftwareTitleIcon.SelfService, + SoftwareIconURL: &iconUrl, + LabelsIncludeAny: activityDetailsForSoftwareTitleIcon.LabelsIncludeAny, + LabelsExcludeAny: activityDetailsForSoftwareTitleIcon.LabelsExcludeAny, + SoftwareTitleID: activityDetailsForSoftwareTitleIcon.SoftwareTitleID, + }); err != nil { + return ctxerr.Wrap(ctx, err, "creating activity for software title icon") + } + + return nil + } + + return ctxerr.New(ctx, "no software installer, VPP app, or in-house app found for software title icon") } diff --git a/server/datastore/mysql/software_title_icons.go b/server/datastore/mysql/software_title_icons.go index e482509593..1ab6389a9a 100644 --- a/server/datastore/mysql/software_title_icons.go +++ b/server/datastore/mysql/software_title_icons.go @@ -148,6 +148,7 @@ func (ds *Datastore) ActivityDetailsForSoftwareTitleIcon(ctx context.Context, te query := ` SELECT software_installers.id AS software_installer_id, + in_house_apps.id AS in_house_app_id, vpp_apps.adam_id AS adam_id, vpp_apps_teams.id AS vpp_app_team_id, vpp_apps.icon_url AS vpp_icon_url, @@ -155,13 +156,14 @@ func (ds *Datastore) ActivityDetailsForSoftwareTitleIcon(ctx context.Context, te software_installers.filename AS filename, teams.name AS team_name, COALESCE(teams.id, 0) AS team_id, - COALESCE(software_installers.self_service, vpp_apps_teams.self_service) AS self_service, + COALESCE(software_installers.self_service, vpp_apps_teams.self_service, false) AS self_service, -- TODO(JK): change false to iha.self_service once that is merged software_titles.id AS software_title_id, vpp_apps.platform AS platform FROM software_title_icons INNER JOIN software_titles ON software_title_icons.software_title_id = software_titles.id LEFT JOIN teams ON software_title_icons.team_id = teams.id LEFT JOIN software_installers ON software_installers.title_id = software_titles.id + LEFT JOIN in_house_apps ON in_house_apps.title_id = software_titles.id LEFT JOIN vpp_apps ON vpp_apps.title_id = software_titles.id LEFT JOIN vpp_apps_teams ON vpp_apps_teams.adam_id = vpp_apps.adam_id AND vpp_apps_teams.platform = vpp_apps.platform WHERE software_title_icons.team_id = ? AND software_title_icons.software_title_id = ? @@ -205,6 +207,21 @@ func (ds *Datastore) ActivityDetailsForSoftwareTitleIcon(ctx context.Context, te return fleet.DetailsForSoftwareIconActivity{}, ctxerr.Wrap(ctx, err, "getting labels for software title icon") } } + if details.InHouseAppID != nil { + labelQuery := ` + SELECT + labels.id AS id, + labels.name AS name, + in_house_app_labels.exclude AS exclude + FROM in_house_app_labels + INNER JOIN labels ON in_house_app_labels.label_id = labels.id + WHERE in_house_app_id = ? + ` + if err := sqlx.SelectContext(ctx, ds.reader(ctx), &labels, labelQuery, details.InHouseAppID); err != nil { + return fleet.DetailsForSoftwareIconActivity{}, ctxerr.Wrap(ctx, err, "getting labels for software title icon") + } + } + for _, l := range labels { if l.Exclude { details.LabelsExcludeAny = append(details.LabelsExcludeAny, fleet.ActivitySoftwareLabel{ diff --git a/server/datastore/mysql/software_title_icons_test.go b/server/datastore/mysql/software_title_icons_test.go index a53c32271e..e07af59138 100644 --- a/server/datastore/mysql/software_title_icons_test.go +++ b/server/datastore/mysql/software_title_icons_test.go @@ -669,6 +669,75 @@ func testDeleteIconsAssociatedWithTitlesWithoutInstallers(t *testing.T, ds *Data err = ds.DeleteIconsAssociatedWithTitlesWithoutInstallers(ctx, 1) require.NoError(t, err) + err = ds.writer(ctx).Select(&titleIds, `SELECT software_title_id FROM software_title_icons where team_id = ?`, teamID) + require.NoError(t, err) + require.Len(t, titleIds, 1) + require.Contains(t, titleIds, titleID) + }, + }, + { + name: "does not delete icons still associated with an in-house app", + before: func(ds *Datastore) { + user := test.NewUser(t, ds, "user1", "user1@example.com", false) + team, err := ds.NewTeam(ctx, &fleet.Team{Name: "team 1"}) + require.NoError(t, err) + + // create an in-house app that will create two titles + payload := fleet.UploadSoftwareInstallerPayload{ + TeamID: &team.ID, + UserID: user.ID, + Title: "foo", + Filename: "foo.ipa", + BundleIdentifier: "foo.bundle.id", + StorageID: "testingtesting123", + Platform: "ios", + Extension: "ipa", + Version: "1.2.3", + ValidatedLabels: &fleet.LabelIdentsWithScope{}, + } + _, titleID, err = ds.MatchOrCreateSoftwareInstaller(ctx, &payload) + require.NoError(t, err) + var softwareInstallerTitleIds []struct { + ID uint `db:"title_id"` + } + err = ds.writer(ctx).Select(&softwareInstallerTitleIds, `SELECT title_id from in_house_apps`) + require.NoError(t, err) + require.Len(t, softwareInstallerTitleIds, 2) // iha create 2 titles + require.Equal(t, titleID, softwareInstallerTitleIds[1].ID) + + _, err = ds.CreateOrUpdateSoftwareTitleIcon(ctx, &fleet.UploadSoftwareTitleIconPayload{ + TeamID: team.ID, + TitleID: titleID, + StorageID: "storage-id-1", + Filename: "test-icon-updated.png", + }) + require.NoError(t, err) + + result, err := ds.writer(ctx).ExecContext(ctx, ` + INSERT INTO software_titles (name, source, bundle_identifier) VALUES (?, ?, ?) + `, "foo2", "ios_apps", "foo2.bundle.id") + require.NoError(t, err) + deletedTitleID64, err := result.LastInsertId() + deletedTitleID = uint(deletedTitleID64) //nolint:gosec + require.NoError(t, err) + _, err = ds.CreateOrUpdateSoftwareTitleIcon(ctx, &fleet.UploadSoftwareTitleIconPayload{ + TeamID: team.ID, + TitleID: deletedTitleID, + StorageID: "storage-id-1", + Filename: "test-icon-updated.png", + }) + require.NoError(t, err) + }, + testFunc: func(t *testing.T, ds *Datastore) { + var titleIds []uint + err = ds.writer(ctx).Select(&titleIds, `SELECT software_title_id FROM software_title_icons where team_id = ?`, teamID) + require.NoError(t, err) + require.Contains(t, titleIds, titleID) + require.Contains(t, titleIds, deletedTitleID) + + err = ds.DeleteIconsAssociatedWithTitlesWithoutInstallers(ctx, 1) + require.NoError(t, err) + err = ds.writer(ctx).Select(&titleIds, `SELECT software_title_id FROM software_title_icons where team_id = ?`, teamID) require.NoError(t, err) require.Len(t, titleIds, 1) diff --git a/server/fleet/software_title_icons.go b/server/fleet/software_title_icons.go index b91e76904d..bcbd7be435 100644 --- a/server/fleet/software_title_icons.go +++ b/server/fleet/software_title_icons.go @@ -47,6 +47,7 @@ type SoftwareTitleIconStore interface { type DetailsForSoftwareIconActivity struct { SoftwareInstallerID *uint `db:"software_installer_id"` + InHouseAppID *uint `db:"in_house_app_id"` AdamID *string `db:"adam_id"` VPPAppTeamID *uint `db:"vpp_app_team_id"` VPPIconUrl *string `db:"vpp_icon_url"` diff --git a/server/service/software_title_icons_test.go b/server/service/software_title_icons_test.go index 07b45e5183..405f575a42 100644 --- a/server/service/software_title_icons_test.go +++ b/server/service/software_title_icons_test.go @@ -141,7 +141,7 @@ func TestUploadSoftwareTitleIcon(t *testing.T) { testFunc func(*testing.T) }{ { - name: "upload icon title with no software installer or vpp app", + name: "upload icon title with no software installer, vpp app, or in-house app", before: func() { capturedActivities = make([]fleet.ActivityDetails, 0) ds.GetSoftwareInstallerMetadataByTeamAndTitleIDFunc = func(ctx context.Context, teamID *uint, titleID uint, includeUnpublished bool) (*fleet.SoftwareInstaller, error) { @@ -150,6 +150,10 @@ func TestUploadSoftwareTitleIcon(t *testing.T) { ds.GetVPPAppMetadataByTeamAndTitleIDFunc = func(ctx context.Context, teamID *uint, titleID uint) (*fleet.VPPAppStoreApp, error) { return nil, ctxerr.Wrap(ctx, &common_mysql.NotFoundError{ResourceType: "VPPAppMetadata"}, "get VPP app metadata") } + ds.GetInHouseAppMetadataByTeamAndTitleIDFunc = func(ctx context.Context, teamID *uint, titleID uint) (*fleet.SoftwareInstaller, error) { + return nil, ctxerr.Wrap(ctx, &common_mysql.NotFoundError{ResourceType: "InHouseAppMetadata"}, "get in-house app metadata") + } + file, err := os.Open("testdata/icons/valid-icon.png") require.NoError(t, err) defer file.Close() @@ -165,7 +169,7 @@ func TestUploadSoftwareTitleIcon(t *testing.T) { } _, err := svc.UploadSoftwareTitleIcon(ctx, payload) require.Error(t, err) - require.Contains(t, err.Error(), "Software title has no software installer or VPP app") + require.Contains(t, err.Error(), "Software title has no software installer, VPP app, or in-house app") }, }, { @@ -271,6 +275,57 @@ func TestUploadSoftwareTitleIcon(t *testing.T) { require.Len(t, capturedActivities, 1) }, }, + + { + name: "upload icon for in-house app", + before: func() { + capturedActivities = make([]fleet.ActivityDetails, 0) + ds.GetInHouseAppMetadataByTeamAndTitleIDFunc = func(ctx context.Context, teamID *uint, titleID uint) (*fleet.SoftwareInstaller, error) { + return &fleet.SoftwareInstaller{ + TitleID: ptr.Uint(1), + TeamID: ptr.Uint(1), + }, nil + } + ds.GetSoftwareTitleIconFunc = func(ctx context.Context, teamID, titleID uint) (*fleet.SoftwareTitleIcon, error) { + return nil, ctxerr.Wrap(ctx, &common_mysql.NotFoundError{ResourceType: "SoftwareTitleIcon"}, "get software title icon") + } + ds.GetTeamIdsForIconStorageIdFunc = func(ctx context.Context, storageID string) ([]uint, error) { + return []uint{1}, nil + } + ds.CreateOrUpdateSoftwareTitleIconFunc = func(ctx context.Context, payload *fleet.UploadSoftwareTitleIconPayload) (*fleet.SoftwareTitleIcon, error) { + sha, err := file.SHA256FromTempFileReader(iconFile) + require.NoError(t, err) + + return &fleet.SoftwareTitleIcon{ + TeamID: 1, + SoftwareTitleID: 1, + StorageID: sha, + Filename: "icon.png", + }, nil + } + ds.ActivityDetailsForSoftwareTitleIconFunc = func(ctx context.Context, teamID uint, titleID uint) (fleet.DetailsForSoftwareIconActivity, error) { + return fleet.DetailsForSoftwareIconActivity{ + InHouseAppID: ptr.Uint(1), + SoftwareTitle: "foo", + Filename: ptr.String("icon.png"), + TeamName: ptr.String("team1"), + TeamID: 1, + SoftwareTitleID: 1, + }, nil + } + }, + testFunc: func(t *testing.T) { + payload := &fleet.UploadSoftwareTitleIconPayload{ + TitleID: 1, + TeamID: 1, + IconFile: iconFile, + Filename: "icon.png", + } + _, err := svc.UploadSoftwareTitleIcon(ctx, payload) + require.NoError(t, err) + require.Len(t, capturedActivities, 1) + }, + }, } for _, tc := range testCases {