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
70 changes: 35 additions & 35 deletions ee/server/service/vpp.go
Original file line number Diff line number Diff line change
Expand Up @@ -717,15 +717,15 @@ var androidApplicationID = regexp.MustCompile(`^([A-Za-z]{1}[A-Za-z\d_]*\.)+[A-Z
// IT admins should not be able to add this app manually via the Software page as it is managed automatically by Fleet.
const fleetAgentPackagePrefix = "com.fleetdm.agent"

func (svc *Service) AddAppStoreApp(ctx context.Context, teamID *uint, appID fleet.VPPAppTeam) (uint, error) {
func (svc *Service) AddAppStoreApp(ctx context.Context, teamID *uint, appID fleet.VPPAppTeam) (uint, string, error) {
if err := svc.authz.Authorize(ctx, &fleet.VPPApp{TeamID: teamID}, fleet.ActionWrite); err != nil {
return 0, err
return 0, "", err
}
if appID.AddAutoInstallPolicy {
// Currently, same write permissions are applied on software and policies,
// but leaving this here in case it changes in the future.
if err := svc.authz.Authorize(ctx, &fleet.Policy{PolicyData: fleet.PolicyData{TeamID: teamID}}, fleet.ActionWrite); err != nil {
return 0, err
return 0, "", err
}
}

Expand All @@ -735,30 +735,30 @@ func (svc *Service) AddAppStoreApp(ctx context.Context, teamID *uint, appID flee
}

if !appID.Platform.SupportsAppStoreApps() {
return 0, fleet.NewInvalidArgumentError("platform",
return 0, "", fleet.NewInvalidArgumentError("platform",
fmt.Sprintf("platform must be one of '%s', '%s', '%s', or '%s'", fleet.IOSPlatform, fleet.IPadOSPlatform, fleet.MacOSPlatform, fleet.AndroidPlatform))
}

validatedLabels, err := ValidateSoftwareLabels(ctx, svc, teamID, appID.LabelsIncludeAny, appID.LabelsExcludeAny, appID.LabelsIncludeAll)
if err != nil {
return 0, ctxerr.Wrap(ctx, err, "validating software labels for adding vpp app")
return 0, "", ctxerr.Wrap(ctx, err, "validating software labels for adding vpp app")
}

teamName := fleet.TeamNameNoTeam
if teamID != nil && *teamID != 0 {
tm, err := svc.ds.TeamLite(ctx, *teamID)
if fleet.IsNotFound(err) {
return 0, fleet.NewInvalidArgumentError("team_id/fleet_id", fmt.Sprintf("fleet %d does not exist", *teamID)).
return 0, "", fleet.NewInvalidArgumentError("team_id/fleet_id", fmt.Sprintf("fleet %d does not exist", *teamID)).
WithStatus(http.StatusNotFound)
} else if err != nil {
return 0, ctxerr.Wrap(ctx, err, "checking if team exists")
return 0, "", ctxerr.Wrap(ctx, err, "checking if team exists")
}

teamName = tm.Name
}

if appID.AddAutoInstallPolicy && appID.Platform != fleet.MacOSPlatform {
return 0, fleet.NewUserMessageError(errors.New("Currently, automatic install is only supported on macOS, Windows, and Linux. Please add the app without automatic_install and manually install it on the Host details page."), http.StatusBadRequest)
return 0, "", fleet.NewUserMessageError(errors.New("Currently, automatic install is only supported on macOS, Windows, and Linux. Please add the app without automatic_install and manually install it on the Host details page."), http.StatusBadRequest)
}

isAndroidAppID := androidApplicationID.MatchString(appID.AdamID)
Expand All @@ -771,32 +771,32 @@ func (svc *Service) AddAppStoreApp(ctx context.Context, teamID *uint, appID flee
switch appID.Platform {
case fleet.AndroidPlatform:
if !isAndroidAppID {
return 0, fleet.NewInvalidArgumentError("app_store_id", "Application ID must be a valid Android application ID")
return 0, "", fleet.NewInvalidArgumentError("app_store_id", "Application ID must be a valid Android application ID")
}
if strings.HasPrefix(appID.AdamID, fleetAgentPackagePrefix) {
return 0, fleet.NewInvalidArgumentError("app_store_id", "The Fleet agent cannot be added manually. "+
return 0, "", fleet.NewInvalidArgumentError("app_store_id", "The Fleet agent cannot be added manually. "+
"It is automatically managed by Fleet when Android MDM is enabled.")
}

if strings.HasPrefix(appID.AdamID, fleet.AndroidWebAppPrefix) && appID.Configuration != nil {
return 0, fleet.NewInvalidArgumentError("configuration", "Couldn't add. Android web apps don't support configurations.")
return 0, "", fleet.NewInvalidArgumentError("configuration", "Couldn't add. Android web apps don't support configurations.")
}

appID.SelfService = true
appID.AddAutoInstallPolicy = false

enterprise, err := svc.ds.GetEnterprise(ctx)
if err != nil {
return 0, &fleet.BadRequestError{Message: "Android MDM is not enabled", InternalErr: err}
return 0, "", &fleet.BadRequestError{Message: "Android MDM is not enabled", InternalErr: err}
}
androidEnterpriseName = enterprise.Name()

androidApp, err := svc.androidModule.EnterprisesApplications(ctx, androidEnterpriseName, appID.AdamID)
if err != nil {
if fleet.IsNotFound(err) {
return 0, fleet.NewInvalidArgumentError("app_store_id", fmt.Sprintf("Couldn't add software. The application ID %q isn't available in Play Store. Please find ID on the Play Store and try again.", appID.AdamID))
return 0, "", fleet.NewInvalidArgumentError("app_store_id", fmt.Sprintf("Couldn't add software. The application ID %q isn't available in Play Store. Please find ID on the Play Store and try again.", appID.AdamID))
}
return 0, ctxerr.Wrap(ctx, err, "add app store app: check if android app exists")
return 0, "", ctxerr.Wrap(ctx, err, "add app store app: check if android app exists")
}

app = &fleet.VPPApp{
Expand All @@ -810,18 +810,18 @@ func (svc *Service) AddAppStoreApp(ctx context.Context, teamID *uint, appID flee
if strings.HasPrefix(appID.AdamID, fleet.AndroidWebAppPrefix) {
exists, err := svc.ds.CheckAndroidWebAppNameExistsOnTeam(ctx, teamID, androidApp.Title, appID.AdamID)
if err != nil {
return 0, ctxerr.Wrap(ctx, err, "checking for duplicate android web app name")
return 0, "", ctxerr.Wrap(ctx, err, "checking for duplicate android web app name")
}
if exists {
return 0, fleet.ConflictError{
return 0, "", fleet.ConflictError{
Message: fmt.Sprintf("Couldn't add. Web app with this name (%q) already exists in this fleet. Please add a web app with a different name or delete the existing app and try again.", androidApp.Title),
}
}
}

default:
if isAndroidAppID {
return 0, fleet.NewInvalidArgumentError(
return 0, "", fleet.NewInvalidArgumentError(
"app_store_id",
fmt.Sprintf(
"Couldn't add software. %q isn't available in Apple Business or Play Store. Please purchase a license in Apple Business or find the app in Play Store and try again.",
Expand All @@ -832,17 +832,17 @@ func (svc *Service) AddAppStoreApp(ctx context.Context, teamID *uint, appID flee

teamTokenInfo, err := svc.getVPPTokenInfo(ctx, teamID)
if err != nil {
return 0, ctxerr.Wrap(ctx, err, "retrieving VPP token")
return 0, "", ctxerr.Wrap(ctx, err, "retrieving VPP token")
}
vppToken := teamTokenInfo.Secret

assets, err := vpp.GetAssets(ctx, vppToken, &vpp.AssetFilter{AdamID: appID.AdamID})
if err != nil {
return 0, ctxerr.Wrap(ctx, err, "retrieving VPP asset")
return 0, "", ctxerr.Wrap(ctx, err, "retrieving VPP asset")
}

if len(assets) == 0 {
return 0, fleet.NewInvalidArgumentError("app_store_id",
return 0, "", fleet.NewInvalidArgumentError("app_store_id",
fmt.Sprintf("Error: Couldn't add software. %q isn't available in Apple Business. Please purchase license in Apple Business and try again.", appID.AdamID))
}

Expand All @@ -853,12 +853,12 @@ func (svc *Service) AddAppStoreApp(ctx context.Context, teamID *uint, appID flee
// re-anchor self-heal.
anchor, err = svc.resolveAddAnchor(ctx, asset.AdamID, appID.Platform, teamTokenInfo)
if err != nil {
return 0, ctxerr.Wrap(ctx, err, "resolving anchor for vpp app add")
return 0, "", ctxerr.Wrap(ctx, err, "resolving anchor for vpp app add")
}

assetMetadata, err := apple_apps.GetMetadata([]string{asset.AdamID}, anchor.region, anchor.fetchSecret, svc.getVPPConfig(ctx))
if err != nil {
return 0, ctxerr.Wrap(ctx, err, "fetching VPP asset metadata")
return 0, "", ctxerr.Wrap(ctx, err, "fetching VPP asset metadata")
}

assetMD := assetMetadata[asset.AdamID]
Expand All @@ -869,17 +869,17 @@ func (svc *Service) AddAppStoreApp(ctx context.Context, teamID *uint, appID flee
platforms := apple_apps.ToVPPApps(assetMD)
appFromApple, ok := platforms[appID.Platform]
if !ok {
return 0, fleet.NewInvalidArgumentError("app_store_id", fmt.Sprintf("%s isn't available for %s", assetMD.Attributes.Name, appID.Platform))
return 0, "", fleet.NewInvalidArgumentError("app_store_id", fmt.Sprintf("%s isn't available for %s", assetMD.Attributes.Name, appID.Platform))
}

if appID.Platform == fleet.MacOSPlatform {
exists, err := svc.ds.CheckConflictingInstallerExists(ctx, teamID, appFromApple.BundleIdentifier, string(appID.Platform))
if err != nil {
return 0, ctxerr.Wrap(ctx, err, "checking existence of conflicting installer")
return 0, "", ctxerr.Wrap(ctx, err, "checking existence of conflicting installer")
}

if exists {
return 0, ctxerr.Wrap(ctx, fleet.ConflictError{
return 0, "", ctxerr.Wrap(ctx, fleet.ConflictError{
Message: fmt.Sprintf(fleet.CantAddSoftwareConflictMessage,
assetMD.Attributes.Name, teamName),
}, "vpp app conflicts with existing software installer")
Expand All @@ -888,11 +888,11 @@ func (svc *Service) AddAppStoreApp(ctx context.Context, teamID *uint, appID flee
// Check if an in-house app (IPA) with the same bundle identifier already exists
exists, err := svc.ds.CheckConflictingInHouseAppExists(ctx, teamID, appFromApple.BundleIdentifier, string(appID.Platform))
if err != nil {
return 0, ctxerr.Wrap(ctx, err, "checking existence of conflicting installer")
return 0, "", ctxerr.Wrap(ctx, err, "checking existence of conflicting installer")
}

if exists {
return 0, ctxerr.Wrap(ctx, fleet.ConflictError{
return 0, "", ctxerr.Wrap(ctx, fleet.ConflictError{
Message: fmt.Sprintf(fleet.CantAddSoftwareConflictMessage,
assetMD.Attributes.Name, teamName),
}, "vpp app conflicts with existing in-house app")
Expand All @@ -904,11 +904,11 @@ func (svc *Service) AddAppStoreApp(ctx context.Context, teamID *uint, appID flee
appID.Categories = server.RemoveDuplicatesFromSlice(appID.Categories)
catIDs, err := svc.ds.GetSoftwareCategoryIDs(ctx, appID.Categories)
if err != nil {
return 0, ctxerr.Wrap(ctx, err, "getting software category ids")
return 0, "", ctxerr.Wrap(ctx, err, "getting software category ids")
}

if len(catIDs) != len(appID.Categories) {
return 0, &fleet.BadRequestError{
return 0, "", &fleet.BadRequestError{
Message: "some or all of the categories provided don't exist",
InternalErr: fmt.Errorf("categories provided: %v", appID.Categories),
}
Expand All @@ -930,27 +930,27 @@ func (svc *Service) AddAppStoreApp(ctx context.Context, teamID *uint, appID flee
if appID.Configuration != nil && appID.Platform == fleet.AndroidPlatform {
changed, err := svc.ds.HasAndroidAppConfigurationChanged(ctx, appID.AdamID, ptr.ValOrZero(teamID), appID.Configuration)
if err != nil {
return 0, ctxerr.Wrap(ctx, err, "checking android app configuration change")
return 0, "", ctxerr.Wrap(ctx, err, "checking android app configuration change")
}
androidConfigChanged = changed
}

addedApp, err := svc.ds.InsertVPPAppWithTeam(ctx, app, teamID)
if err != nil {
return 0, ctxerr.Wrap(ctx, err, "writing VPP app to db")
return 0, "", ctxerr.Wrap(ctx, err, "writing VPP app to db")
}
// If the original anchored country was orphaned (no token left), we
// re-anchor to the adding team's country. The country_code column is
// INSERT-only on insertVPPApps, so we explicitly UPDATE here.
if appID.Platform != fleet.AndroidPlatform && anchor.reAnchor && anchor.anchorCountry != "" {
if err := svc.ds.UpdateVPPAppCountryCode(ctx, app.AdamID, app.Platform, anchor.anchorCountry); err != nil {
return 0, ctxerr.Wrap(ctx, err, "re-anchoring vpp app country")
return 0, "", ctxerr.Wrap(ctx, err, "re-anchoring vpp app country")
}
}
if appID.Platform == fleet.AndroidPlatform {
err := worker.QueueMakeAndroidAppAvailableJob(ctx, svc.ds, svc.logger, appID.AdamID, addedApp.AppTeamID, androidEnterpriseName, androidConfigChanged)
if err != nil {
return 0, ctxerr.Wrap(ctx, err, "enqueuing job to make android app available")
return 0, "", ctxerr.Wrap(ctx, err, "enqueuing job to make android app available")
}
}

Expand All @@ -971,7 +971,7 @@ func (svc *Service) AddAppStoreApp(ctx context.Context, teamID *uint, appID flee
}

if err := svc.NewActivity(ctx, authz.UserFromContext(ctx), act); err != nil {
return 0, ctxerr.Wrap(ctx, err, "create activity for add app store app")
return 0, "", ctxerr.Wrap(ctx, err, "create activity for add app store app")
}

if appID.AddAutoInstallPolicy && app.AddedAutomaticInstallPolicy != nil {
Expand All @@ -986,7 +986,7 @@ func (svc *Service) AddAppStoreApp(ctx context.Context, teamID *uint, appID flee

}

return addedApp.TitleID, nil
return addedApp.TitleID, app.Name, nil

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fleet's MySQL layer doesn't normalize VPP app names on insert (no triggers, no column transforms). The value you pass in is the value that gets stored. Adding a re-read after write is an extra DB round-trip to guard against something that doesn't happen.

}

func (svc *Service) getVPPConfig(ctx context.Context) apple_apps.Config {
Expand Down
4 changes: 2 additions & 2 deletions server/fleet/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -794,8 +794,8 @@ type Service interface {

GetAppStoreApps(ctx context.Context, teamID *uint) ([]*VPPApp, error)

// AddAppStoreApp persists a VPP app onto a team and returns the resulting title ID
AddAppStoreApp(ctx context.Context, teamID *uint, appTeam VPPAppTeam) (uint, error)
// AddAppStoreApp persists a VPP app onto a team and returns the resulting title ID and app name
AddAppStoreApp(ctx context.Context, teamID *uint, appTeam VPPAppTeam) (uint, string, error)
UpdateAppStoreApp(ctx context.Context, titleID uint, teamID *uint, payload AppStoreAppUpdatePayload) (*VPPAppStoreApp, *ActivityEditedAppStoreApp, error)

// GetInHouseAppManifest returns a manifest XML file that points at the download URL for the given in-house app.
Expand Down
4 changes: 2 additions & 2 deletions server/mock/service/service_mock.go
Original file line number Diff line number Diff line change
Expand Up @@ -504,7 +504,7 @@ type HasSelfServiceSoftwareInstallersFunc func(ctx context.Context, host *fleet.

type GetAppStoreAppsFunc func(ctx context.Context, teamID *uint) ([]*fleet.VPPApp, error)

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

type UpdateAppStoreAppFunc func(ctx context.Context, titleID uint, teamID *uint, payload fleet.AppStoreAppUpdatePayload) (*fleet.VPPAppStoreApp, *fleet.ActivityEditedAppStoreApp, error)

Expand Down Expand Up @@ -3983,7 +3983,7 @@ func (s *Service) GetAppStoreApps(ctx context.Context, teamID *uint) ([]*fleet.V
return s.GetAppStoreAppsFunc(ctx, teamID)
}

func (s *Service) AddAppStoreApp(ctx context.Context, teamID *uint, appTeam fleet.VPPAppTeam) (uint, error) {
func (s *Service) AddAppStoreApp(ctx context.Context, teamID *uint, appTeam fleet.VPPAppTeam) (uint, string, error) {
s.mu.Lock()
s.AddAppStoreAppFuncInvoked = true
s.mu.Unlock()
Expand Down
2 changes: 2 additions & 0 deletions server/service/integration_android_software_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,7 @@ func (s *integrationMDMTestSuite) TestAndroidAppsSelfService() {
http.StatusOK,
&addAppResp,
)
require.Equal(t, androidAppNewTeam2.Name, addAppResp.Name)

s.DoJSON("GET", "/api/latest/fleet/software/titles", nil, http.StatusOK, &listSWTitles, "team_id", fmt.Sprint(team.ID))
s.Assert().Len(listSWTitles.SoftwareTitles, 2)
Expand Down Expand Up @@ -388,6 +389,7 @@ func (s *integrationMDMTestSuite) TestAndroidAppsSelfService() {
http.StatusOK,
&appWithConfigResp,
)
require.Equal(t, androidAppWithConfig.Name, appWithConfigResp.Name)

// Verify that activity includes configuration
s.lastActivityMatches(fleet.ActivityAddedAppStoreApp{}.ActivityName(),
Expand Down
2 changes: 1 addition & 1 deletion server/service/software_installers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ func TestSoftwareInstallersAuth(t *testing.T) {
checkAuthErr(t, true, err)
}

_, err = svc.AddAppStoreApp(ctx, tt.teamID, fleet.VPPAppTeam{VPPAppID: fleet.VPPAppID{AdamID: "123", Platform: fleet.IOSPlatform}})
_, _, err = svc.AddAppStoreApp(ctx, tt.teamID, fleet.VPPAppTeam{VPPAppID: fleet.VPPAppID{AdamID: "123", Platform: fleet.IOSPlatform}})
if tt.teamID == nil {
require.Error(t, err)
} else if tt.shouldFailWrite {
Expand Down
13 changes: 7 additions & 6 deletions server/service/vpp.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,15 +65,16 @@ type addAppStoreAppRequest struct {
}

type addAppStoreAppResponse struct {
TitleID uint `json:"software_title_id,omitempty"`
Err error `json:"error,omitempty"`
TitleID uint `json:"software_title_id,omitempty"`
Name string `json:"name,omitempty"`

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need a docs update for this?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will do separate PR into 4.86.0 docs

Err error `json:"error,omitempty"`
}

func (r addAppStoreAppResponse) Error() error { return r.Err }

func addAppStoreAppEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) {
req := request.(*addAppStoreAppRequest)
titleID, err := svc.AddAppStoreApp(ctx, req.TeamID, fleet.VPPAppTeam{
titleID, name, err := svc.AddAppStoreApp(ctx, req.TeamID, fleet.VPPAppTeam{
VPPAppID: fleet.VPPAppID{AdamID: req.AppStoreID, Platform: req.Platform},
SelfService: req.SelfService,
LabelsIncludeAny: req.LabelsIncludeAny,
Expand All @@ -87,15 +88,15 @@ func addAppStoreAppEndpoint(ctx context.Context, request interface{}, svc fleet.
return &addAppStoreAppResponse{Err: err}, nil
}

return &addAppStoreAppResponse{TitleID: titleID}, nil
return &addAppStoreAppResponse{TitleID: titleID, Name: name}, nil
}

func (svc *Service) AddAppStoreApp(ctx context.Context, _ *uint, _ fleet.VPPAppTeam) (uint, error) {
func (svc *Service) AddAppStoreApp(ctx context.Context, _ *uint, _ fleet.VPPAppTeam) (uint, string, error) {
// skipauth: No authorization check needed due to implementation returning
// only license error.
svc.authz.SkipAuthorization(ctx)

return 0, fleet.ErrMissingLicense
return 0, "", fleet.ErrMissingLicense
}

//////////////////////////////////////////////////////////////////////////////
Expand Down
2 changes: 1 addition & 1 deletion server/service/vpp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ func TestVPPAuth(t *testing.T) {
checkAuthErr(t, tt.shouldFailRead, err)
}

_, err = svc.AddAppStoreApp(ctx, tt.teamID, fleet.VPPAppTeam{VPPAppID: fleet.VPPAppID{AdamID: "123", Platform: fleet.IOSPlatform}})
_, _, err = svc.AddAppStoreApp(ctx, tt.teamID, fleet.VPPAppTeam{VPPAppID: fleet.VPPAppID{AdamID: "123", Platform: fleet.IOSPlatform}})

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's being tested elsewhere: server/service/integration_android_software_test.go in TestAndroidAppsSelfService, lines ~307 and ~392

if tt.teamID == nil {
require.Error(t, err)
} else {
Expand Down
Loading