mirror of
https://github.com/fleetdm/fleet
synced 2026-05-23 17:08:53 +00:00
35493 Android software configurations API endpoints (#36096)
**Related issue:** Resolves #35493 Notes: - Currently this adds a new function `updateAndroidAppConfigurationTx` that uses a passed transaction to stay consistent with how uploading/editing vpp apps treats display names and custom icons. - In some places configuration uses `omitempty` to use `json.RawMessage` but avoid it being set to "null" in requests/respones. # Checklist for submitter ## 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 - Can add an app with empty configuration {} - Can delete the app, and configuration deletes as well - Can't add app with invalid configuration - "reason": "Couldn't update configuration. Only \"managedConfiguration\" and \"workProfileWidgets\" are supported as top-level keys." - Can add an app with a good configuration - Can edit app and change the configuration to something valid, invalid gives error For unreleased bug fixes in a release candidate, one of: - [ ] Confirmed that the fix is not expected to adversely impact load test results - [ ] Alerted the release DRI if additional load testing is needed
This commit is contained in:
parent
8ddb92de23
commit
c274ebc63b
10 changed files with 266 additions and 27 deletions
|
|
@ -449,6 +449,9 @@ func (svc *Service) AddAppStoreApp(ctx context.Context, teamID *uint, appID flee
|
|||
|
||||
assetMD := assetMetadata[asset.AdamID]
|
||||
|
||||
// Configuration is an Android only feature
|
||||
appID.Configuration = nil
|
||||
|
||||
platforms := getPlatformsFromSupportedDevices(assetMD.SupportedDevices)
|
||||
if _, ok := platforms[appID.Platform]; !ok {
|
||||
return 0, fleet.NewInvalidArgumentError("app_store_id", fmt.Sprintf("%s isn't available for %s", assetMD.TrackName, appID.Platform))
|
||||
|
|
@ -512,6 +515,7 @@ func (svc *Service) AddAppStoreApp(ctx context.Context, teamID *uint, appID flee
|
|||
SelfService: app.SelfService,
|
||||
LabelsIncludeAny: actLabelsIncl,
|
||||
LabelsExcludeAny: actLabelsExcl,
|
||||
Configuration: app.Configuration,
|
||||
}
|
||||
|
||||
if err := svc.NewActivity(ctx, authz.UserFromContext(ctx), act); err != nil {
|
||||
|
|
@ -604,7 +608,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, displayName *string) (*fleet.VPPAppStoreApp, error) {
|
||||
func (svc *Service) UpdateAppStoreApp(ctx context.Context, titleID uint, teamID *uint, payload fleet.AppStoreAppUpdatePayload) (*fleet.VPPAppStoreApp, error) {
|
||||
if err := svc.authz.Authorize(ctx, &fleet.VPPApp{TeamID: teamID}, fleet.ActionWrite); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -623,9 +627,9 @@ func (svc *Service) UpdateAppStoreApp(ctx context.Context, titleID uint, teamID
|
|||
}
|
||||
|
||||
var validatedLabels *fleet.LabelIdentsWithScope
|
||||
if labelsExcludeAny != nil || labelsIncludeAny != nil {
|
||||
if payload.LabelsExcludeAny != nil || payload.LabelsIncludeAny != nil {
|
||||
var err error
|
||||
validatedLabels, err = ValidateSoftwareLabels(ctx, svc, labelsIncludeAny, labelsExcludeAny)
|
||||
validatedLabels, err = ValidateSoftwareLabels(ctx, svc, payload.LabelsIncludeAny, payload.LabelsExcludeAny)
|
||||
if err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "UpdateAppStoreApp: validating software labels")
|
||||
}
|
||||
|
|
@ -637,8 +641,8 @@ func (svc *Service) UpdateAppStoreApp(ctx context.Context, titleID uint, teamID
|
|||
}
|
||||
|
||||
selfServiceVal := meta.SelfService
|
||||
if selfService != nil {
|
||||
selfServiceVal = *selfService
|
||||
if payload.SelfService != nil {
|
||||
selfServiceVal = *payload.SelfService
|
||||
}
|
||||
|
||||
appToWrite := &fleet.VPPApp{
|
||||
|
|
@ -648,7 +652,8 @@ func (svc *Service) UpdateAppStoreApp(ctx context.Context, titleID uint, teamID
|
|||
},
|
||||
SelfService: selfServiceVal,
|
||||
ValidatedLabels: validatedLabels,
|
||||
DisplayName: displayName,
|
||||
DisplayName: payload.DisplayName,
|
||||
Configuration: payload.Configuration,
|
||||
},
|
||||
TeamID: teamID,
|
||||
TitleID: titleID,
|
||||
|
|
@ -660,23 +665,27 @@ func (svc *Service) UpdateAppStoreApp(ctx context.Context, titleID uint, teamID
|
|||
appToWrite.IconURL = *meta.IconURL
|
||||
}
|
||||
|
||||
if categories != nil {
|
||||
categories = server.RemoveDuplicatesFromSlice(categories)
|
||||
catIDs, err := svc.ds.GetSoftwareCategoryIDs(ctx, categories)
|
||||
if payload.Categories != nil {
|
||||
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(categories) {
|
||||
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", categories),
|
||||
InternalErr: fmt.Errorf("categories provided: %v", payload.Categories),
|
||||
}
|
||||
}
|
||||
|
||||
appToWrite.CategoryIDs = catIDs
|
||||
}
|
||||
|
||||
if payload.Configuration != nil {
|
||||
appToWrite.Configuration = payload.Configuration
|
||||
}
|
||||
|
||||
// check if labels have changed
|
||||
var existingLabels fleet.LabelIdentsWithScope
|
||||
switch {
|
||||
|
|
@ -738,7 +747,7 @@ func (svc *Service) UpdateAppStoreApp(ctx context.Context, titleID uint, teamID
|
|||
|
||||
actLabelsIncl, actLabelsExcl := activitySoftwareLabelsFromValidatedLabels(validatedLabels)
|
||||
|
||||
displayNameVal := ptr.ValOrZero(displayName)
|
||||
displayNameVal := ptr.ValOrZero(payload.DisplayName)
|
||||
|
||||
act := fleet.ActivityEditedAppStoreApp{
|
||||
TeamName: &teamName,
|
||||
|
|
@ -752,6 +761,7 @@ func (svc *Service) UpdateAppStoreApp(ctx context.Context, titleID uint, teamID
|
|||
LabelsExcludeAny: actLabelsExcl,
|
||||
SoftwareIconURL: meta.IconURL,
|
||||
SoftwareDisplayName: displayNameVal,
|
||||
Configuration: appToWrite.Configuration,
|
||||
}
|
||||
if err := svc.NewActivity(ctx, authz.UserFromContext(ctx), act); err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "create activity for update app store app")
|
||||
|
|
|
|||
|
|
@ -1624,3 +1624,35 @@ func (ds *Datastore) DeleteAndroidAppConfiguration(ctx context.Context, appID st
|
|||
|
||||
return nil
|
||||
}
|
||||
|
||||
// updateAndroidAppConfigurationTx inserts or updates an app configuration using a transaction
|
||||
func (ds *Datastore) updateAndroidAppConfigurationTx(ctx context.Context, tx sqlx.ExtContext, teamID *uint, appID string, config json.RawMessage) error {
|
||||
err := fleet.ValidateAndroidAppConfiguration(config)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "validating android app configuration")
|
||||
}
|
||||
|
||||
var tid *uint
|
||||
var globalOrTeamID uint
|
||||
if teamID != nil {
|
||||
globalOrTeamID = *teamID
|
||||
|
||||
if *teamID > 0 {
|
||||
tid = teamID
|
||||
}
|
||||
}
|
||||
|
||||
stmt := `
|
||||
INSERT INTO
|
||||
android_app_configurations (application_id, team_id, global_or_team_id, configuration)
|
||||
VALUES (?, ?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
configuration = VALUES(configuration)
|
||||
`
|
||||
|
||||
_, err = tx.ExecContext(ctx, stmt, appID, tid, globalOrTeamID, config)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "updateAndroidAppConfiguration")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -59,6 +59,7 @@ func TestAndroid(t *testing.T) {
|
|||
{"InsertAndroidAppConfiguration_Duplicate", testInsertAndroidAppConfigurationDuplicate},
|
||||
{"AndroidAppConfiguration_CascadeDeleteTeam", testAndroidAppConfigurationCascadeDeleteTeam},
|
||||
{"AndroidAppConfiguration_GlobalVsTeam", testAndroidAppConfigurationGlobalVsTeam},
|
||||
{"AddDeleteAndroidAppWithConfiguration", testAddDeleteAndroidAppWithConfiguration},
|
||||
}
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
|
|
@ -2538,3 +2539,71 @@ func testAndroidAppConfigurationGlobalVsTeam(t *testing.T, ds *Datastore) {
|
|||
require.Equal(t, teamID, *retrievedTeam.TeamID)
|
||||
require.Equal(t, teamID, retrievedTeam.GlobalOrTeamID)
|
||||
}
|
||||
|
||||
func testAddDeleteAndroidAppWithConfiguration(t *testing.T, ds *Datastore) {
|
||||
ctx := context.Background()
|
||||
|
||||
team1, err := ds.NewTeam(ctx, &fleet.Team{Name: "team1"})
|
||||
require.NoError(t, err)
|
||||
|
||||
test.CreateInsertGlobalVPPToken(t, ds)
|
||||
|
||||
testConfig := json.RawMessage(`{"ManagedConfiguration": {"DisableShareScreen": true, "DisableComputerAudio": true}}`)
|
||||
// Create android and VPP apps
|
||||
app1, err := ds.InsertVPPAppWithTeam(ctx, &fleet.VPPApp{
|
||||
Name: "android1", BundleIdentifier: "android1",
|
||||
VPPAppTeam: fleet.VPPAppTeam{VPPAppID: fleet.VPPAppID{AdamID: "something_android_app_1", Platform: fleet.AndroidPlatform},
|
||||
Configuration: testConfig,
|
||||
}}, &team1.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
app2, err := ds.InsertVPPAppWithTeam(ctx, &fleet.VPPApp{
|
||||
Name: "vpp1", BundleIdentifier: "com.app.vpp1",
|
||||
VPPAppTeam: fleet.VPPAppTeam{VPPAppID: fleet.VPPAppID{AdamID: "adam_vpp_app_forapple_1", Platform: fleet.IOSPlatform},
|
||||
Configuration: json.RawMessage(`{"ManagedConfiguration": {"ios app shouldn't have configuration": true}}`),
|
||||
}}, &team1.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Get android app without team
|
||||
meta, err := ds.GetVPPAppMetadataByTeamAndTitleID(ctx, nil, app1.TitleID)
|
||||
require.NoError(t, err)
|
||||
require.Zero(t, meta.Configuration)
|
||||
|
||||
// Get android app and configuration
|
||||
meta, err = ds.GetVPPAppMetadataByTeamAndTitleID(ctx, &team1.ID, app1.TitleID)
|
||||
require.NoError(t, err)
|
||||
require.NotZero(t, meta.VPPAppsTeamsID)
|
||||
require.NotZero(t, meta.Configuration)
|
||||
require.Equal(t, "android1", meta.BundleIdentifier)
|
||||
require.Equal(t, testConfig, meta.Configuration)
|
||||
|
||||
// Get ios app
|
||||
meta2, err := ds.GetVPPAppMetadataByTeamAndTitleID(ctx, nil, app2.TitleID)
|
||||
require.NoError(t, err)
|
||||
require.NotZero(t, meta2.VPPAppsTeamsID)
|
||||
|
||||
// Edit android app
|
||||
newConfig := json.RawMessage(`{"workProfileWidgets": "WORK_PROFILE_WIDGETS_ALLOWED"}`)
|
||||
app1.VPPAppTeam.Configuration = newConfig
|
||||
_, err = ds.InsertVPPAppWithTeam(ctx, app1, &team1.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Check that configuration was changed
|
||||
meta, err = ds.GetVPPAppMetadataByTeamAndTitleID(ctx, &team1.ID, app1.TitleID)
|
||||
require.NoError(t, err)
|
||||
require.NotZero(t, meta.VPPAppsTeamsID)
|
||||
require.Equal(t, newConfig, meta.Configuration)
|
||||
|
||||
// Add invalid configuration
|
||||
badConfig := json.RawMessage(`"-": "-"`)
|
||||
app1.VPPAppTeam.Configuration = badConfig
|
||||
_, err = ds.InsertVPPAppWithTeam(ctx, app1, &team1.ID)
|
||||
require.Error(t, err)
|
||||
|
||||
// Delete app, should delete configuration
|
||||
require.NoError(t, ds.DeleteVPPAppFromTeam(ctx, &team1.ID, app1.VPPAppID))
|
||||
_, err = ds.GetVPPAppMetadataByTeamAndTitleID(ctx, &team1.ID, app1.TitleID)
|
||||
require.ErrorContains(t, err, "not found")
|
||||
_, err = ds.GetAndroidAppConfiguration(ctx, app1.AdamID, team1.ID)
|
||||
require.ErrorContains(t, err, "not found")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -96,6 +96,14 @@ WHERE
|
|||
|
||||
app.DisplayName = displayName
|
||||
|
||||
config, err := ds.GetAndroidAppConfiguration(ctx, app.AdamID, tmID) // tmID can be used as globalOrTeamID
|
||||
if err != nil && !fleet.IsNotFound(err) {
|
||||
return nil, ctxerr.Wrap(ctx, err, "get android configuration for app store app")
|
||||
}
|
||||
if config != nil && config.Configuration != nil {
|
||||
app.Configuration = config.Configuration
|
||||
}
|
||||
|
||||
if teamID != nil {
|
||||
policies, err := ds.getPoliciesBySoftwareTitleIDs(ctx, []uint{titleID}, *teamID)
|
||||
if err != nil {
|
||||
|
|
@ -642,6 +650,12 @@ func (ds *Datastore) InsertVPPAppWithTeam(ctx context.Context, app *fleet.VPPApp
|
|||
}
|
||||
}
|
||||
|
||||
if app.Configuration != nil && app.Platform == fleet.AndroidPlatform {
|
||||
if err := ds.updateAndroidAppConfigurationTx(ctx, tx, teamID, app.AdamID, app.Configuration); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "setting configuration for android app")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
|
|
@ -905,6 +919,14 @@ func (ds *Datastore) DeleteVPPAppFromTeam(ctx context.Context, teamID *uint, app
|
|||
return notFound("VPPApp").WithMessage(fmt.Sprintf("adam id %s platform %s for team id %d", appID.AdamID, appID.Platform,
|
||||
globalOrTeamID))
|
||||
}
|
||||
|
||||
if appID.Platform == fleet.AndroidPlatform {
|
||||
err := ds.DeleteAndroidAppConfiguration(ctx, appID.AdamID, globalOrTeamID)
|
||||
if err != nil && !fleet.IsNotFound(err) {
|
||||
return ctxerr.Wrap(ctx, err, "deleting android app configuration")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2262,6 +2262,7 @@ type ActivityAddedAppStoreApp struct {
|
|||
SelfService bool `json:"self_service"`
|
||||
LabelsIncludeAny []ActivitySoftwareLabel `json:"labels_include_any,omitempty"`
|
||||
LabelsExcludeAny []ActivitySoftwareLabel `json:"labels_exclude_any,omitempty"`
|
||||
Configuration json.RawMessage `json:"configuration,omitempty"`
|
||||
}
|
||||
|
||||
func (a ActivityAddedAppStoreApp) ActivityName() string {
|
||||
|
|
@ -2413,6 +2414,7 @@ type ActivityEditedAppStoreApp struct {
|
|||
LabelsIncludeAny []ActivitySoftwareLabel `json:"labels_include_any,omitempty"`
|
||||
LabelsExcludeAny []ActivitySoftwareLabel `json:"labels_exclude_any,omitempty"`
|
||||
SoftwareDisplayName string `json:"software_display_name"`
|
||||
Configuration json.RawMessage `json:"configuration,omitempty"`
|
||||
}
|
||||
|
||||
func (a ActivityEditedAppStoreApp) ActivityName() string {
|
||||
|
|
|
|||
|
|
@ -728,7 +728,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, displayName *string) (*VPPAppStoreApp, error)
|
||||
UpdateAppStoreApp(ctx context.Context, titleID uint, teamID *uint, payload AppStoreAppUpdatePayload) (*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)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package fleet
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
|
@ -47,6 +48,9 @@ type VPPAppTeam struct {
|
|||
// app creation if AddAutoInstallPolicy is true.
|
||||
AddedAutomaticInstallPolicy *Policy `json:"-"`
|
||||
DisplayName *string `json:"display_name"`
|
||||
// Configuration is a json file used to customize Android app
|
||||
// behavior/settings. Relevant to Android only.
|
||||
Configuration json.RawMessage `json:"configuration,omitempty"`
|
||||
}
|
||||
|
||||
// VPPApp represents a VPP (Volume Purchase Program) application,
|
||||
|
|
@ -102,8 +106,9 @@ 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"`
|
||||
DisplayName string `json:"display_name"`
|
||||
Categories []string `json:"categories"`
|
||||
DisplayName string `json:"display_name"`
|
||||
Configuration json.RawMessage `json:"configuration"`
|
||||
}
|
||||
|
||||
// VPPAppStatusSummary represents aggregated status metrics for a VPP app.
|
||||
|
|
@ -138,3 +143,12 @@ const (
|
|||
DefaultVPPInstallVerifyTimeout = 10 * time.Minute
|
||||
DefaultVPPVerifyRequestDelay = 5 * time.Second
|
||||
)
|
||||
|
||||
type AppStoreAppUpdatePayload struct {
|
||||
SelfService *bool
|
||||
LabelsIncludeAny []string
|
||||
LabelsExcludeAny []string
|
||||
Categories []string
|
||||
DisplayName *string
|
||||
Configuration json.RawMessage
|
||||
}
|
||||
|
|
|
|||
|
|
@ -470,7 +470,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, displayName *string) (*fleet.VPPAppStoreApp, error)
|
||||
type UpdateAppStoreAppFunc func(ctx context.Context, titleID uint, teamID *uint, payload fleet.AppStoreAppUpdatePayload) (*fleet.VPPAppStoreApp, error)
|
||||
|
||||
type GetInHouseAppManifestFunc func(ctx context.Context, titleID uint, teamID *uint) ([]byte, error)
|
||||
|
||||
|
|
@ -3719,11 +3719,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, displayName *string) (*fleet.VPPAppStoreApp, error) {
|
||||
func (s *Service) UpdateAppStoreApp(ctx context.Context, titleID uint, teamID *uint, payload fleet.AppStoreAppUpdatePayload) (*fleet.VPPAppStoreApp, error) {
|
||||
s.mu.Lock()
|
||||
s.UpdateAppStoreAppFuncInvoked = true
|
||||
s.mu.Unlock()
|
||||
return s.UpdateAppStoreAppFunc(ctx, titleID, teamID, selfService, labelsIncludeAny, labelsExcludeAny, categories, displayName)
|
||||
return s.UpdateAppStoreAppFunc(ctx, titleID, teamID, payload)
|
||||
}
|
||||
|
||||
func (s *Service) GetInHouseAppManifest(ctx context.Context, titleID uint, teamID *uint) ([]byte, error) {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package service
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
|
@ -221,4 +222,82 @@ func (s *integrationMDMTestSuite) TestAndroidAppSelfService() {
|
|||
// Should have hit the android API endpoint
|
||||
s.Assert().True(s.androidAPIClient.EnterprisesPoliciesModifyPolicyApplicationsFuncInvoked)
|
||||
|
||||
// Test Android app configurations
|
||||
|
||||
// Android app with configuration
|
||||
appConfiguration := json.RawMessage(`{"workProfileWidgets": "WORK_PROFILE_WIDGETS_ALLOWED"}`)
|
||||
androidAppWithConfig := &fleet.VPPApp{
|
||||
VPPAppTeam: fleet.VPPAppTeam{
|
||||
VPPAppID: fleet.VPPAppID{
|
||||
AdamID: "com.fooooooo",
|
||||
Platform: fleet.AndroidPlatform,
|
||||
},
|
||||
Configuration: appConfiguration,
|
||||
},
|
||||
Name: "foo",
|
||||
BundleIdentifier: "com.fooooooo",
|
||||
IconURL: "https://example.com/images/2",
|
||||
}
|
||||
|
||||
// Add Android app
|
||||
var appWithConfigResp addAppStoreAppResponse
|
||||
s.DoJSON(
|
||||
"POST",
|
||||
"/api/latest/fleet/software/app_store_apps",
|
||||
&addAppStoreAppRequest{
|
||||
AppStoreID: androidAppWithConfig.AdamID,
|
||||
Platform: androidAppWithConfig.VPPAppID.Platform,
|
||||
Configuration: androidAppWithConfig.Configuration,
|
||||
},
|
||||
http.StatusOK,
|
||||
&appWithConfigResp,
|
||||
)
|
||||
|
||||
// Verify that activity includes configuration
|
||||
s.lastActivityMatches(fleet.ActivityAddedAppStoreApp{}.ActivityName(),
|
||||
fmt.Sprintf(`{"team_name": "%s", "software_title": "%s", "software_title_id": %d, "app_store_id": "%s", "team_id": %s, "platform": "%s", "self_service": true,"configuration": %s}`,
|
||||
"", "Test App", appWithConfigResp.TitleID, androidAppWithConfig.AdamID, "null", androidAppWithConfig.Platform, androidAppWithConfig.Configuration), 0)
|
||||
|
||||
// Should see it in host software library
|
||||
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d/software", host1.ID), nil, http.StatusOK, &getHostSw, "available_for_install", "true")
|
||||
assert.Len(t, getHostSw.Software, 2)
|
||||
s.Assert().NotNil(getHostSw.Software[1].AppStoreApp)
|
||||
s.Assert().Equal(androidAppWithConfig.AdamID, getHostSw.Software[1].AppStoreApp.AppStoreID)
|
||||
|
||||
// Edit app without changing configuration
|
||||
s.DoJSON(
|
||||
"PATCH",
|
||||
fmt.Sprintf("/api/latest/fleet/software/titles/%d/app_store_app", appWithConfigResp.TitleID),
|
||||
&updateAppStoreAppRequest{},
|
||||
http.StatusOK,
|
||||
&addAppResp,
|
||||
)
|
||||
s.lastActivityMatches(fleet.ActivityEditedAppStoreApp{}.ActivityName(), "", 0)
|
||||
|
||||
var titleWithConfigResp getSoftwareTitleResponse
|
||||
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/software/titles/%d", appWithConfigResp.TitleID), &getSoftwareTitleRequest{
|
||||
ID: appWithConfigResp.TitleID,
|
||||
TeamID: nil,
|
||||
}, http.StatusOK, &titleWithConfigResp)
|
||||
|
||||
var responseConf map[string]any
|
||||
require.NoError(t, json.Unmarshal(titleWithConfigResp.SoftwareTitle.AppStoreApp.Configuration, &responseConf))
|
||||
require.Contains(t, responseConf, "workProfileWidgets")
|
||||
|
||||
// Edit app and change configuration
|
||||
newConfig := json.RawMessage(`{"managedConfiguration": {"key": "value"}}`)
|
||||
s.DoJSON(
|
||||
"PATCH",
|
||||
fmt.Sprintf("/api/latest/fleet/software/titles/%d/app_store_app", appWithConfigResp.TitleID),
|
||||
&updateAppStoreAppRequest{
|
||||
Configuration: newConfig,
|
||||
},
|
||||
http.StatusOK,
|
||||
&addAppResp,
|
||||
)
|
||||
|
||||
// Verify that configuration changed and last activity is correct
|
||||
s.lastActivityMatches(fleet.ActivityEditedAppStoreApp{}.ActivityName(),
|
||||
fmt.Sprintf(`{"team_name": "%s", "software_title": "%s", "software_icon_url":"https://example.com/1.jpg", "software_title_id": %d, "app_store_id": "%s", "team_id": %s, "software_display_name":"", "platform": "%s", "self_service": true,"configuration": %s}`,
|
||||
"", "Test App", appWithConfigResp.TitleID, androidAppWithConfig.AdamID, "null", androidAppWithConfig.Platform, newConfig), 0)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package service
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
|
|
@ -58,6 +59,7 @@ type addAppStoreAppRequest struct {
|
|||
LabelsIncludeAny []string `json:"labels_include_any"`
|
||||
LabelsExcludeAny []string `json:"labels_exclude_any"`
|
||||
Categories []string `json:"categories"`
|
||||
Configuration json.RawMessage `json:"configuration,omitempty"`
|
||||
}
|
||||
|
||||
type addAppStoreAppResponse struct {
|
||||
|
|
@ -76,6 +78,7 @@ func addAppStoreAppEndpoint(ctx context.Context, request interface{}, svc fleet.
|
|||
LabelsExcludeAny: req.LabelsExcludeAny,
|
||||
AddAutoInstallPolicy: req.AutomaticInstall,
|
||||
Categories: req.Categories,
|
||||
Configuration: req.Configuration,
|
||||
})
|
||||
if err != nil {
|
||||
return &addAppStoreAppResponse{Err: err}, nil
|
||||
|
|
@ -97,13 +100,14 @@ func (svc *Service) AddAppStoreApp(ctx context.Context, _ *uint, _ fleet.VPPAppT
|
|||
//////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
type updateAppStoreAppRequest struct {
|
||||
TitleID uint `url:"title_id"`
|
||||
TeamID *uint `json:"team_id"`
|
||||
SelfService *bool `json:"self_service"`
|
||||
LabelsIncludeAny []string `json:"labels_include_any"`
|
||||
LabelsExcludeAny []string `json:"labels_exclude_any"`
|
||||
Categories []string `json:"categories"`
|
||||
DisplayName *string `json:"display_name"`
|
||||
TitleID uint `url:"title_id"`
|
||||
TeamID *uint `json:"team_id"`
|
||||
SelfService *bool `json:"self_service"`
|
||||
LabelsIncludeAny []string `json:"labels_include_any"`
|
||||
LabelsExcludeAny []string `json:"labels_exclude_any"`
|
||||
Categories []string `json:"categories"`
|
||||
Configuration json.RawMessage `json:"configuration,omitempty"`
|
||||
DisplayName *string `json:"display_name"`
|
||||
}
|
||||
|
||||
type updateAppStoreAppResponse struct {
|
||||
|
|
@ -116,7 +120,14 @@ func (r updateAppStoreAppResponse) Error() error { return r.Err }
|
|||
func updateAppStoreAppEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) {
|
||||
req := request.(*updateAppStoreAppRequest)
|
||||
|
||||
updatedApp, err := svc.UpdateAppStoreApp(ctx, req.TitleID, req.TeamID, req.SelfService, req.LabelsIncludeAny, req.LabelsExcludeAny, req.Categories, req.DisplayName)
|
||||
updatedApp, err := svc.UpdateAppStoreApp(ctx, req.TitleID, req.TeamID, fleet.AppStoreAppUpdatePayload{
|
||||
SelfService: req.SelfService,
|
||||
LabelsIncludeAny: req.LabelsIncludeAny,
|
||||
LabelsExcludeAny: req.LabelsExcludeAny,
|
||||
Categories: req.Categories,
|
||||
Configuration: req.Configuration,
|
||||
DisplayName: req.DisplayName,
|
||||
})
|
||||
if err != nil {
|
||||
return updateAppStoreAppResponse{Err: err}, nil
|
||||
}
|
||||
|
|
@ -124,7 +135,7 @@ func updateAppStoreAppEndpoint(ctx context.Context, request interface{}, svc fle
|
|||
return updateAppStoreAppResponse{AppStoreApp: updatedApp}, nil
|
||||
}
|
||||
|
||||
func (svc *Service) UpdateAppStoreApp(ctx context.Context, titleID uint, teamID *uint, selfService *bool, labelsIncludeAny, labelsExcludeAny, categories []string, displayName *string) (*fleet.VPPAppStoreApp, error) {
|
||||
func (svc *Service) UpdateAppStoreApp(ctx context.Context, titleID uint, teamID *uint, payload fleet.AppStoreAppUpdatePayload) (*fleet.VPPAppStoreApp, error) {
|
||||
// skipauth: No authorization check needed due to implementation returning
|
||||
// only license error.
|
||||
svc.authz.SkipAuthorization(ctx)
|
||||
|
|
|
|||
Loading…
Reference in a new issue