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:
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user