Android App Configurations GitOps (#37188)

<!-- Add the related story/sub-task/bug number, like Resolves #123, or
remove if NA -->
**Related issue:** Resolves #35495
- Updates `generate-gitops` to export android app configurations in
relative files
- Updates backend to set the android app configurations state to what
yaml files specify
- If an existing configuration was not included, it will be set to `{}`

# 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
This commit is contained in:
Jonathan Katz
2025-12-15 15:57:45 -05:00
committed by GitHub
parent bc6b4b50bc
commit e0a3959192
10 changed files with 354 additions and 29 deletions
+21 -1
View File
@@ -2,6 +2,7 @@ package fleetctl
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"math"
@@ -1596,7 +1597,7 @@ func (cmd *GenerateGitopsCommand) generateSoftware(filePath string, teamID uint,
if downloadIcons && softwareTitle.IconUrl != nil && strings.HasPrefix(*softwareTitle.IconUrl, "/api") {
fileName := fmt.Sprintf("lib/%s/icons/%s", teamFilename, filenamePrefix+"-icon.png")
path := fmt.Sprintf("../%s", fileName)
softwareSpec["icon"] = map[string]interface{}{
softwareSpec["icon"] = map[string]any{
"path": path,
}
icon, err := cmd.Client.GetSoftwareTitleIcon(softwareTitle.ID, teamID)
@@ -1608,6 +1609,25 @@ func (cmd *GenerateGitopsCommand) generateSoftware(filePath string, teamID uint,
// TODO write files immediately rather than queueing them up
cmd.FilesToWrite[fileName] = icon
}
config := softwareTitle.AppStoreApp.Configuration
if config != nil && !slices.Equal(config, json.RawMessage("{}")) {
fileName := fmt.Sprintf("lib/%s/configs/%s", teamFilename, filenamePrefix+"-config.json")
path := fmt.Sprintf("../%s", fileName)
softwareSpec["configuration"] = map[string]any{
"path": path,
}
// format config because it is received with incorrect indentation
var buf bytes.Buffer
err := json.Indent(&buf, softwareTitle.AppStoreApp.Configuration, "", " ")
if err != nil {
fmt.Fprintf(cmd.CLI.App.ErrWriter, "Error formatting android app config %s: %s\n", sw.Name, err)
return nil, err
}
cmd.FilesToWrite[fileName] = buf.Bytes()
}
}
var labels []fleet.SoftwareScopeLabel
+2
View File
@@ -121,6 +121,7 @@ func (svc *Service) BatchAssociateVPPApps(ctx context.Context, teamName string,
LabelsIncludeAny: payload.LabelsIncludeAny,
Categories: payload.Categories,
DisplayName: payload.DisplayName,
Configuration: payload.Configuration,
})
}
@@ -178,6 +179,7 @@ func (svc *Service) BatchAssociateVPPApps(ctx context.Context, teamName string,
switch payload.Platform {
case fleet.AndroidPlatform:
appStoreApp.SelfService = true
appStoreApp.Configuration = payload.Configuration
incomingAndroidApps = append(incomingAndroidApps, appStoreApp)
case fleet.IOSPlatform, fleet.IPadOSPlatform, fleet.MacOSPlatform:
incomingAppleApps = append(incomingAppleApps, appStoreApp)
+3 -12
View File
@@ -12,6 +12,7 @@ import (
"github.com/fleetdm/fleet/v4/server/datastore/mysql/common_mysql"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/mdm/android"
"github.com/fleetdm/fleet/v4/server/ptr"
"github.com/go-kit/log/level"
"github.com/google/uuid"
"github.com/jmoiron/sqlx"
@@ -1645,7 +1646,7 @@ WHERE
}
// HasAndroidAppConfigurationChanged checks if the new configuration for an Android app
// identified by adam_id and global_or_team_id is different from the existing one. This
// identified by application_id and global_or_team_id is different from the existing one. This
// is a datastore method so that we rely on mysql's canonicalisation of JSON for comparison.
func (ds *Datastore) HasAndroidAppConfigurationChanged(ctx context.Context, applicationID string, globalOrTeamID uint, newConfig json.RawMessage) (bool, error) {
const stmt = `
@@ -1834,16 +1835,6 @@ func (ds *Datastore) updateAndroidAppConfigurationTx(ctx context.Context, tx sql
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)
@@ -1852,7 +1843,7 @@ func (ds *Datastore) updateAndroidAppConfigurationTx(ctx context.Context, tx sql
configuration = VALUES(configuration)
`
_, err = tx.ExecContext(ctx, stmt, appID, tid, globalOrTeamID, config)
_, err = tx.ExecContext(ctx, stmt, appID, teamID, ptr.ValOrZero(teamID), config)
if err != nil {
return ctxerr.Wrap(ctx, err, "updateAndroidAppConfiguration")
}
+25 -2
View File
@@ -433,7 +433,7 @@ func (ds *Datastore) SetTeamVPPApps(ctx context.Context, teamID *uint, incomingA
// upsert it if it does not exist or labels or SelfService or InstallDuringSetup flags are changed
existingApp, isExistingApp := existingApps[incomingApp.VPPAppID]
incomingApp.AppTeamID = existingApp.AppTeamID
var labelsChanged, categoriesChanged, displayNameChanged bool
var labelsChanged, categoriesChanged, displayNameChanged, configurationChanged bool
if isExistingApp {
existingLabels, err := ds.getExistingLabels(ctx, incomingApp.AppTeamID)
if err != nil {
@@ -453,6 +453,17 @@ func (ds *Datastore) SetTeamVPPApps(ctx context.Context, teamID *uint, incomingA
incomingDisplayName := ptr.ValOrZero(incomingApp.DisplayName)
displayNameChanged = existingDisplayName != incomingDisplayName
if incomingApp.Platform == fleet.AndroidPlatform {
configurationChanged, err = ds.HasAndroidAppConfigurationChanged(ctx, existingApp.AdamID, ptr.ValOrZero(teamID), incomingApp.Configuration)
if err != nil {
return ctxerr.Wrap(ctx, err, "getting existing configuration for android app")
}
// Set configuration to empty if it exists and is provided as null
if configurationChanged && len(incomingApp.Configuration) == 0 {
incomingApp.Configuration = json.RawMessage("{}")
}
}
}
// Get the hosts that are NOT in label scope currently (before the update happens)
@@ -470,6 +481,7 @@ func (ds *Datastore) SetTeamVPPApps(ctx context.Context, teamID *uint, incomingA
labelsChanged ||
categoriesChanged ||
displayNameChanged ||
configurationChanged ||
incomingApp.InstallDuringSetup != nil &&
existingApp.InstallDuringSetup != nil &&
*incomingApp.InstallDuringSetup != *existingApp.InstallDuringSetup {
@@ -483,7 +495,7 @@ func (ds *Datastore) SetTeamVPPApps(ctx context.Context, teamID *uint, incomingA
if teamID != nil && *teamID > 0 {
tm, err := ds.TeamLite(ctx, *teamID)
if err != nil {
return ctxerr.Wrap(ctx, err, "get team for VPP app conflict error")
return ctxerr.Wrap(ctx, err, "get team name for VPP app conflict error")
}
teamName = tm.Name
}
@@ -541,6 +553,12 @@ func (ds *Datastore) SetTeamVPPApps(ctx context.Context, teamID *uint, incomingA
}
}
if toAdd.Configuration != nil {
if err := ds.updateAndroidAppConfigurationTx(ctx, tx, teamID, toAdd.AdamID, toAdd.Configuration); err != nil {
return ctxerr.Wrap(ctx, err, "setting configuration for android app")
}
}
if hostsNotInScope, ok := appsWithChangedLabels[toAdd.AppTeamID]; ok {
hostsInScope, err := ds.GetIncludedHostIDMapForVPPAppTx(ctx, tx, toAdd.AppTeamID)
if err != nil {
@@ -827,6 +845,11 @@ func removeVPPAppTeams(ctx context.Context, tx sqlx.ExtContext, appID fleet.VPPA
return ctxerr.Wrap(ctx, err, "deleting vpp app from team")
}
_, err = tx.ExecContext(ctx, `DELETE FROM android_app_configurations WHERE application_id = ? AND global_or_team_id = ?`, appID.AdamID, tmID)
if err != nil {
return ctxerr.Wrap(ctx, err, "deleting android app configuration")
}
return nil
}
+83
View File
@@ -2,6 +2,7 @@ package mysql
import (
"context"
"encoding/json"
"fmt"
"testing"
"time"
@@ -41,6 +42,7 @@ func TestVPP(t *testing.T) {
{"TestGetUnverifiedVPPInstallsForHost", testGetUnverifiedVPPInstallsForHost},
{"SoftwareTitleDisplayName", testSoftwareTitleDisplayNameVPP},
{"AndroidVPPAppStatus", testAndroidVPPAppStatus},
{"AndroidAppConfigs", testAndroidAppConfigs},
}
for _, c := range cases {
@@ -2424,3 +2426,84 @@ func testAndroidVPPAppStatus(t *testing.T, ds *Datastore) {
require.Len(t, hosts, 1)
require.Equal(t, host3.Host.ID, hosts[0].ID)
}
func testAndroidAppConfigs(t *testing.T, ds *Datastore) {
ctx := context.Background()
// Create a team
team, err := ds.NewTeam(ctx, &fleet.Team{Name: "androids"})
require.NoError(t, err)
// Insert some VPP apps for no team
app1 := &fleet.VPPApp{Name: "android_app_1", VPPAppTeam: fleet.VPPAppTeam{VPPAppID: fleet.VPPAppID{AdamID: "1", Platform: fleet.AndroidPlatform}}, BundleIdentifier: "b1"}
_, err = ds.InsertVPPAppWithTeam(ctx, app1, nil)
require.NoError(t, err)
app2 := &fleet.VPPApp{Name: "android_app_2", VPPAppTeam: fleet.VPPAppTeam{VPPAppID: fleet.VPPAppID{AdamID: "2", Platform: fleet.AndroidPlatform}}, BundleIdentifier: "b2"}
_, err = ds.InsertVPPAppWithTeam(ctx, app2, nil)
require.NoError(t, err)
app3 := &fleet.VPPApp{Name: "android_app_3", VPPAppTeam: fleet.VPPAppTeam{VPPAppID: fleet.VPPAppID{AdamID: "3", Platform: fleet.AndroidPlatform}}, BundleIdentifier: "b3"}
_, err = ds.InsertVPPAppWithTeam(ctx, app3, nil)
require.NoError(t, err)
app4 := &fleet.VPPApp{Name: "android_app_4", VPPAppTeam: fleet.VPPAppTeam{VPPAppID: fleet.VPPAppID{AdamID: "4", Platform: fleet.AndroidPlatform}}, BundleIdentifier: "b4"}
_, err = ds.InsertVPPAppWithTeam(ctx, app4, nil)
require.NoError(t, err)
config1 := json.RawMessage(`{"workProfileWidgets":"WORK_PROFILE_WIDGETS_ALLOWED", "managedConfiguration": {"1":1}}`)
expectedConfig1 := json.RawMessage(`{"workProfileWidgets": "WORK_PROFILE_WIDGETS_ALLOWED", "managedConfiguration": {"1": 1}}`)
err = ds.SetTeamVPPApps(ctx, &team.ID, []fleet.VPPAppTeam{
{VPPAppID: app1.VPPAppID, SelfService: true, DisplayName: ptr.String("name 1")},
{VPPAppID: app2.VPPAppID, SelfService: true, DisplayName: ptr.String("name 2"), Configuration: json.RawMessage(nil)},
{VPPAppID: app3.VPPAppID, SelfService: true, DisplayName: ptr.String("name 3"), Configuration: json.RawMessage(`{}`)},
{VPPAppID: app4.VPPAppID, SelfService: true, DisplayName: ptr.String("name 4"), Configuration: config1},
}, map[string]uint{"1_android": 1, "2_android": 2, "3_android": 3, "4_android": 4})
require.NoError(t, err)
assigned, err := ds.GetAssignedVPPApps(ctx, &team.ID)
require.NoError(t, err)
require.Len(t, assigned, 4)
for _, a := range assigned {
config, err := ds.GetAndroidAppConfiguration(ctx, a.AdamID, team.ID)
require.True(t, err == nil || fleet.IsNotFound(err))
if config != nil {
a.Configuration = config.Configuration
assigned[a.VPPAppID] = a
}
}
require.Equal(t, json.RawMessage(nil), assigned[app1.VPPAppID].Configuration)
require.Equal(t, json.RawMessage(nil), assigned[app2.VPPAppID].Configuration)
require.Equal(t, json.RawMessage(`{}`), assigned[app3.VPPAppID].Configuration)
require.Equal(t, expectedConfig1, assigned[app4.VPPAppID].Configuration)
err = ds.SetTeamVPPApps(ctx, &team.ID, []fleet.VPPAppTeam{
{VPPAppID: app1.VPPAppID}, // stays nil
{VPPAppID: app2.VPPAppID, Configuration: json.RawMessage(`{"managedConfiguration": 1}`)}, // updates
{VPPAppID: app3.VPPAppID, Configuration: json.RawMessage(nil)},
{VPPAppID: app4.VPPAppID, Configuration: config1},
}, map[string]uint{"1": 1, "2": 2, "3": 3, "4": 4})
require.NoError(t, err)
assigned, err = ds.GetAssignedVPPApps(ctx, &team.ID)
require.NoError(t, err)
require.Len(t, assigned, 4)
for _, a := range assigned {
config, err := ds.GetAndroidAppConfiguration(ctx, a.AdamID, team.ID)
require.True(t, err == nil || fleet.IsNotFound(err))
if config != nil {
a.Configuration = config.Configuration
assigned[a.VPPAppID] = a
}
}
require.Equal(t, json.RawMessage(nil), assigned[app1.VPPAppID].Configuration)
require.Equal(t, json.RawMessage(`{"managedConfiguration": 1}`), assigned[app2.VPPAppID].Configuration)
require.Equal(t, json.RawMessage(`{}`), assigned[app3.VPPAppID].Configuration)
require.Equal(t, expectedConfig1, assigned[app4.VPPAppID].Configuration)
// Delete all
err = ds.SetTeamVPPApps(ctx, &team.ID, []fleet.VPPAppTeam{}, nil)
require.NoError(t, err)
}
+9 -7
View File
@@ -656,11 +656,12 @@ type VPPBatchPayload struct {
LabelsExcludeAny []string `json:"labels_exclude_any"`
LabelsIncludeAny []string `json:"labels_include_any"`
// Categories is the list of names of software categories associated with this VPP app.
Categories []string `json:"categories"`
DisplayName string `json:"display_name"`
IconPath string `json:"-"`
IconHash string `json:"-"`
Platform InstallableDevicePlatform `json:"platform"`
Categories []string `json:"categories"`
DisplayName string `json:"display_name"`
IconPath string `json:"-"`
IconHash string `json:"-"`
Platform InstallableDevicePlatform `json:"platform"`
Configuration json.RawMessage `json:"configuration,omitempty"`
}
func (v VPPBatchPayload) GetPlatform() string {
@@ -681,8 +682,9 @@ type VPPBatchPayloadWithPlatform struct {
// Categories is the list of names of software categories associated with this VPP app.
Categories []string `json:"categories"`
// CategoryIDs is the list of IDs of software categories associated with this VPP app.
CategoryIDs []uint `json:"-"`
DisplayName string `json:"display_name"`
CategoryIDs []uint `json:"-"`
DisplayName string `json:"display_name"`
Configuration json.RawMessage `json:"configuration,omitempty"`
}
type SoftwareCategory struct {
+2
View File
@@ -268,10 +268,12 @@ type TeamSpecAppStoreApp struct {
Icon TeamSpecSoftwareAsset `json:"icon"`
Platform string `json:"platform"`
DisplayName string `json:"display_name,omitempty"`
Configuration TeamSpecSoftwareAsset `json:"configuration"`
}
func (spec TeamSpecAppStoreApp) ResolvePaths(baseDir string) TeamSpecAppStoreApp {
spec.Icon.Path = resolveApplyRelativePath(baseDir, spec.Icon.Path)
spec.Configuration.Path = resolveApplyRelativePath(baseDir, spec.Configuration.Path)
return spec
}
+49 -4
View File
@@ -881,7 +881,15 @@ func (c *Client) ApplyGroup(
return nil, nil, nil, nil, fmt.Errorf("Couldn't edit app store app (%s). Invalid custom icon file %s: %w", app.AppStoreID, app.Icon.Path, err)
}
appPayloads = append(appPayloads, fleet.VPPBatchPayload{
var androidConfig json.RawMessage
if app.Platform == string(fleet.AndroidPlatform) {
androidConfig, err = getAndroidAppConfig(app.Configuration.Path)
if err != nil {
return nil, nil, nil, nil, fmt.Errorf("Couldn't edit app store app (%s). Reading configuration %s: %w", app.AppStoreID, app.Configuration.Path, err)
}
}
payload := fleet.VPPBatchPayload{
AppStoreID: app.AppStoreID,
SelfService: app.SelfService,
InstallDuringSetup: installDuringSetup,
@@ -892,7 +900,12 @@ func (c *Client) ApplyGroup(
IconPath: app.Icon.Path,
IconHash: iconHash,
Platform: fleet.InstallableDevicePlatform(app.Platform),
})
}
if androidConfig != nil {
payload.Configuration = androidConfig
}
appPayloads = append(appPayloads, payload)
// can be referenced by macos_setup.software.app_store_id
if tmSoftwareAppsByAppID[tmName] == nil {
tmSoftwareAppsByAppID[tmName] = make(map[string]fleet.TeamSpecAppStoreApp, len(apps))
@@ -1284,6 +1297,26 @@ func getIconHashIfValid(path string) (string, error) {
return hash, nil
}
func getAndroidAppConfig(path string) (json.RawMessage, error) {
if path == "" {
return nil, nil
}
configReader, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("reading android app configuration file: %w", err)
}
config := json.RawMessage(configReader)
err = fleet.ValidateAndroidAppConfiguration(config)
if err != nil {
return nil, err
}
return config, nil
}
func extractAppCfgMacOSSetup(appCfg any) *fleet.MacOSSetup {
asMap, ok := appCfg.(map[string]interface{})
if !ok {
@@ -2483,7 +2516,15 @@ func (c *Client) doGitOpsNoTeamSetupAndSoftware(
return nil, nil, fmt.Errorf("Couldn't edit app store app (%s). Invalid custom icon file %s: %w", vppApp.AppStoreID, vppApp.Icon.Path, err)
}
appsPayload = append(appsPayload, fleet.VPPBatchPayload{
var androidConfig json.RawMessage
if vppApp.Platform == string(fleet.AndroidPlatform) {
androidConfig, err = getAndroidAppConfig(vppApp.Configuration.Path)
if err != nil {
return nil, nil, fmt.Errorf("Couldn't edit app store app (%s). Reading configuration %s: %w", vppApp.AppStoreID, vppApp.Configuration.Path, err)
}
}
payload := fleet.VPPBatchPayload{
AppStoreID: vppApp.AppStoreID,
SelfService: vppApp.SelfService,
InstallDuringSetup: &installDuringSetup,
@@ -2491,7 +2532,11 @@ func (c *Client) doGitOpsNoTeamSetupAndSoftware(
IconPath: vppApp.Icon.Path,
IconHash: iconHash,
Platform: fleet.InstallableDevicePlatform(vppApp.Platform),
})
}
if androidConfig != nil {
payload.Configuration = androidConfig
}
appsPayload = append(appsPayload, payload)
}
}
@@ -277,9 +277,7 @@ func (s *integrationMDMTestSuite) TestAndroidAppsSelfService() {
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")
require.Contains(t, string(titleWithConfigResp.SoftwareTitle.AppStoreApp.Configuration), "workProfileWidgets")
// Edit app and change configuration
newConfig := json.RawMessage(`{"managedConfiguration": {"key": "value"}}`)
@@ -496,3 +494,161 @@ func (s *integrationMDMTestSuite) enableAndroidMDM(t *testing.T) string {
return enterpriseID
}
func (s *integrationMDMTestSuite) TestAndroidAppConfigurations() {
t := s.T()
appConf, err := s.ds.AppConfig(context.Background())
require.NoError(s.T(), err)
appConf.MDM.AndroidEnabledAndConfigured = false
err = s.ds.SaveAppConfig(context.Background(), appConf)
require.NoError(s.T(), err)
s.enableAndroidMDM(t)
s.androidAPIClient.EnterprisesApplicationsFunc = func(ctx context.Context, enterpriseName string, packageName string) (*androidmanagement.Application, error) {
return &androidmanagement.Application{IconUrl: "https://example.com/1.jpg", Title: "Test App"}, nil
}
var createTeamResp teamResponse
s.DoJSON("POST", "/api/latest/fleet/teams", &fleet.Team{
Name: t.Name(),
}, http.StatusOK, &createTeamResp)
require.NotZero(t, createTeamResp.Team.ID)
teamID := &createTeamResp.Team.ID
// Android app with configuration
exampleConfiguration := json.RawMessage(`{"workProfileWidgets":"WORK_PROFILE_WIDGETS_ALLOWED"}`)
androidAppFoo := &fleet.VPPApp{
VPPAppTeam: fleet.VPPAppTeam{
AppTeamID: ptr.ValOrZero(teamID),
VPPAppID: fleet.VPPAppID{
AdamID: "com.foo",
Platform: fleet.AndroidPlatform,
},
Configuration: exampleConfiguration,
},
}
// Add Android app
var appWithConfigResp addAppStoreAppResponse
s.DoJSON("POST", "/api/latest/fleet/software/app_store_apps",
&addAppStoreAppRequest{
TeamID: teamID,
AppStoreID: androidAppFoo.AdamID,
Platform: androidAppFoo.VPPAppID.Platform,
Configuration: androidAppFoo.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": %d, "platform": "%s", "self_service": true,"configuration": %s}`,
t.Name(), "Test App", appWithConfigResp.TitleID, androidAppFoo.AdamID, ptr.ValOrZero(teamID), androidAppFoo.Platform, androidAppFoo.Configuration), 0)
var listSWTitles listSoftwareTitlesResponse
s.DoJSON("GET", "/api/latest/fleet/software/titles", nil, http.StatusOK, &listSWTitles, "team_id", fmt.Sprint(*teamID))
s.Assert().Len(listSWTitles.SoftwareTitles, 1)
s.Assert().Equal(androidAppFoo.AdamID, listSWTitles.SoftwareTitles[0].AppStoreApp.AppStoreID)
// Batch app store apps call won't create an activity
var batchResp batchAssociateAppStoreAppsResponse
s.DoJSON("POST", "/api/latest/fleet/software/app_store_apps/batch",
batchAssociateAppStoreAppsRequest{
DryRun: false,
Apps: []fleet.VPPBatchPayload{
{AppStoreID: "app_1", SelfService: true, Platform: fleet.AndroidPlatform, Configuration: json.RawMessage("{}")},
{AppStoreID: "app_2", SelfService: true, Platform: fleet.AndroidPlatform, Configuration: json.RawMessage("{}")},
{AppStoreID: "app_3", SelfService: true, Platform: fleet.AndroidPlatform, Configuration: json.RawMessage("{}")},
{AppStoreID: "app_4", SelfService: true, Platform: fleet.AndroidPlatform, Configuration: exampleConfiguration},
},
},
http.StatusOK, &batchResp, "team_name", t.Name(),
)
s.DoJSON("GET", "/api/latest/fleet/software/titles", nil, http.StatusOK, &listSWTitles, "team_id", fmt.Sprint(*teamID))
s.Assert().Len(listSWTitles.SoftwareTitles, 4)
s.Assert().Equal("app_1", listSWTitles.SoftwareTitles[0].AppStoreApp.AppStoreID)
titleApp1 := listSWTitles.SoftwareTitles[0].ID
titleApp2 := listSWTitles.SoftwareTitles[1].ID
// Batch app store apps call won't create an activity
// Add apps to team 0
s.DoJSON("POST", "/api/latest/fleet/software/app_store_apps/batch",
batchAssociateAppStoreAppsRequest{
DryRun: false,
Apps: []fleet.VPPBatchPayload{
{AppStoreID: "app_1", SelfService: true, Platform: fleet.AndroidPlatform, Configuration: json.RawMessage("{}")},
{AppStoreID: "app_2", SelfService: true, Platform: fleet.AndroidPlatform, Configuration: json.RawMessage("{}")},
{AppStoreID: "app_3", SelfService: true, Platform: fleet.AndroidPlatform, Configuration: json.RawMessage("{}")},
{AppStoreID: "app_4", SelfService: true, Platform: fleet.AndroidPlatform, Configuration: json.RawMessage("{}")},
},
}, http.StatusOK, &batchResp,
)
s.DoJSON("GET", "/api/latest/fleet/software/titles", nil, http.StatusOK, &listSWTitles, "team_id", fmt.Sprint(0))
s.Assert().Len(listSWTitles.SoftwareTitles, 4)
// Update configurations
s.DoJSON("POST", "/api/latest/fleet/software/app_store_apps/batch",
batchAssociateAppStoreAppsRequest{
DryRun: false,
Apps: []fleet.VPPBatchPayload{
{AppStoreID: "app_1", Platform: fleet.AndroidPlatform, Configuration: nil},
{AppStoreID: "app_2", Platform: fleet.AndroidPlatform, Configuration: exampleConfiguration},
{AppStoreID: "app_3", Platform: fleet.AndroidPlatform, Configuration: json.RawMessage("{}")},
{AppStoreID: "app_4", Platform: fleet.AndroidPlatform, Configuration: json.RawMessage("{}")},
},
},
http.StatusOK, &batchResp, "team_name", t.Name(),
)
s.DoJSON("GET", "/api/latest/fleet/software/titles", nil, http.StatusOK, &listSWTitles, "team_id", fmt.Sprint(*teamID))
s.Assert().Len(listSWTitles.SoftwareTitles, 4)
var titleResp getSoftwareTitleResponse
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/software/titles/%d", titleApp1), &getSoftwareTitleRequest{
ID: titleApp1,
TeamID: teamID,
}, http.StatusOK, &titleResp)
require.Equal(t, "app_1", *titleResp.SoftwareTitle.ApplicationID)
require.Equal(t, json.RawMessage(`{}`), titleResp.SoftwareTitle.AppStoreApp.Configuration)
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/software/titles/%d", titleApp2), &getSoftwareTitleRequest{
ID: titleApp2,
TeamID: teamID,
}, http.StatusOK, &titleResp)
require.Equal(t, "app_2", *titleResp.SoftwareTitle.ApplicationID)
require.Contains(t, string(titleResp.SoftwareTitle.AppStoreApp.Configuration), `"workProfileWidgets": "WORK_PROFILE_WIDGETS_ALLOWED"`)
// Remove 2 other apps, 2 configurations should be deleted and 2 should be emptied/remain
s.DoJSON("POST", "/api/latest/fleet/software/app_store_apps/batch",
batchAssociateAppStoreAppsRequest{
DryRun: false,
Apps: []fleet.VPPBatchPayload{
{AppStoreID: "app_1", Platform: fleet.AndroidPlatform, Configuration: nil},
{AppStoreID: "app_2", Platform: fleet.AndroidPlatform, Configuration: exampleConfiguration},
},
},
http.StatusOK, &batchResp, "team_name", t.Name(),
)
s.DoJSON("GET", "/api/latest/fleet/software/titles", nil, http.StatusOK, &listSWTitles, "team_id", fmt.Sprint(*teamID))
s.Assert().Len(listSWTitles.SoftwareTitles, 2)
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/software/titles/%d", titleApp1), &getSoftwareTitleRequest{
ID: titleApp1,
TeamID: teamID,
}, http.StatusOK, &titleResp)
require.Equal(t, "app_1", *titleResp.SoftwareTitle.ApplicationID)
require.Equal(t, json.RawMessage(`{}`), titleResp.SoftwareTitle.AppStoreApp.Configuration)
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/software/titles/%d", titleApp2), &getSoftwareTitleRequest{
ID: titleApp2,
TeamID: teamID,
}, http.StatusOK, &titleResp)
require.Equal(t, "app_2", *titleResp.SoftwareTitle.ApplicationID)
require.Contains(t, string(titleResp.SoftwareTitle.AppStoreApp.Configuration), `"workProfileWidgets": "WORK_PROFILE_WIDGETS_ALLOWED"`)
}
@@ -141,3 +141,4 @@ github.com/fleetdm/fleet/v4/server/fleet/TeamSpecAppStoreApp InstallDuringSetup
github.com/fleetdm/fleet/v4/server/fleet/TeamSpecAppStoreApp Icon fleet.TeamSpecSoftwareAsset
github.com/fleetdm/fleet/v4/server/fleet/TeamSpecAppStoreApp Platform string
github.com/fleetdm/fleet/v4/server/fleet/TeamSpecAppStoreApp DisplayName string
github.com/fleetdm/fleet/v4/server/fleet/TeamSpecAppStoreApp Configuration fleet.TeamSpecSoftwareAsset