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
19 changes: 9 additions & 10 deletions ee/server/service/in_house_apps.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,7 @@ func (svc *Service) updateInHouseAppInstaller(ctx context.Context, payload *flee
return nil, ctxerr.Wrap(ctx, err, "getting existing installer")
}

if payload.SelfService == nil && payload.InstallerFile == nil && payload.PreInstallQuery == nil &&
payload.InstallScript == nil && payload.PostInstallScript == nil && payload.UninstallScript == nil &&
payload.LabelsIncludeAny == nil && payload.LabelsExcludeAny == nil {
if payload.IsNoopPayload(software) {
return existingInstaller, nil // no payload, noop
}

Expand All @@ -41,13 +39,14 @@ func (svc *Service) updateInHouseAppInstaller(ctx context.Context, payload *flee
selfService = *payload.SelfService
}
activity := fleet.ActivityTypeEditedSoftware{
SoftwareTitle: existingInstaller.SoftwareTitle,
TeamName: teamName,
TeamID: actTeamID,
SoftwarePackage: &existingInstaller.Name,
SoftwareTitleID: payload.TitleID,
SoftwareIconURL: existingInstaller.IconUrl,
SelfService: selfService,
SoftwareTitle: existingInstaller.SoftwareTitle,
TeamName: teamName,
TeamID: actTeamID,
SoftwarePackage: &existingInstaller.Name,
SoftwareTitleID: payload.TitleID,
SoftwareIconURL: existingInstaller.IconUrl,
SelfService: selfService,
SoftwareDisplayName: payload.DisplayName,
}

var payloadForNewInstallerFile *fleet.UploadSoftwareInstallerPayload
Expand Down
4 changes: 1 addition & 3 deletions ee/server/service/software_installers.go
Original file line number Diff line number Diff line change
Expand Up @@ -360,9 +360,7 @@ func (svc *Service) UpdateSoftwareInstaller(ctx context.Context, payload *fleet.
return nil, ctxerr.Wrap(ctx, err, "getting existing installer")
}

if payload.SelfService == nil && payload.InstallerFile == nil && payload.PreInstallQuery == nil &&
payload.InstallScript == nil && payload.PostInstallScript == nil && payload.UninstallScript == nil &&
payload.LabelsIncludeAny == nil && payload.LabelsExcludeAny == nil && software.DisplayName == payload.DisplayName {
if payload.IsNoopPayload(software) {
return existingInstaller, nil // no payload, noop
}

Expand Down
3 changes: 2 additions & 1 deletion ee/server/service/vpp.go
Original file line number Diff line number Diff line change
Expand Up @@ -599,7 +599,7 @@ func getVPPAppsMetadata(ctx context.Context, ids []fleet.VPPAppTeam) ([]*fleet.V
return apps, nil
}

func (svc *Service) UpdateAppStoreApp(ctx context.Context, titleID uint, teamID *uint, selfService bool, labelsIncludeAny, labelsExcludeAny, categories []string) (*fleet.VPPAppStoreApp, error) {
func (svc *Service) UpdateAppStoreApp(ctx context.Context, titleID uint, teamID *uint, selfService bool, labelsIncludeAny, labelsExcludeAny, categories []string, displayName string) (*fleet.VPPAppStoreApp, error) {
if err := svc.authz.Authorize(ctx, &fleet.VPPApp{TeamID: teamID}, fleet.ActionWrite); err != nil {
return nil, err
}
Expand Down Expand Up @@ -634,6 +634,7 @@ func (svc *Service) UpdateAppStoreApp(ctx context.Context, titleID uint, teamID
},
SelfService: selfService,
ValidatedLabels: validatedLabels,
DisplayName: displayName,
},
TeamID: teamID,
TitleID: titleID,
Expand Down
11 changes: 11 additions & 0 deletions server/datastore/mysql/in_house_apps.go
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,13 @@ WHERE
dest.Categories = categories
}

displayName, err := ds.getSoftwareTitleDisplayName(ctx, tmID, titleID)
if err != nil && !fleet.IsNotFound(err) {
return nil, ctxerr.Wrap(ctx, err, "get in house app display name")
}

dest.DisplayName = displayName

if teamID != nil {
icon, err := ds.GetSoftwareTitleIcon(ctx, *teamID, titleID)
if err != nil && !fleet.IsNotFound(err) {
Expand Down Expand Up @@ -295,6 +302,10 @@ func (ds *Datastore) SaveInHouseAppUpdates(ctx context.Context, payload *fleet.U
}
}

if err := updateSoftwareTitleDisplayName(ctx, tx, payload.TeamID, payload.TitleID, payload.DisplayName); err != nil {
return ctxerr.Wrap(ctx, err, "update in house app display name")
}

return nil
})
if err != nil {
Expand Down
7 changes: 7 additions & 0 deletions server/datastore/mysql/software_installers.go
Original file line number Diff line number Diff line change
Expand Up @@ -930,6 +930,13 @@ WHERE
dest.Categories = categories
}

displayName, err := ds.getSoftwareTitleDisplayName(ctx, tmID, titleID)
if err != nil && !fleet.IsNotFound(err) {
return nil, ctxerr.Wrap(ctx, err, "get software title display name")
}

dest.DisplayName = displayName

if teamID != nil {
policies, err := ds.getPoliciesBySoftwareTitleIDs(ctx, []uint{titleID}, *teamID)
if err != nil {
Expand Down
13 changes: 13 additions & 0 deletions server/datastore/mysql/vpp.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,18 @@ WHERE
}
app.Categories = categories

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

displayName, err := ds.getSoftwareTitleDisplayName(ctx, tmID, titleID)
if err != nil && !fleet.IsNotFound(err) {
return nil, ctxerr.Wrap(ctx, err, "get display name for app store app")
}

app.DisplayName = displayName

if teamID != nil {
policies, err := ds.getPoliciesBySoftwareTitleIDs(ctx, []uint{titleID}, *teamID)
if err != nil {
Expand All @@ -98,6 +110,7 @@ WHERE
if icon != nil {
app.IconURL = ptr.String(icon.IconUrl())
}

}

return &app, nil
Expand Down
2 changes: 1 addition & 1 deletion server/fleet/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -717,7 +717,7 @@ type Service interface {

// AddAppStoreApp persists a VPP app onto a team and returns the resulting title ID
AddAppStoreApp(ctx context.Context, teamID *uint, appTeam VPPAppTeam) (uint, error)
UpdateAppStoreApp(ctx context.Context, titleID uint, teamID *uint, selfService bool, labelsIncludeAny, labelsExcludeAny, categories []string) (*VPPAppStoreApp, error)
UpdateAppStoreApp(ctx context.Context, titleID uint, teamID *uint, selfService bool, labelsIncludeAny, labelsExcludeAny, categories []string, displayName string) (*VPPAppStoreApp, error)

// GetInHouseAppManifest returns a manifest XML file that points at the download URL for the given in-house app.
GetInHouseAppManifest(ctx context.Context, titleID uint, teamID *uint) ([]byte, error)
Expand Down
9 changes: 9 additions & 0 deletions server/fleet/software_installer.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,9 @@ type SoftwareInstaller struct {
Categories []string `json:"categories"`

BundleIdentifier string `json:"-" db:"bundle_identifier"`

// DisplayName is an end-user friendly name.
DisplayName string `json:"display_name"`
}

// SoftwarePackageResponse is the response type used when applying software by batch.
Expand Down Expand Up @@ -567,6 +570,12 @@ type UpdateSoftwareInstallerPayload struct {
DisplayName string
}

func (u *UpdateSoftwareInstallerPayload) IsNoopPayload(existing *SoftwareTitle) bool {
return u.SelfService == nil && u.InstallerFile == nil && u.PreInstallQuery == nil &&
u.InstallScript == nil && u.PostInstallScript == nil && u.UninstallScript == nil &&
u.LabelsIncludeAny == nil && u.LabelsExcludeAny == nil && u.DisplayName == existing.DisplayName
}

// DownloadSoftwareInstallerPayload is the payload for downloading a software installer.
type DownloadSoftwareInstallerPayload struct {
Filename string
Expand Down
5 changes: 3 additions & 2 deletions server/fleet/vpp.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ type VPPAppTeam struct {
// automatically created when a VPP app is added to Fleet. This field should be set after VPP
// app creation if AddAutoInstallPolicy is true.
AddedAutomaticInstallPolicy *Policy `json:"-"`
DisplayName string `json:"-"`
DisplayName string `json:"display_name"`
}

// VPPApp represents a VPP (Volume Purchase Program) application,
Expand Down Expand Up @@ -102,7 +102,8 @@ type VPPAppStoreApp struct {
AddedAt time.Time `db:"added_at" json:"created_at"`
// Categories is the list of categories to which this software belongs: e.g. "Productivity",
// "Browsers", etc.
Categories []string `json:"categories"`
Categories []string `json:"categories"`
DisplayName string `json:"display_name"`
}

// VPPAppStatusSummary represents aggregated status metrics for a VPP app.
Expand Down
6 changes: 3 additions & 3 deletions server/mock/service/service_mock.go
Original file line number Diff line number Diff line change
Expand Up @@ -456,7 +456,7 @@ type GetAppStoreAppsFunc func(ctx context.Context, teamID *uint) ([]*fleet.VPPAp

type AddAppStoreAppFunc func(ctx context.Context, teamID *uint, appTeam fleet.VPPAppTeam) (uint, error)

type UpdateAppStoreAppFunc func(ctx context.Context, titleID uint, teamID *uint, selfService bool, labelsIncludeAny []string, labelsExcludeAny []string, categories []string) (*fleet.VPPAppStoreApp, error)
type UpdateAppStoreAppFunc func(ctx context.Context, titleID uint, teamID *uint, selfService bool, labelsIncludeAny []string, labelsExcludeAny []string, categories []string, displayName string) (*fleet.VPPAppStoreApp, error)

type GetInHouseAppManifestFunc func(ctx context.Context, titleID uint, teamID *uint) ([]byte, error)

Expand Down Expand Up @@ -3640,11 +3640,11 @@ func (s *Service) AddAppStoreApp(ctx context.Context, teamID *uint, appTeam flee
return s.AddAppStoreAppFunc(ctx, teamID, appTeam)
}

func (s *Service) UpdateAppStoreApp(ctx context.Context, titleID uint, teamID *uint, selfService bool, labelsIncludeAny []string, labelsExcludeAny []string, categories []string) (*fleet.VPPAppStoreApp, error) {
func (s *Service) UpdateAppStoreApp(ctx context.Context, titleID uint, teamID *uint, selfService bool, labelsIncludeAny []string, labelsExcludeAny []string, categories []string, displayName string) (*fleet.VPPAppStoreApp, error) {
s.mu.Lock()
s.UpdateAppStoreAppFuncInvoked = true
s.mu.Unlock()
return s.UpdateAppStoreAppFunc(ctx, titleID, teamID, selfService, labelsIncludeAny, labelsExcludeAny, categories)
return s.UpdateAppStoreAppFunc(ctx, titleID, teamID, selfService, labelsIncludeAny, labelsExcludeAny, categories, displayName)
}

func (s *Service) GetInHouseAppManifest(ctx context.Context, titleID uint, teamID *uint) ([]byte, error) {
Expand Down
148 changes: 148 additions & 0 deletions server/service/integration_software_titles_test.go
Original file line number Diff line number Diff line change
@@ -1,18 +1,22 @@
package service

import (
"context"
"encoding/json"
"fmt"
"net/http"
"strings"

"github.com/fleetdm/fleet/v4/server/datastore/mysql"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/ptr"
"github.com/jmoiron/sqlx"
"github.com/stretchr/testify/require"
)

func (s *integrationMDMTestSuite) TestSoftwareTitleDisplayNames() {
t := s.T()
ctx := context.Background()

// Create a team
var newTeamResp teamResponse
Expand Down Expand Up @@ -155,4 +159,148 @@ func (s *integrationMDMTestSuite) TestSoftwareTitleDisplayNames() {
require.Equal(t, getDeviceSw.Software[0].Name, "ruby")
s.Assert().Empty(getDeviceSw.Software[0].DisplayName)

// Test display names with app store apps
includeAnyApp := fleet.VPPApp{
VPPAppTeam: fleet.VPPAppTeam{
VPPAppID: fleet.VPPAppID{
AdamID: "1",
Platform: fleet.MacOSPlatform,
},
},
Name: "App 1",
BundleIdentifier: "a-1",
IconURL: "https://example.com/images/1",
LatestVersion: "1.0.0",
}

var addAppResp addAppStoreAppResponse
addAppReq := &addAppStoreAppRequest{
TeamID: &team.ID,
AppStoreID: includeAnyApp.AdamID,
SelfService: true,
}

// Now add it for real
s.DoJSON("POST", "/api/latest/fleet/software/app_store_apps", addAppReq, http.StatusOK, &addAppResp)

macOSTitleID := addAppResp.TitleID

updateAppReq := &updateAppStoreAppRequest{TeamID: &team.ID, SelfService: false, DisplayName: "MacOSAppStoreAppUpdated1"}
var updateAppResp updateAppStoreAppResponse
s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/software/titles/%d/app_store_app", macOSTitleID), updateAppReq, http.StatusOK, &updateAppResp)

s.Assert().Equal(updateAppReq.DisplayName, updateAppResp.AppStoreApp.DisplayName)

stResp = getSoftwareTitleResponse{}
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/software/titles/%d", macOSTitleID), getSoftwareTitleRequest{}, http.StatusOK, &stResp, "team_id", fmt.Sprint(team.ID))
s.Assert().Equal(updateAppReq.DisplayName, stResp.SoftwareTitle.DisplayName)

// List software titles has display name
s.DoJSON("GET", "/api/latest/fleet/software/titles", listSoftwareTitlesRequest{}, http.StatusOK, &resp, "team_id", fmt.Sprint(team.ID), "query", includeAnyApp.Name)
for _, a := range resp.SoftwareTitles {
if a.ID == macOSTitleID {
s.Assert().Equal(updateAppReq.DisplayName, a.DisplayName)
}
}

updateAppReq = &updateAppStoreAppRequest{TeamID: &team.ID, SelfService: false, DisplayName: "MacOSAppStoreAppUpdated2"}
s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/software/titles/%d/app_store_app", macOSTitleID), updateAppReq, http.StatusOK, &updateAppResp)

s.Assert().Equal(updateAppReq.DisplayName, updateAppResp.AppStoreApp.DisplayName)

stResp = getSoftwareTitleResponse{}
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/software/titles/%d", macOSTitleID), getSoftwareTitleRequest{}, http.StatusOK, &stResp, "team_id", fmt.Sprint(team.ID))
s.Assert().Equal(updateAppReq.DisplayName, stResp.SoftwareTitle.DisplayName)

// List software titles has display name
s.DoJSON("GET", "/api/latest/fleet/software/titles", listSoftwareTitlesRequest{}, http.StatusOK, &resp, "team_id", fmt.Sprint(team.ID), "query", includeAnyApp.Name)
for _, a := range resp.SoftwareTitles {
if a.ID == macOSTitleID {
s.Assert().Equal(updateAppReq.DisplayName, a.DisplayName)
}
}

updateAppReq = &updateAppStoreAppRequest{TeamID: &team.ID, SelfService: false, DisplayName: ""}
s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/software/titles/%d/app_store_app", macOSTitleID), updateAppReq, http.StatusOK, &updateAppResp)

s.Assert().Equal(updateAppReq.DisplayName, updateAppResp.AppStoreApp.DisplayName)

stResp = getSoftwareTitleResponse{}
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/software/titles/%d", macOSTitleID), getSoftwareTitleRequest{}, http.StatusOK, &stResp, "team_id", fmt.Sprint(team.ID))
s.Assert().Empty(stResp.SoftwareTitle.DisplayName)

// List software titles has display name
s.DoJSON("GET", "/api/latest/fleet/software/titles", listSoftwareTitlesRequest{}, http.StatusOK, &resp, "team_id", fmt.Sprint(team.ID), "query", includeAnyApp.Name)
for _, a := range resp.SoftwareTitles {
if a.ID == macOSTitleID {
s.Assert().Empty(a.DisplayName)
}
}

// Test display names with in-house apps
// Upload in-house app for iOS, with the label as "exclude any"
s.uploadSoftwareInstaller(t, &fleet.UploadSoftwareInstallerPayload{Filename: "ipa_test.ipa", TeamID: &team.ID}, http.StatusOK, "")

// Get title ID
mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
return sqlx.GetContext(ctx, q, &titleID, "SELECT title_id FROM in_house_apps WHERE filename = 'ipa_test.ipa'")
})

s.updateSoftwareInstaller(t, &fleet.UpdateSoftwareInstallerPayload{
TitleID: titleID,
TeamID: &team.ID,
DisplayName: "InHouseAppUpdate",
}, http.StatusOK, "")

// Entity has display name
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/software/titles/%d", titleID), getSoftwareTitleRequest{}, http.StatusOK, &stResp, "team_id", fmt.Sprint(team.ID))
s.Assert().Equal("InHouseAppUpdate", stResp.SoftwareTitle.DisplayName)

// List software titles has display name
s.DoJSON("GET", "/api/latest/fleet/software/titles", listSoftwareTitlesRequest{}, http.StatusOK, &resp, "team_id", fmt.Sprint(team.ID))

for _, t := range resp.SoftwareTitles {
if t.ID == titleID {
s.Assert().Equal("InHouseAppUpdate", t.DisplayName)
}
}

s.updateSoftwareInstaller(t, &fleet.UpdateSoftwareInstallerPayload{
TitleID: titleID,
TeamID: &team.ID,
DisplayName: "InHouseAppUpdate2",
}, http.StatusOK, "")

// Entity has display name
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/software/titles/%d", titleID), getSoftwareTitleRequest{}, http.StatusOK, &stResp, "team_id", fmt.Sprint(team.ID))
s.Assert().Equal("InHouseAppUpdate2", stResp.SoftwareTitle.DisplayName)

// List software titles has display name
s.DoJSON("GET", "/api/latest/fleet/software/titles", listSoftwareTitlesRequest{}, http.StatusOK, &resp, "team_id", fmt.Sprint(team.ID))

for _, t := range resp.SoftwareTitles {
if t.ID == titleID {
s.Assert().Equal("InHouseAppUpdate2", t.DisplayName)
}
}

s.updateSoftwareInstaller(t, &fleet.UpdateSoftwareInstallerPayload{
TitleID: titleID,
TeamID: &team.ID,
DisplayName: "",
}, http.StatusOK, "")

// Entity has display name
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/software/titles/%d", titleID), getSoftwareTitleRequest{}, http.StatusOK, &stResp, "team_id", fmt.Sprint(team.ID))
s.Assert().Empty(stResp.SoftwareTitle.DisplayName)

// List software titles has display name
s.DoJSON("GET", "/api/latest/fleet/software/titles", listSoftwareTitlesRequest{}, http.StatusOK, &resp, "team_id", fmt.Sprint(team.ID))

for _, t := range resp.SoftwareTitles {
if t.ID == titleID {
s.Assert().Empty(t.DisplayName)
}
}

}
9 changes: 9 additions & 0 deletions server/service/testing_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -832,5 +832,14 @@ func (ts *withServer) updateSoftwareInstaller(
if expectedError != "" {
errMsg := extractServerErrorText(r.Body)
require.Contains(t, errMsg, expectedError)
return
}

bodyBytes, err := io.ReadAll(r.Body)
require.NoError(t, err)

var resp getSoftwareInstallerResponse
require.NoError(t, json.Unmarshal(bodyBytes, &resp))

assert.Equal(t, payload.DisplayName, resp.SoftwareInstaller.DisplayName)
}
Loading
Loading