SE: CLI setup experience changes (#22956)

This commit is contained in:
Martin Angers
2024-10-23 14:51:02 -04:00
committed by GitHub
parent aaea56de2b
commit d1c3b5b28e
42 changed files with 799 additions and 261 deletions
@@ -0,0 +1 @@
* Added support to `fleetctl gitops` to specify a setup experience script to run and software to install, for a team or no team.
+1 -1
View File
@@ -94,7 +94,7 @@ func applyCommand() *cli.Command {
teamsSoftwareInstallers := make(map[string][]fleet.SoftwarePackageResponse)
teamsScripts := make(map[string][]fleet.ScriptResponse)
_, _, _, err = fleetClient.ApplyGroup(c.Context, specs, baseDir, logf, nil, opts, teamsSoftwareInstallers, teamsScripts)
_, _, _, err = fleetClient.ApplyGroup(c.Context, false, specs, baseDir, logf, nil, opts, teamsSoftwareInstallers, teamsScripts)
if err != nil {
return err
}
+15
View File
@@ -438,6 +438,9 @@ func TestGitOpsBasicTeam(t *testing.T) {
ds.ListSoftwareTitlesFunc = func(ctx context.Context, opt fleet.SoftwareTitleListOptions, tmFilter fleet.TeamFilter) ([]fleet.SoftwareTitleListResult, int, *fleet.PaginationMetadata, error) {
return nil, 0, nil, nil
}
ds.DeleteSetupExperienceScriptFunc = func(ctx context.Context, teamID *uint) error {
return nil
}
tmpFile, err := os.CreateTemp(t.TempDir(), "*.yml")
require.NoError(t, err)
@@ -903,6 +906,9 @@ func TestGitOpsFullTeam(t *testing.T) {
ds.ListSoftwareTitlesFunc = func(ctx context.Context, opt fleet.SoftwareTitleListOptions, tmFilter fleet.TeamFilter) ([]fleet.SoftwareTitleListResult, int, *fleet.PaginationMetadata, error) {
return nil, 0, nil, nil
}
ds.DeleteSetupExperienceScriptFunc = func(ctx context.Context, teamID *uint) error {
return nil
}
startSoftwareInstallerServer(t)
@@ -1159,6 +1165,9 @@ func TestGitOpsBasicGlobalAndTeam(t *testing.T) {
ds.ListABMTokensFunc = func(ctx context.Context) ([]*fleet.ABMToken, error) {
return []*fleet.ABMToken{}, nil
}
ds.DeleteSetupExperienceScriptFunc = func(ctx context.Context, teamID *uint) error {
return nil
}
globalFile, err := os.CreateTemp(t.TempDir(), "*.yml")
require.NoError(t, err)
@@ -1437,6 +1446,9 @@ func TestGitOpsBasicGlobalAndNoTeam(t *testing.T) {
ds.ListABMTokensFunc = func(ctx context.Context) ([]*fleet.ABMToken, error) {
return []*fleet.ABMToken{}, nil
}
ds.DeleteSetupExperienceScriptFunc = func(ctx context.Context, teamID *uint) error {
return nil
}
globalFileBasic, err := os.CreateTemp(t.TempDir(), "*.yml")
require.NoError(t, err)
@@ -2354,6 +2366,9 @@ func setupFullGitOpsPremiumServer(t *testing.T) (*mock.Store, **fleet.AppConfig,
ds.ListABMTokensFunc = func(ctx context.Context) ([]*fleet.ABMToken, error) {
return []*fleet.ABMToken{}, nil
}
ds.DeleteSetupExperienceScriptFunc = func(ctx context.Context, teamID *uint) error {
return nil
}
t.Setenv("FLEET_SERVER_URL", fleetServerURL)
t.Setenv("ORG_NAME", orgName)
+1 -1
View File
@@ -389,7 +389,7 @@ Use the stop and reset subcommands to manage the server and dependencies once st
// so pass in the current working directory.
teamsSoftwareInstallers := make(map[string][]fleet.SoftwarePackageResponse)
teamsScripts := make(map[string][]fleet.ScriptResponse)
_, _, _, err = client.ApplyGroup(c.Context, specs, ".", logf, nil, fleet.ApplyClientSpecOptions{}, teamsSoftwareInstallers, teamsScripts)
_, _, _, err = client.ApplyGroup(c.Context, false, specs, ".", logf, nil, fleet.ApplyClientSpecOptions{}, teamsSoftwareInstallers, teamsScripts)
if err != nil {
return err
}
+3 -1
View File
@@ -130,7 +130,9 @@
"bootstrap_package": null,
"enable_end_user_authentication": false,
"macos_setup_assistant": null,
"enable_release_device_manually": false
"enable_release_device_manually": false,
"script": null,
"software": null
},
"windows_settings": {
"custom_settings": null
@@ -50,6 +50,8 @@ spec:
enable_end_user_authentication: false
enable_release_device_manually: false
macos_setup_assistant:
script:
software:
windows_settings:
custom_settings: null
end_user_authentication:
@@ -82,7 +82,9 @@
"bootstrap_package": null,
"enable_end_user_authentication": false,
"macos_setup_assistant": null,
"enable_release_device_manually": false
"enable_release_device_manually": false,
"script": null,
"software": null
},
"windows_settings": {
"custom_settings": null
@@ -50,6 +50,8 @@ spec:
enable_end_user_authentication: false
enable_release_device_manually: false
macos_setup_assistant:
script:
software:
windows_settings:
custom_settings:
end_user_authentication:
+6 -2
View File
@@ -54,7 +54,9 @@
"bootstrap_package": null,
"enable_end_user_authentication": false,
"macos_setup_assistant": null,
"enable_release_device_manually": false
"enable_release_device_manually": false,
"script": null,
"software": null
},
"windows_settings": {
"custom_settings": null
@@ -137,7 +139,9 @@
"bootstrap_package": null,
"enable_end_user_authentication": false,
"macos_setup_assistant": null,
"enable_release_device_manually": false
"enable_release_device_manually": false,
"script": null,
"software": null
},
"windows_settings": {
"custom_settings": null
+4
View File
@@ -34,6 +34,8 @@ spec:
enable_end_user_authentication: false
enable_release_device_manually: false
macos_setup_assistant:
script:
software:
scripts: null
secrets: null
webhook_settings:
@@ -84,6 +86,8 @@ spec:
enable_end_user_authentication: false
enable_release_device_manually: false
macos_setup_assistant:
script:
software:
scripts: null
webhook_settings:
host_status_webhook: null
@@ -40,6 +40,8 @@ spec:
enable_end_user_authentication: false
macos_setup_assistant: null
enable_release_device_manually: false
script: null
software: null
macos_updates:
deadline: null
minimum_version: null
@@ -40,6 +40,8 @@ spec:
enable_end_user_authentication: false
macos_setup_assistant: %s
enable_release_device_manually: false
script: null
software: null
macos_updates:
deadline: null
minimum_version: null
@@ -22,6 +22,8 @@ spec:
enable_end_user_authentication: false
macos_setup_assistant: null
enable_release_device_manually: false
script: null
software: null
macos_updates:
deadline: null
minimum_version: null
@@ -62,6 +64,8 @@ spec:
bootstrap_package: null
macos_setup_assistant: null
enable_release_device_manually: false
script: null
software: null
macos_updates:
deadline: null
minimum_version: null
@@ -22,6 +22,8 @@ spec:
enable_end_user_authentication: false
macos_setup_assistant: %s
enable_release_device_manually: false
script: null
software: null
macos_updates:
deadline: null
minimum_version: null
@@ -62,6 +64,8 @@ spec:
bootstrap_package: %s
macos_setup_assistant: %s
enable_release_device_manually: false
script: null
software: null
macos_updates:
deadline: null
minimum_version: null
@@ -20,6 +20,8 @@ spec:
enable_end_user_authentication: false
macos_setup_assistant: null
enable_release_device_manually: false
script: null
software: null
macos_updates:
deadline: null
minimum_version: null
+2
View File
@@ -21,6 +21,8 @@ spec:
enable_end_user_authentication: false
macos_setup_assistant: %s
enable_release_device_manually: false
script: null
software: null
macos_updates:
deadline: null
minimum_version: null
+10 -9
View File
@@ -1301,15 +1301,16 @@ func (svc *Service) softwareBatchUpload(
}
installer := &fleet.UploadSoftwareInstallerPayload{
TeamID: teamID,
InstallScript: p.InstallScript,
PreInstallQuery: p.PreInstallQuery,
PostInstallScript: p.PostInstallScript,
UninstallScript: p.UninstallScript,
InstallerFile: bytes.NewReader(bodyBytes),
SelfService: p.SelfService,
UserID: userID,
URL: p.URL,
TeamID: teamID,
InstallScript: p.InstallScript,
PreInstallQuery: p.PreInstallQuery,
PostInstallScript: p.PostInstallScript,
UninstallScript: p.UninstallScript,
InstallerFile: bytes.NewReader(bodyBytes),
SelfService: p.SelfService,
UserID: userID,
URL: p.URL,
InstallDuringSetup: p.InstallDuringSetup,
}
// set the filename before adding metadata, as it is used as fallback
+9
View File
@@ -1396,6 +1396,15 @@ func (svc *Service) editTeamFromSpec(
}
}
// if the setup experience script was cleared, remove it for that team
if spec.MDM.MacOSSetup.Script.Set &&
spec.MDM.MacOSSetup.Script.Value == "" &&
oldMacOSSetup.Script.Value != "" {
if err := svc.DeleteSetupExperienceScript(ctx, &team.ID); err != nil {
return ctxerr.Wrapf(ctx, err, "clear setup experience script for team %d", team.ID)
}
}
if didUpdateMacOSEndUserAuth {
if err := svc.updateMacOSSetupEnableEndUserAuth(
ctx, spec.MDM.MacOSSetup.EnableEndUserAuthentication, &team.ID, &team.Name,
+21 -11
View File
@@ -79,9 +79,10 @@ func (svc *Service) BatchAssociateVPPApps(ctx context.Context, teamName string,
SelfService: false,
Platform: fleet.IPadOSPlatform,
}, {
AppStoreID: payload.AppStoreID,
SelfService: payload.SelfService,
Platform: fleet.MacOSPlatform,
AppStoreID: payload.AppStoreID,
SelfService: payload.SelfService,
Platform: fleet.MacOSPlatform,
InstallDuringSetup: payload.InstallDuringSetup,
}}...)
}
@@ -101,7 +102,14 @@ func (svc *Service) BatchAssociateVPPApps(ctx context.Context, teamName string,
return fleet.NewInvalidArgumentError("app_store_apps.platform",
fmt.Sprintf("platform must be one of '%s', '%s', or '%s", fleet.IOSPlatform, fleet.IPadOSPlatform, fleet.MacOSPlatform))
}
vppAppTeams = append(vppAppTeams, fleet.VPPAppTeam{VPPAppID: fleet.VPPAppID{AdamID: payload.AppStoreID, Platform: payload.Platform}, SelfService: payload.SelfService})
vppAppTeams = append(vppAppTeams, fleet.VPPAppTeam{
VPPAppID: fleet.VPPAppID{
AdamID: payload.AppStoreID,
Platform: payload.Platform,
},
SelfService: payload.SelfService,
InstallDuringSetup: payload.InstallDuringSetup,
})
}
var missingAssets []string
@@ -374,14 +382,15 @@ func (svc *Service) AddAppStoreApp(ctx context.Context, teamID *uint, appID flee
func getVPPAppsMetadata(ctx context.Context, ids []fleet.VPPAppTeam) ([]*fleet.VPPApp, error) {
var apps []*fleet.VPPApp
// Map of adamID to platform, then to whether it's available as self-service.
adamIDMap := make(map[string]map[fleet.AppleDevicePlatform]bool)
// Map of adamID to platform, then to whether it's available as self-service
// and installed during setup.
adamIDMap := make(map[string]map[fleet.AppleDevicePlatform]fleet.VPPAppTeam)
for _, id := range ids {
if _, ok := adamIDMap[id.AdamID]; !ok {
adamIDMap[id.AdamID] = make(map[fleet.AppleDevicePlatform]bool, 1)
adamIDMap[id.AdamID][id.Platform] = id.SelfService
adamIDMap[id.AdamID] = make(map[fleet.AppleDevicePlatform]fleet.VPPAppTeam, 1)
adamIDMap[id.AdamID][id.Platform] = fleet.VPPAppTeam{SelfService: id.SelfService, InstallDuringSetup: id.InstallDuringSetup}
} else {
adamIDMap[id.AdamID][id.Platform] = id.SelfService
adamIDMap[id.AdamID][id.Platform] = fleet.VPPAppTeam{SelfService: id.SelfService, InstallDuringSetup: id.InstallDuringSetup}
}
}
@@ -397,14 +406,15 @@ func getVPPAppsMetadata(ctx context.Context, ids []fleet.VPPAppTeam) ([]*fleet.V
for adamID, metadata := range assetMetatada {
platforms := getPlatformsFromSupportedDevices(metadata.SupportedDevices)
for platform := range platforms {
if selfService, ok := adamIDMap[adamID][platform]; ok {
if props, ok := adamIDMap[adamID][platform]; ok {
app := &fleet.VPPApp{
VPPAppTeam: fleet.VPPAppTeam{
VPPAppID: fleet.VPPAppID{
AdamID: adamID,
Platform: platform,
},
SelfService: selfService,
SelfService: props.SelfService,
InstallDuringSetup: props.InstallDuringSetup,
},
BundleIdentifier: metadata.BundleID,
IconURL: metadata.ArtworkURL,
+2 -1
View File
@@ -757,7 +757,8 @@ func parseSoftware(top map[string]json.RawMessage, result *GitOps, baseDir strin
for _, item := range software.Packages {
var softwarePackageSpec fleet.SoftwarePackageSpec
if item.Path != nil {
fileBytes, err := os.ReadFile(resolveApplyRelativePath(baseDir, *item.Path))
softwarePackageSpec.ReferencedYamlPath = resolveApplyRelativePath(baseDir, *item.Path)
fileBytes, err := os.ReadFile(softwarePackageSpec.ReferencedYamlPath)
if err != nil {
multiError = multierror.Append(multiError, fmt.Errorf("failed to read policies file %s: %v", *item.Path, err))
continue
+1 -1
View File
@@ -65,7 +65,7 @@ CREATE TABLE `app_config_json` (
UNIQUE KEY `id` (`id`)
) /*!50100 TABLESPACE `innodb_system` */ ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
INSERT INTO `app_config_json` VALUES (1,'{\"mdm\": {\"ios_updates\": {\"deadline\": null, \"minimum_version\": null}, \"macos_setup\": {\"bootstrap_package\": null, \"macos_setup_assistant\": null, \"enable_end_user_authentication\": false, \"enable_release_device_manually\": false}, \"macos_updates\": {\"deadline\": null, \"minimum_version\": null}, \"ipados_updates\": {\"deadline\": null, \"minimum_version\": null}, \"macos_settings\": {\"custom_settings\": null}, \"macos_migration\": {\"mode\": \"\", \"enable\": false, \"webhook_url\": \"\"}, \"windows_updates\": {\"deadline_days\": null, \"grace_period_days\": null}, \"apple_server_url\": \"\", \"windows_settings\": {\"custom_settings\": null}, \"apple_bm_terms_expired\": false, \"apple_business_manager\": null, \"enable_disk_encryption\": false, \"enabled_and_configured\": false, \"end_user_authentication\": {\"idp_name\": \"\", \"metadata\": \"\", \"entity_id\": \"\", \"issuer_uri\": \"\", \"metadata_url\": \"\"}, \"volume_purchasing_program\": null, \"windows_enabled_and_configured\": false, \"apple_bm_enabled_and_configured\": false}, \"scripts\": null, \"features\": {\"enable_host_users\": true, \"enable_software_inventory\": false}, \"org_info\": {\"org_name\": \"\", \"contact_url\": \"\", \"org_logo_url\": \"\", \"org_logo_url_light_background\": \"\"}, \"integrations\": {\"jira\": null, \"zendesk\": null, \"google_calendar\": null, \"ndes_scep_proxy\": null}, \"sso_settings\": {\"idp_name\": \"\", \"metadata\": \"\", \"entity_id\": \"\", \"enable_sso\": false, \"issuer_uri\": \"\", \"metadata_url\": \"\", \"idp_image_url\": \"\", \"enable_jit_role_sync\": false, \"enable_sso_idp_login\": false, \"enable_jit_provisioning\": false}, \"agent_options\": {\"config\": {\"options\": {\"logger_plugin\": \"tls\", \"pack_delimiter\": \"/\", \"logger_tls_period\": 10, \"distributed_plugin\": \"tls\", \"disable_distributed\": false, \"logger_tls_endpoint\": \"/api/osquery/log\", \"distributed_interval\": 10, \"distributed_tls_max_attempts\": 3}, \"decorators\": {\"load\": [\"SELECT uuid AS host_uuid FROM system_info;\", \"SELECT hostname AS hostname FROM system_info;\"]}}, \"overrides\": {}}, \"fleet_desktop\": {\"transparency_url\": \"\"}, \"smtp_settings\": {\"port\": 587, \"domain\": \"\", \"server\": \"\", \"password\": \"\", \"user_name\": \"\", \"configured\": false, \"enable_smtp\": false, \"enable_ssl_tls\": true, \"sender_address\": \"\", \"enable_start_tls\": true, \"verify_ssl_certs\": true, \"authentication_type\": \"0\", \"authentication_method\": \"0\"}, \"server_settings\": {\"server_url\": \"\", \"enable_analytics\": false, \"query_report_cap\": 0, \"scripts_disabled\": false, \"deferred_save_host\": false, \"live_query_disabled\": false, \"ai_features_disabled\": false, \"query_reports_disabled\": false}, \"webhook_settings\": {\"interval\": \"0s\", \"activities_webhook\": {\"destination_url\": \"\", \"enable_activities_webhook\": false}, \"host_status_webhook\": {\"days_count\": 0, \"destination_url\": \"\", \"host_percentage\": 0, \"enable_host_status_webhook\": false}, \"vulnerabilities_webhook\": {\"destination_url\": \"\", \"host_batch_size\": 0, \"enable_vulnerabilities_webhook\": false}, \"failing_policies_webhook\": {\"policy_ids\": null, \"destination_url\": \"\", \"host_batch_size\": 0, \"enable_failing_policies_webhook\": false}}, \"host_expiry_settings\": {\"host_expiry_window\": 0, \"host_expiry_enabled\": false}, \"vulnerability_settings\": {\"databases_path\": \"\"}, \"activity_expiry_settings\": {\"activity_expiry_window\": 0, \"activity_expiry_enabled\": false}}','2020-01-01 01:01:01','2020-01-01 01:01:01');
INSERT INTO `app_config_json` VALUES (1,'{\"mdm\": {\"ios_updates\": {\"deadline\": null, \"minimum_version\": null}, \"macos_setup\": {\"script\": null, \"software\": null, \"bootstrap_package\": null, \"macos_setup_assistant\": null, \"enable_end_user_authentication\": false, \"enable_release_device_manually\": false}, \"macos_updates\": {\"deadline\": null, \"minimum_version\": null}, \"ipados_updates\": {\"deadline\": null, \"minimum_version\": null}, \"macos_settings\": {\"custom_settings\": null}, \"macos_migration\": {\"mode\": \"\", \"enable\": false, \"webhook_url\": \"\"}, \"windows_updates\": {\"deadline_days\": null, \"grace_period_days\": null}, \"apple_server_url\": \"\", \"windows_settings\": {\"custom_settings\": null}, \"apple_bm_terms_expired\": false, \"apple_business_manager\": null, \"enable_disk_encryption\": false, \"enabled_and_configured\": false, \"end_user_authentication\": {\"idp_name\": \"\", \"metadata\": \"\", \"entity_id\": \"\", \"issuer_uri\": \"\", \"metadata_url\": \"\"}, \"volume_purchasing_program\": null, \"windows_enabled_and_configured\": false, \"apple_bm_enabled_and_configured\": false}, \"scripts\": null, \"features\": {\"enable_host_users\": true, \"enable_software_inventory\": false}, \"org_info\": {\"org_name\": \"\", \"contact_url\": \"\", \"org_logo_url\": \"\", \"org_logo_url_light_background\": \"\"}, \"integrations\": {\"jira\": null, \"zendesk\": null, \"google_calendar\": null, \"ndes_scep_proxy\": null}, \"sso_settings\": {\"idp_name\": \"\", \"metadata\": \"\", \"entity_id\": \"\", \"enable_sso\": false, \"issuer_uri\": \"\", \"metadata_url\": \"\", \"idp_image_url\": \"\", \"enable_jit_role_sync\": false, \"enable_sso_idp_login\": false, \"enable_jit_provisioning\": false}, \"agent_options\": {\"config\": {\"options\": {\"logger_plugin\": \"tls\", \"pack_delimiter\": \"/\", \"logger_tls_period\": 10, \"distributed_plugin\": \"tls\", \"disable_distributed\": false, \"logger_tls_endpoint\": \"/api/osquery/log\", \"distributed_interval\": 10, \"distributed_tls_max_attempts\": 3}, \"decorators\": {\"load\": [\"SELECT uuid AS host_uuid FROM system_info;\", \"SELECT hostname AS hostname FROM system_info;\"]}}, \"overrides\": {}}, \"fleet_desktop\": {\"transparency_url\": \"\"}, \"smtp_settings\": {\"port\": 587, \"domain\": \"\", \"server\": \"\", \"password\": \"\", \"user_name\": \"\", \"configured\": false, \"enable_smtp\": false, \"enable_ssl_tls\": true, \"sender_address\": \"\", \"enable_start_tls\": true, \"verify_ssl_certs\": true, \"authentication_type\": \"0\", \"authentication_method\": \"0\"}, \"server_settings\": {\"server_url\": \"\", \"enable_analytics\": false, \"query_report_cap\": 0, \"scripts_disabled\": false, \"deferred_save_host\": false, \"live_query_disabled\": false, \"ai_features_disabled\": false, \"query_reports_disabled\": false}, \"webhook_settings\": {\"interval\": \"0s\", \"activities_webhook\": {\"destination_url\": \"\", \"enable_activities_webhook\": false}, \"host_status_webhook\": {\"days_count\": 0, \"destination_url\": \"\", \"host_percentage\": 0, \"enable_host_status_webhook\": false}, \"vulnerabilities_webhook\": {\"destination_url\": \"\", \"host_batch_size\": 0, \"enable_vulnerabilities_webhook\": false}, \"failing_policies_webhook\": {\"policy_ids\": null, \"destination_url\": \"\", \"host_batch_size\": 0, \"enable_failing_policies_webhook\": false}}, \"host_expiry_settings\": {\"host_expiry_window\": 0, \"host_expiry_enabled\": false}, \"vulnerability_settings\": {\"databases_path\": \"\"}, \"activity_expiry_settings\": {\"activity_expiry_window\": 0, \"activity_expiry_enabled\": false}}','2020-01-01 01:01:01','2020-01-01 01:01:01');
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `calendar_events` (
+4 -4
View File
@@ -85,7 +85,7 @@ WHERE global_or_team_id = ?`
if err != nil {
return ctxerr.Wrap(ctx, err, "retrieving number of inserted software installers")
}
totalInsertions += uint(inserts)
totalInsertions += uint(inserts) // nolint: gosec
// VPP apps
res, err = tx.ExecContext(ctx, stmtVPPApps, hostUUID, teamID)
@@ -96,7 +96,7 @@ WHERE global_or_team_id = ?`
if err != nil {
return ctxerr.Wrap(ctx, err, "retrieving number of inserted vpp apps")
}
totalInsertions += uint(inserts)
totalInsertions += uint(inserts) // nolint: gosec
// Scripts
res, err = tx.ExecContext(ctx, stmtSetupScripts, hostUUID, teamID)
@@ -107,7 +107,7 @@ WHERE global_or_team_id = ?`
if err != nil {
return ctxerr.Wrap(ctx, err, "retrieving number of inserted setup experience scripts")
}
totalInsertions += uint(inserts)
totalInsertions += uint(inserts) // nolint: gosec
if err := setHostAwaitingConfiguration(ctx, tx, hostUUID, true); err != nil {
return ctxerr.Wrap(ctx, err, "setting host awaiting configuration to true")
@@ -403,7 +403,7 @@ func (ds *Datastore) SetSetupExperienceScript(ctx context.Context, script *fleet
id, _ := scRes.LastInsertId()
// then create the script entity
_, err = insertSetupExperienceScript(ctx, tx, script, uint(id))
_, err = insertSetupExperienceScript(ctx, tx, script, uint(id)) // nolint: gosec
return err
})
@@ -186,13 +186,13 @@ func testEnqueueSetupExperienceItems(t *testing.T, ds *Datastore) {
HostUUID: hostTeam1,
Name: "script1",
Status: "pending",
ScriptID: nullableUint(uint(script1ID)),
ScriptID: nullableUint(uint(script1ID)), // nolint: gosec
},
{
HostUUID: hostTeam2,
Name: "script2",
Status: "pending",
ScriptID: nullableUint(uint(script2ID)),
ScriptID: nullableUint(uint(script2ID)), // nolint: gosec
},
} {
var found bool
@@ -267,7 +267,7 @@ func testEnqueueSetupExperienceItems(t *testing.T, ds *Datastore) {
HostUUID: hostTeam1,
Name: "script1",
Status: "pending",
ScriptID: nullableUint(uint(script1ID)),
ScriptID: nullableUint(uint(script1ID)), // nolint: gosec
},
} {
var found bool
@@ -303,7 +303,7 @@ type setupExperienceInsertTestRows struct {
}
func nullableUint(val uint) sql.NullInt64 {
return sql.NullInt64{Int64: int64(val), Valid: true}
return sql.NullInt64{Int64: int64(val), Valid: true} // nolint: gosec
}
func testGetSetupExperienceTitles(t *testing.T, ds *Datastore) {
@@ -668,7 +668,7 @@ func testSetupExperienceStatusResults(t *testing.T, ds *Datastore) {
require.NoError(t, err)
id, err := res.LastInsertId()
require.NoError(t, err)
scriptID = uint(id)
scriptID = uint(id) // nolint: gosec
return nil
})
@@ -680,7 +680,7 @@ func testSetupExperienceStatusResults(t *testing.T, ds *Datastore) {
require.NoError(t, err)
id, err := res.LastInsertId()
require.NoError(t, err)
sesr.ID = uint(id)
sesr.ID = uint(id) // nolint: gosec
return nil
})
}
+30 -36
View File
@@ -847,16 +847,6 @@ WHERE
team_id = ?
`
const countInstallDuringSetupAllInstallersInTeam = `
SELECT
COUNT(*)
FROM
software_installers
WHERE
global_or_team_id = ? AND
install_during_setup = 1
`
const deleteAllInstallersInTeam = `
DELETE FROM
software_installers
@@ -881,8 +871,8 @@ WHERE
SELECT
COUNT(*)
FROM
software_installers
WHERE
software_installers
WHERE
global_or_team_id = ? AND
title_id NOT IN (?) AND
install_during_setup = 1
@@ -926,11 +916,12 @@ INSERT INTO software_installers (
user_name,
user_email,
url,
package_ids
package_ids,
install_during_setup
) VALUES (
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?,
(SELECT id FROM software_titles WHERE name = ? AND source = ? AND browser = ''),
?, (SELECT name FROM users WHERE id = ?), (SELECT email FROM users WHERE id = ?), ?, ?
?, (SELECT name FROM users WHERE id = ?), (SELECT email FROM users WHERE id = ?), ?, ?, COALESCE(?, false)
)
ON DUPLICATE KEY UPDATE
install_script_content_id = VALUES(install_script_content_id),
@@ -946,7 +937,8 @@ ON DUPLICATE KEY UPDATE
user_id = VALUES(user_id),
user_name = VALUES(user_name),
user_email = VALUES(user_email),
url = VALUES(url)
url = VALUES(url),
install_during_setup = COALESCE(?, install_during_setup)
`
// use a team id of 0 if no-team
@@ -955,20 +947,19 @@ ON DUPLICATE KEY UPDATE
globalOrTeamID = *tmID
}
// if we're batch-setting installers and replacing the ones installed during
// setup in the same go, no need to validate that we don't delete one marked
// as install during setup (since we're overwriting those). This is always
// called from fleetctl gitops, so it should always be the case anyway.
var replacingInstallDuringSetup bool
if len(installers) == 0 || installers[0].InstallDuringSetup != nil {
replacingInstallDuringSetup = true
}
if err := ds.withRetryTxx(ctx, func(tx sqlx.ExtContext) error {
// if no installers are provided, just delete whatever was in
// the table
if len(installers) == 0 {
// check if any existing installer is install_during_setup, fail if there is one
// TODO: will need to be reconciled with https://github.com/fleetdm/fleet/issues/22385
var countInstallDuringSetup int
if err := sqlx.GetContext(ctx, tx, &countInstallDuringSetup, countInstallDuringSetupAllInstallersInTeam, globalOrTeamID); err != nil {
return ctxerr.Wrap(ctx, err, "check installers installed during setup")
}
if countInstallDuringSetup > 0 {
return errDeleteInstallerInstalledDuringSetup
}
if _, err := tx.ExecContext(ctx, unsetAllInstallersFromPolicies, globalOrTeamID); err != nil {
return ctxerr.Wrap(ctx, err, "unset all obsolete installers in policies")
}
@@ -1005,17 +996,18 @@ ON DUPLICATE KEY UPDATE
}
// check if any in the list are install_during_setup, fail if there is one
// TODO: will need to be reconciled with https://github.com/fleetdm/fleet/issues/22385
stmt, args, err = sqlx.In(countInstallDuringSetupNotInList, globalOrTeamID, titleIDs)
if err != nil {
return ctxerr.Wrap(ctx, err, "build statement to check installers install_during_setup")
}
var countInstallDuringSetup int
if err := sqlx.GetContext(ctx, tx, &countInstallDuringSetup, stmt, args...); err != nil {
return ctxerr.Wrap(ctx, err, "check installers installed during setup")
}
if countInstallDuringSetup > 0 {
return errDeleteInstallerInstalledDuringSetup
if !replacingInstallDuringSetup {
stmt, args, err = sqlx.In(countInstallDuringSetupNotInList, globalOrTeamID, titleIDs)
if err != nil {
return ctxerr.Wrap(ctx, err, "build statement to check installers install_during_setup")
}
var countInstallDuringSetup int
if err := sqlx.GetContext(ctx, tx, &countInstallDuringSetup, stmt, args...); err != nil {
return ctxerr.Wrap(ctx, err, "check installers installed during setup")
}
if countInstallDuringSetup > 0 {
return errDeleteInstallerInstalledDuringSetup
}
}
stmt, args, err = sqlx.In(deleteInstallersNotInList, globalOrTeamID, titleIDs)
@@ -1100,6 +1092,8 @@ ON DUPLICATE KEY UPDATE
installer.UserID,
installer.URL,
strings.Join(installer.PackageIDs, ","),
installer.InstallDuringSetup,
installer.InstallDuringSetup,
}
upsertQuery := insertNewOrEditedInstaller
if len(existing) > 0 && existing[0].IsPackageModified { // update uploaded_at for updated installer package
@@ -719,21 +719,23 @@ func testBatchSetSoftwareInstallers(t *testing.T, ds *Datastore) {
})
// add a new installer + ins0 installer
// mark ins0 as install_during_setup
ins1 := "installer1"
ins1File := bytes.NewReader([]byte("installer1"))
err = ds.BatchSetSoftwareInstallers(ctx, &team.ID, []*fleet.UploadSoftwareInstallerPayload{
{
InstallScript: "install",
InstallerFile: ins0File,
StorageID: ins0,
Filename: ins0,
Title: ins0,
Source: "apps",
Version: "1",
PreInstallQuery: "select 0 from foo;",
UserID: user1.ID,
Platform: "darwin",
URL: "https://example.com",
InstallScript: "install",
InstallerFile: ins0File,
StorageID: ins0,
Filename: ins0,
Title: ins0,
Source: "apps",
Version: "1",
PreInstallQuery: "select 0 from foo;",
UserID: user1.ID,
Platform: "darwin",
URL: "https://example.com",
InstallDuringSetup: ptr.Bool(true),
},
{
InstallScript: "install",
@@ -767,12 +769,6 @@ func testBatchSetSoftwareInstallers(t *testing.T, ds *Datastore) {
{Name: ins1, Source: "apps", Browser: ""},
})
// mark ins0 as install_during_setup
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
_, err := q.ExecContext(ctx, ` UPDATE software_installers SET install_during_setup = 1 WHERE title_id = ?`, *softwareInstallers[0].TitleID)
return err
})
// remove ins0 fails due to install_during_setup
err = ds.BatchSetSoftwareInstallers(ctx, &team.ID, []*fleet.UploadSoftwareInstallerPayload{
{
@@ -791,16 +787,72 @@ func testBatchSetSoftwareInstallers(t *testing.T, ds *Datastore) {
require.Error(t, err)
require.ErrorIs(t, err, errDeleteInstallerInstalledDuringSetup)
// remove everything fails due to ins0 install_during_setup
err = ds.BatchSetSoftwareInstallers(ctx, &team.ID, []*fleet.UploadSoftwareInstallerPayload{})
require.Error(t, err)
require.ErrorIs(t, err, errDeleteInstallerInstalledDuringSetup)
// batch-set both installers again, this time with nil install_during_setup for ins0,
// will keep it as true.
err = ds.BatchSetSoftwareInstallers(ctx, &team.ID, []*fleet.UploadSoftwareInstallerPayload{
{
InstallScript: "install",
InstallerFile: ins0File,
StorageID: ins0,
Filename: ins0,
Title: ins0,
Source: "apps",
Version: "1",
PreInstallQuery: "select 0 from foo;",
UserID: user1.ID,
Platform: "darwin",
URL: "https://example.com",
InstallDuringSetup: nil,
},
{
InstallScript: "install",
PostInstallScript: "post-install",
InstallerFile: ins1File,
StorageID: ins1,
Filename: ins1,
Title: ins1,
Source: "apps",
Version: "2",
PreInstallQuery: "select 1 from bar;",
UserID: user1.ID,
Platform: "darwin",
URL: "https://example2.com",
},
})
require.NoError(t, err)
// mark ins0 as NOT install_during_setup
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
_, err := q.ExecContext(ctx, ` UPDATE software_installers SET install_during_setup = 0 WHERE title_id = ?`, *softwareInstallers[0].TitleID)
return err
err = ds.BatchSetSoftwareInstallers(ctx, &team.ID, []*fleet.UploadSoftwareInstallerPayload{
{
InstallScript: "install",
InstallerFile: ins0File,
StorageID: ins0,
Filename: ins0,
Title: ins0,
Source: "apps",
Version: "1",
PreInstallQuery: "select 0 from foo;",
UserID: user1.ID,
Platform: "darwin",
URL: "https://example.com",
InstallDuringSetup: ptr.Bool(false),
},
{
InstallScript: "install",
PostInstallScript: "post-install",
InstallerFile: ins1File,
StorageID: ins1,
Filename: ins1,
Title: ins1,
Source: "apps",
Version: "2",
PreInstallQuery: "select 1 from bar;",
UserID: user1.ID,
Platform: "darwin",
URL: "https://example2.com",
},
})
require.NoError(t, err)
// remove ins0
err = ds.BatchSetSoftwareInstallers(ctx, &team.ID, []*fleet.UploadSoftwareInstallerPayload{
+2
View File
@@ -642,6 +642,8 @@ func testTeamsMDMConfig(t *testing.T, ds *Datastore) {
BootstrapPackage: optjson.SetString("bootstrap"),
MacOSSetupAssistant: optjson.SetString("assistant"),
EnableReleaseDeviceManually: optjson.SetBool(false),
Script: optjson.String{Set: true},
Software: optjson.Slice[*fleet.MacOSSetupSoftware]{Set: true, Value: []*fleet.MacOSSetupSoftware{}},
},
WindowsSettings: fleet.WindowsSettings{
CustomSettings: optjson.SetSlice([]fleet.MDMProfileSpec{{Path: "foo"}, {Path: "bar"}}),
+22 -9
View File
@@ -169,6 +169,15 @@ func (ds *Datastore) SetTeamVPPApps(ctx context.Context, teamID *uint, appFleets
return ctxerr.Wrap(ctx, err, "SetTeamVPPApps getting list of existing apps")
}
// if we're batch-setting apps and replacing the ones installed during setup
// in the same go, no need to validate that we don't delete one marked as
// install during setup (since we're overwriting those). This is always
// called from fleetctl gitops, so it should always be the case anyway.
var replacingInstallDuringSetup bool
if len(appFleets) == 0 || appFleets[0].InstallDuringSetup != nil {
replacingInstallDuringSetup = true
}
var toAddApps []fleet.VPPAppTeam
var toRemoveApps []fleet.VPPAppID
@@ -181,9 +190,8 @@ func (ds *Datastore) SetTeamVPPApps(ctx context.Context, teamID *uint, appFleets
}
}
if !found {
// if app is marked as install during setup, prevent deletion.
// TODO: will need to be reconciled with https://github.com/fleetdm/fleet/issues/22385
if appTeamInfo.InstallDuringSetup {
// if app is marked as install during setup, prevent deletion unless we're replacing those.
if !replacingInstallDuringSetup && appTeamInfo.InstallDuringSetup != nil && *appTeamInfo.InstallDuringSetup {
return errDeleteInstallerInstalledDuringSetup
}
toRemoveApps = append(toRemoveApps, existingApp)
@@ -191,7 +199,10 @@ func (ds *Datastore) SetTeamVPPApps(ctx context.Context, teamID *uint, appFleets
}
for _, appFleet := range appFleets {
if existingFleet, ok := existingApps[appFleet.VPPAppID]; !ok || existingFleet.SelfService != appFleet.SelfService {
// upsert it if it does not exist or SelfService or InstallDuringSetup flags are changed
if existingFleet, ok := existingApps[appFleet.VPPAppID]; !ok || existingFleet.SelfService != appFleet.SelfService ||
appFleet.InstallDuringSetup != nil &&
existingFleet.InstallDuringSetup != nil && *appFleet.InstallDuringSetup != *existingFleet.InstallDuringSetup {
toAddApps = append(toAddApps, appFleet)
}
}
@@ -310,11 +321,13 @@ ON DUPLICATE KEY UPDATE
func insertVPPAppTeams(ctx context.Context, tx sqlx.ExtContext, appID fleet.VPPAppTeam, teamID *uint, vppTokenID uint) error {
stmt := `
INSERT INTO vpp_apps_teams
(adam_id, global_or_team_id, team_id, platform, self_service, vpp_token_id)
(adam_id, global_or_team_id, team_id, platform, self_service, vpp_token_id, install_during_setup)
VALUES
(?, ?, ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE self_service = VALUES(self_service)
`
(?, ?, ?, ?, ?, ?, COALESCE(?, false))
ON DUPLICATE KEY UPDATE
self_service = VALUES(self_service),
install_during_setup = COALESCE(?, install_during_setup)
`
var globalOrTmID uint
if teamID != nil {
@@ -325,7 +338,7 @@ ON DUPLICATE KEY UPDATE self_service = VALUES(self_service)
}
}
_, err := tx.ExecContext(ctx, stmt, appID.AdamID, globalOrTmID, teamID, appID.Platform, appID.SelfService, vppTokenID)
_, err := tx.ExecContext(ctx, stmt, appID.AdamID, globalOrTmID, teamID, appID.Platform, appID.SelfService, vppTokenID, appID.InstallDuringSetup, appID.InstallDuringSetup)
if IsDuplicate(err) {
err = &existsError{
Identifier: fmt.Sprintf("%s %s self_service: %v", appID.AdamID, appID.Platform, appID.SelfService),
+28 -23
View File
@@ -511,11 +511,17 @@ func testVPPApps(t *testing.T, ds *Datastore) {
// Check that getting the assigned apps works
appSet, err := ds.GetAssignedVPPApps(ctx, &team.ID)
require.NoError(t, err)
assert.Equal(t, map[fleet.VPPAppID]fleet.VPPAppTeam{app1.VPPAppID: {VPPAppID: app1.VPPAppID}, app2.VPPAppID: {VPPAppID: app2.VPPAppID}}, appSet)
assert.Equal(t, map[fleet.VPPAppID]fleet.VPPAppTeam{
app1.VPPAppID: {VPPAppID: app1.VPPAppID, InstallDuringSetup: ptr.Bool(false)},
app2.VPPAppID: {VPPAppID: app2.VPPAppID, InstallDuringSetup: ptr.Bool(false)},
}, appSet)
appSet, err = ds.GetAssignedVPPApps(ctx, nil)
require.NoError(t, err)
assert.Equal(t, map[fleet.VPPAppID]fleet.VPPAppTeam{appNoTeam1.VPPAppID: {VPPAppID: appNoTeam1.VPPAppID}, appNoTeam2.VPPAppID: {VPPAppID: appNoTeam2.VPPAppID}}, appSet)
assert.Equal(t, map[fleet.VPPAppID]fleet.VPPAppTeam{
appNoTeam1.VPPAppID: {VPPAppID: appNoTeam1.VPPAppID, InstallDuringSetup: ptr.Bool(false)},
appNoTeam2.VPPAppID: {VPPAppID: appNoTeam2.VPPAppID, InstallDuringSetup: ptr.Bool(false)},
}, appSet)
var appTitles []fleet.SoftwareTitle
err = sqlx.SelectContext(ctx, ds.reader(ctx), &appTitles, `SELECT name, bundle_identifier FROM software_titles WHERE bundle_identifier IN (?,?) ORDER BY bundle_identifier`, app1.BundleIdentifier, app2.BundleIdentifier)
@@ -560,29 +566,24 @@ func testSetTeamVPPApps(t *testing.T, ds *Datastore) {
require.Len(t, assigned, 0)
// Assign 2 apps
// make app1 install_during_setup for that team
err = ds.SetTeamVPPApps(ctx, &team.ID, []fleet.VPPAppTeam{
{VPPAppID: app1.VPPAppID},
{VPPAppID: app1.VPPAppID, InstallDuringSetup: ptr.Bool(true)},
{VPPAppID: app2.VPPAppID, SelfService: true},
})
require.NoError(t, err)
// make app1 install_during_setup for that team
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
_, err := q.ExecContext(ctx, `UPDATE vpp_apps_teams SET install_during_setup = 1 WHERE global_or_team_id = ? AND adam_id = ?`, team.ID, app1.AdamID)
return err
})
assigned, err = ds.GetAssignedVPPApps(ctx, &team.ID)
require.NoError(t, err)
require.Len(t, assigned, 2)
assert.Contains(t, assigned, app1.VPPAppID)
assert.Contains(t, assigned, app2.VPPAppID)
assert.True(t, assigned[app2.VPPAppID].SelfService)
assert.True(t, assigned[app1.VPPAppID].InstallDuringSetup)
assert.True(t, *assigned[app1.VPPAppID].InstallDuringSetup)
// Assign an additional app
err = ds.SetTeamVPPApps(ctx, &team.ID, []fleet.VPPAppTeam{
{VPPAppID: app1.VPPAppID},
{VPPAppID: app1.VPPAppID, InstallDuringSetup: ptr.Bool(true)},
{VPPAppID: app2.VPPAppID},
{VPPAppID: app3.VPPAppID},
})
@@ -595,11 +596,11 @@ func testSetTeamVPPApps(t *testing.T, ds *Datastore) {
require.Contains(t, assigned, app2.VPPAppID)
require.Contains(t, assigned, app3.VPPAppID)
assert.False(t, assigned[app2.VPPAppID].SelfService)
assert.True(t, assigned[app1.VPPAppID].InstallDuringSetup)
assert.True(t, *assigned[app1.VPPAppID].InstallDuringSetup)
// Swap one app out for another
err = ds.SetTeamVPPApps(ctx, &team.ID, []fleet.VPPAppTeam{
{VPPAppID: app1.VPPAppID},
{VPPAppID: app1.VPPAppID, InstallDuringSetup: ptr.Bool(true)},
{VPPAppID: app2.VPPAppID, SelfService: true},
{VPPAppID: app4.VPPAppID},
})
@@ -612,7 +613,7 @@ func testSetTeamVPPApps(t *testing.T, ds *Datastore) {
require.Contains(t, assigned, app2.VPPAppID)
require.Contains(t, assigned, app4.VPPAppID)
assert.True(t, assigned[app2.VPPAppID].SelfService)
assert.True(t, assigned[app1.VPPAppID].InstallDuringSetup)
assert.True(t, *assigned[app1.VPPAppID].InstallDuringSetup)
// Remove app1 fails because it is installed during setup
err = ds.SetTeamVPPApps(ctx, &team.ID, []fleet.VPPAppTeam{
@@ -622,18 +623,22 @@ func testSetTeamVPPApps(t *testing.T, ds *Datastore) {
require.Error(t, err)
require.ErrorIs(t, err, errDeleteInstallerInstalledDuringSetup)
// Remove all apps fails because app1 is installed during setup
err = ds.SetTeamVPPApps(ctx, &team.ID, []fleet.VPPAppTeam{})
require.Error(t, err)
require.ErrorIs(t, err, errDeleteInstallerInstalledDuringSetup)
// make app1 NOT install_during_setup for that team
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
_, err := q.ExecContext(ctx, `UPDATE vpp_apps_teams SET install_during_setup = 0 WHERE global_or_team_id = ? AND adam_id = ?`, team.ID, app1.AdamID)
return err
err = ds.SetTeamVPPApps(ctx, &team.ID, []fleet.VPPAppTeam{
{VPPAppID: app1.VPPAppID, InstallDuringSetup: ptr.Bool(false)},
{VPPAppID: app2.VPPAppID, SelfService: true},
{VPPAppID: app4.VPPAppID},
})
require.NoError(t, err)
// Remove all apps now works
// Remove app1 now works
err = ds.SetTeamVPPApps(ctx, &team.ID, []fleet.VPPAppTeam{
{VPPAppID: app2.VPPAppID, SelfService: true},
{VPPAppID: app4.VPPAppID},
})
require.NoError(t, err)
// Remove all apps
err = ds.SetTeamVPPApps(ctx, &team.ID, []fleet.VPPAppTeam{})
require.NoError(t, err)
+22 -4
View File
@@ -420,10 +420,19 @@ func (s *MacOSSettings) FromMap(m map[string]interface{}) (map[string]bool, erro
// MacOSSetup contains settings related to the setup of DEP enrolled devices.
type MacOSSetup struct {
BootstrapPackage optjson.String `json:"bootstrap_package"`
EnableEndUserAuthentication bool `json:"enable_end_user_authentication"`
MacOSSetupAssistant optjson.String `json:"macos_setup_assistant"`
EnableReleaseDeviceManually optjson.Bool `json:"enable_release_device_manually"`
BootstrapPackage optjson.String `json:"bootstrap_package"`
EnableEndUserAuthentication bool `json:"enable_end_user_authentication"`
MacOSSetupAssistant optjson.String `json:"macos_setup_assistant"`
EnableReleaseDeviceManually optjson.Bool `json:"enable_release_device_manually"`
Script optjson.String `json:"script"`
Software optjson.Slice[*MacOSSetupSoftware] `json:"software"`
}
// MacOSSetupSoftware represents a VPP app or a software package to install
// during the setup experience of a macOS device.
type MacOSSetupSoftware struct {
AppStoreID string `json:"app_store_id"`
PackagePath string `json:"package_path"`
}
// MacOSMigration contains settings related to the MDM migration work flow.
@@ -670,6 +679,15 @@ func (c *AppConfig) Copy() *AppConfig {
clone.MDM.VolumePurchasingProgram = optjson.SetSlice(vpp)
}
if c.MDM.MacOSSetup.Software.Set {
sw := make([]*MacOSSetupSoftware, len(c.MDM.MacOSSetup.Software.Value))
for i, s := range c.MDM.MacOSSetup.Software.Value {
s := *s
sw[i] = &s
}
clone.MDM.MacOSSetup.Software = optjson.SetSlice(sw)
}
return &clone
}
+9 -8
View File
@@ -373,14 +373,15 @@ type ScriptPayload struct {
}
type SoftwareInstallerPayload struct {
URL string `json:"url"`
PreInstallQuery string `json:"pre_install_query"`
InstallScript string `json:"install_script"`
UninstallScript string `json:"uninstall_script"`
PostInstallScript string `json:"post_install_script"`
SelfService bool `json:"self_service"`
FleetMaintained bool `json:"-"`
Filename string `json:"-"`
URL string `json:"url"`
PreInstallQuery string `json:"pre_install_query"`
InstallScript string `json:"install_script"`
UninstallScript string `json:"uninstall_script"`
PostInstallScript string `json:"post_install_script"`
SelfService bool `json:"self_service"`
FleetMaintained bool `json:"-"`
Filename string `json:"-"`
InstallDuringSetup *bool `json:"install_during_setup"` // if nil, do not change saved value, otherwise set it
}
type HostLockWipeStatus struct {
+7 -5
View File
@@ -427,12 +427,14 @@ func SoftwareFromOsqueryRow(name, version, source, vendor, installedPath, releas
}
type VPPBatchPayload struct {
AppStoreID string `json:"app_store_id"`
SelfService bool `json:"self_service"`
AppStoreID string `json:"app_store_id"`
SelfService bool `json:"self_service"`
InstallDuringSetup *bool `json:"install_during_setup"` // keep saved value if nil, otherwise set as indicated
}
type VPPBatchPayloadWithPlatform struct {
AppStoreID string `json:"app_store_id"`
SelfService bool `json:"self_service"`
Platform AppleDevicePlatform `json:"platform"`
AppStoreID string `json:"app_store_id"`
SelfService bool `json:"self_service"`
Platform AppleDevicePlatform `json:"platform"`
InstallDuringSetup *bool `json:"install_during_setup"` // keep saved value if nil, otherwise set as indicated
}
+30 -19
View File
@@ -308,25 +308,26 @@ func (s *HostSoftwareInstallerResultAuthz) AuthzType() string {
}
type UploadSoftwareInstallerPayload struct {
TeamID *uint
InstallScript string
PreInstallQuery string
PostInstallScript string
InstallerFile io.ReadSeeker // TODO: maybe pull this out of the payload and only pass it to methods that need it (e.g., won't be needed when storing metadata in the database)
StorageID string
Filename string
Title string
Version string
Source string
Platform string
BundleIdentifier string
SelfService bool
UserID uint
URL string
FleetLibraryAppID *uint
PackageIDs []string
UninstallScript string
Extension string
TeamID *uint
InstallScript string
PreInstallQuery string
PostInstallScript string
InstallerFile io.ReadSeeker // TODO: maybe pull this out of the payload and only pass it to methods that need it (e.g., won't be needed when storing metadata in the database)
StorageID string
Filename string
Title string
Version string
Source string
Platform string
BundleIdentifier string
SelfService bool
UserID uint
URL string
FleetLibraryAppID *uint
PackageIDs []string
UninstallScript string
Extension string
InstallDuringSetup *bool // keep saved value if nil, otherwise set as indicated
}
type UpdateSoftwareInstallerPayload struct {
@@ -438,6 +439,16 @@ type SoftwarePackageSpec struct {
InstallScript TeamSpecSoftwareAsset `json:"install_script"`
PostInstallScript TeamSpecSoftwareAsset `json:"post_install_script"`
UninstallScript TeamSpecSoftwareAsset `json:"uninstall_script"`
// ReferencedYamlPath is the resolved path of the file used to fill the
// software package. Only present after parsing a GitOps file on the fleetctl
// side of processing. This is required to match a macos_setup.software to
// its corresponding software package, as we do this matching by yaml path.
//
// It must be JSON-marshaled because it gets set during gitops file processing,
// which is then re-marshaled to JSON from this struct and later re-unmarshaled
// during ApplyGroup...
ReferencedYamlPath string `json:"referenced_yaml_path"`
}
type SoftwareSpec struct {
+10
View File
@@ -239,6 +239,14 @@ func (t *TeamMDM) Copy() *TeamMDM {
}
clone.WindowsSettings.CustomSettings = optjson.SetSlice(windowsSettings)
}
if t.MacOSSetup.Software.Set {
sw := make([]*MacOSSetupSoftware, len(t.MacOSSetup.Software.Value))
for i, s := range t.MacOSSetup.Software.Value {
s := *s
sw[i] = &s
}
clone.MacOSSetup.Software = optjson.SetSlice(sw)
}
return &clone
}
@@ -518,5 +526,7 @@ func TeamSpecFromTeam(t *Team) (*TeamSpec, error) {
HostExpirySettings: &t.Config.HostExpirySettings,
WebhookSettings: webhookSettings,
Integrations: integrations,
Scripts: t.Config.Scripts,
Software: t.Config.Software,
}, nil
}
+7 -2
View File
@@ -16,8 +16,13 @@ type VPPAppID struct {
type VPPAppTeam struct {
VPPAppID
SelfService bool `db:"self_service" json:"self_service"`
InstallDuringSetup bool `db:"install_during_setup" json:"-"`
SelfService bool `db:"self_service" json:"self_service"`
// InstallDuringSetup is either the stored value of that flag for the VPP app
// or the value to set to that VPP app when batch-setting it. When used to
// set the value, if nil it will keep the currently saved value (or default
// to false), while if not nil, it will update the flag's value in the DB.
InstallDuringSetup *bool `db:"install_during_setup" json:"-"`
}
// VPPApp represents a VPP (Volume Purchase Program) application,
+57 -21
View File
@@ -908,8 +908,14 @@ func TestMDMAppleConfig(t *testing.T) {
name: "nochange",
licenseTier: "free",
expectedMDM: fleet.MDM{
AppleBusinessManager: optjson.Slice[fleet.MDMAppleABMAssignmentInfo]{Set: true, Value: []fleet.MDMAppleABMAssignmentInfo{}},
MacOSSetup: fleet.MacOSSetup{BootstrapPackage: optjson.String{Set: true}, MacOSSetupAssistant: optjson.String{Set: true}, EnableReleaseDeviceManually: optjson.SetBool(false)},
AppleBusinessManager: optjson.Slice[fleet.MDMAppleABMAssignmentInfo]{Set: true, Value: []fleet.MDMAppleABMAssignmentInfo{}},
MacOSSetup: fleet.MacOSSetup{
BootstrapPackage: optjson.String{Set: true},
MacOSSetupAssistant: optjson.String{Set: true},
EnableReleaseDeviceManually: optjson.SetBool(false),
Software: optjson.Slice[*fleet.MacOSSetupSoftware]{Set: true, Value: []*fleet.MacOSSetupSoftware{}},
Script: optjson.String{Set: true},
},
MacOSUpdates: fleet.AppleOSUpdateSettings{MinimumVersion: optjson.String{Set: true}, Deadline: optjson.String{Set: true}},
IOSUpdates: fleet.AppleOSUpdateSettings{MinimumVersion: optjson.String{Set: true}, Deadline: optjson.String{Set: true}},
IPadOSUpdates: fleet.AppleOSUpdateSettings{MinimumVersion: optjson.String{Set: true}, Deadline: optjson.String{Set: true}},
@@ -943,12 +949,18 @@ func TestMDMAppleConfig(t *testing.T) {
expectedMDM: fleet.MDM{
AppleBusinessManager: optjson.Slice[fleet.MDMAppleABMAssignmentInfo]{Set: true, Value: []fleet.MDMAppleABMAssignmentInfo{}},
DeprecatedAppleBMDefaultTeam: "foobar",
MacOSSetup: fleet.MacOSSetup{BootstrapPackage: optjson.String{Set: true}, MacOSSetupAssistant: optjson.String{Set: true}, EnableReleaseDeviceManually: optjson.SetBool(false)},
MacOSUpdates: fleet.AppleOSUpdateSettings{MinimumVersion: optjson.String{Set: true}, Deadline: optjson.String{Set: true}},
IOSUpdates: fleet.AppleOSUpdateSettings{MinimumVersion: optjson.String{Set: true}, Deadline: optjson.String{Set: true}},
IPadOSUpdates: fleet.AppleOSUpdateSettings{MinimumVersion: optjson.String{Set: true}, Deadline: optjson.String{Set: true}},
VolumePurchasingProgram: optjson.Slice[fleet.MDMAppleVolumePurchasingProgramInfo]{Set: true, Value: []fleet.MDMAppleVolumePurchasingProgramInfo{}},
WindowsUpdates: fleet.WindowsUpdates{DeadlineDays: optjson.Int{Set: true}, GracePeriodDays: optjson.Int{Set: true}},
MacOSSetup: fleet.MacOSSetup{
BootstrapPackage: optjson.String{Set: true},
MacOSSetupAssistant: optjson.String{Set: true},
EnableReleaseDeviceManually: optjson.SetBool(false),
Software: optjson.Slice[*fleet.MacOSSetupSoftware]{Set: true, Value: []*fleet.MacOSSetupSoftware{}},
Script: optjson.String{Set: true},
},
MacOSUpdates: fleet.AppleOSUpdateSettings{MinimumVersion: optjson.String{Set: true}, Deadline: optjson.String{Set: true}},
IOSUpdates: fleet.AppleOSUpdateSettings{MinimumVersion: optjson.String{Set: true}, Deadline: optjson.String{Set: true}},
IPadOSUpdates: fleet.AppleOSUpdateSettings{MinimumVersion: optjson.String{Set: true}, Deadline: optjson.String{Set: true}},
VolumePurchasingProgram: optjson.Slice[fleet.MDMAppleVolumePurchasingProgramInfo]{Set: true, Value: []fleet.MDMAppleVolumePurchasingProgramInfo{}},
WindowsUpdates: fleet.WindowsUpdates{DeadlineDays: optjson.Int{Set: true}, GracePeriodDays: optjson.Int{Set: true}},
WindowsSettings: fleet.WindowsSettings{
CustomSettings: optjson.Slice[fleet.MDMProfileSpec]{Set: true, Value: []fleet.MDMProfileSpec{}},
},
@@ -962,12 +974,18 @@ func TestMDMAppleConfig(t *testing.T) {
expectedMDM: fleet.MDM{
AppleBusinessManager: optjson.Slice[fleet.MDMAppleABMAssignmentInfo]{Set: true, Value: []fleet.MDMAppleABMAssignmentInfo{}},
DeprecatedAppleBMDefaultTeam: "foobar",
MacOSSetup: fleet.MacOSSetup{BootstrapPackage: optjson.String{Set: true}, MacOSSetupAssistant: optjson.String{Set: true}, EnableReleaseDeviceManually: optjson.SetBool(false)},
MacOSUpdates: fleet.AppleOSUpdateSettings{MinimumVersion: optjson.String{Set: true}, Deadline: optjson.String{Set: true}},
IOSUpdates: fleet.AppleOSUpdateSettings{MinimumVersion: optjson.String{Set: true}, Deadline: optjson.String{Set: true}},
IPadOSUpdates: fleet.AppleOSUpdateSettings{MinimumVersion: optjson.String{Set: true}, Deadline: optjson.String{Set: true}},
VolumePurchasingProgram: optjson.Slice[fleet.MDMAppleVolumePurchasingProgramInfo]{Set: true, Value: []fleet.MDMAppleVolumePurchasingProgramInfo{}},
WindowsUpdates: fleet.WindowsUpdates{DeadlineDays: optjson.Int{Set: true}, GracePeriodDays: optjson.Int{Set: true}},
MacOSSetup: fleet.MacOSSetup{
BootstrapPackage: optjson.String{Set: true},
MacOSSetupAssistant: optjson.String{Set: true},
EnableReleaseDeviceManually: optjson.SetBool(false),
Software: optjson.Slice[*fleet.MacOSSetupSoftware]{Set: true, Value: []*fleet.MacOSSetupSoftware{}},
Script: optjson.String{Set: true},
},
MacOSUpdates: fleet.AppleOSUpdateSettings{MinimumVersion: optjson.String{Set: true}, Deadline: optjson.String{Set: true}},
IOSUpdates: fleet.AppleOSUpdateSettings{MinimumVersion: optjson.String{Set: true}, Deadline: optjson.String{Set: true}},
IPadOSUpdates: fleet.AppleOSUpdateSettings{MinimumVersion: optjson.String{Set: true}, Deadline: optjson.String{Set: true}},
VolumePurchasingProgram: optjson.Slice[fleet.MDMAppleVolumePurchasingProgramInfo]{Set: true, Value: []fleet.MDMAppleVolumePurchasingProgramInfo{}},
WindowsUpdates: fleet.WindowsUpdates{DeadlineDays: optjson.Int{Set: true}, GracePeriodDays: optjson.Int{Set: true}},
WindowsSettings: fleet.WindowsSettings{
CustomSettings: optjson.Slice[fleet.MDMProfileSpec]{Set: true, Value: []fleet.MDMProfileSpec{}},
},
@@ -985,9 +1003,15 @@ func TestMDMAppleConfig(t *testing.T) {
newMDM: fleet.MDM{EndUserAuthentication: fleet.MDMEndUserAuthentication{SSOProviderSettings: fleet.SSOProviderSettings{EntityID: "foo"}}},
oldMDM: fleet.MDM{EndUserAuthentication: fleet.MDMEndUserAuthentication{SSOProviderSettings: fleet.SSOProviderSettings{EntityID: "foo"}}},
expectedMDM: fleet.MDM{
AppleBusinessManager: optjson.Slice[fleet.MDMAppleABMAssignmentInfo]{Set: true, Value: []fleet.MDMAppleABMAssignmentInfo{}},
EndUserAuthentication: fleet.MDMEndUserAuthentication{SSOProviderSettings: fleet.SSOProviderSettings{EntityID: "foo"}},
MacOSSetup: fleet.MacOSSetup{BootstrapPackage: optjson.String{Set: true}, MacOSSetupAssistant: optjson.String{Set: true}, EnableReleaseDeviceManually: optjson.SetBool(false)},
AppleBusinessManager: optjson.Slice[fleet.MDMAppleABMAssignmentInfo]{Set: true, Value: []fleet.MDMAppleABMAssignmentInfo{}},
EndUserAuthentication: fleet.MDMEndUserAuthentication{SSOProviderSettings: fleet.SSOProviderSettings{EntityID: "foo"}},
MacOSSetup: fleet.MacOSSetup{
BootstrapPackage: optjson.String{Set: true},
MacOSSetupAssistant: optjson.String{Set: true},
EnableReleaseDeviceManually: optjson.SetBool(false),
Software: optjson.Slice[*fleet.MacOSSetupSoftware]{Set: true, Value: []*fleet.MacOSSetupSoftware{}},
Script: optjson.String{Set: true},
},
MacOSUpdates: fleet.AppleOSUpdateSettings{MinimumVersion: optjson.String{Set: true}, Deadline: optjson.String{Set: true}},
IOSUpdates: fleet.AppleOSUpdateSettings{MinimumVersion: optjson.String{Set: true}, Deadline: optjson.String{Set: true}},
IPadOSUpdates: fleet.AppleOSUpdateSettings{MinimumVersion: optjson.String{Set: true}, Deadline: optjson.String{Set: true}},
@@ -1015,7 +1039,13 @@ func TestMDMAppleConfig(t *testing.T) {
MetadataURL: "http://isser.metadata.com",
IDPName: "onelogin",
}},
MacOSSetup: fleet.MacOSSetup{BootstrapPackage: optjson.String{Set: true}, MacOSSetupAssistant: optjson.String{Set: true}, EnableReleaseDeviceManually: optjson.SetBool(false)},
MacOSSetup: fleet.MacOSSetup{
BootstrapPackage: optjson.String{Set: true},
MacOSSetupAssistant: optjson.String{Set: true},
EnableReleaseDeviceManually: optjson.SetBool(false),
Software: optjson.Slice[*fleet.MacOSSetupSoftware]{Set: true, Value: []*fleet.MacOSSetupSoftware{}},
Script: optjson.String{Set: true},
},
MacOSUpdates: fleet.AppleOSUpdateSettings{MinimumVersion: optjson.String{Set: true}, Deadline: optjson.String{Set: true}},
IOSUpdates: fleet.AppleOSUpdateSettings{MinimumVersion: optjson.String{Set: true}, Deadline: optjson.String{Set: true}},
IPadOSUpdates: fleet.AppleOSUpdateSettings{MinimumVersion: optjson.String{Set: true}, Deadline: optjson.String{Set: true}},
@@ -1075,9 +1105,15 @@ func TestMDMAppleConfig(t *testing.T) {
EnableDiskEncryption: optjson.SetBool(false),
},
expectedMDM: fleet.MDM{
AppleBusinessManager: optjson.Slice[fleet.MDMAppleABMAssignmentInfo]{Set: true, Value: []fleet.MDMAppleABMAssignmentInfo{}},
EnableDiskEncryption: optjson.Bool{Set: true, Valid: true, Value: false},
MacOSSetup: fleet.MacOSSetup{BootstrapPackage: optjson.String{Set: true}, MacOSSetupAssistant: optjson.String{Set: true}, EnableReleaseDeviceManually: optjson.SetBool(false)},
AppleBusinessManager: optjson.Slice[fleet.MDMAppleABMAssignmentInfo]{Set: true, Value: []fleet.MDMAppleABMAssignmentInfo{}},
EnableDiskEncryption: optjson.Bool{Set: true, Valid: true, Value: false},
MacOSSetup: fleet.MacOSSetup{
BootstrapPackage: optjson.String{Set: true},
MacOSSetupAssistant: optjson.String{Set: true},
EnableReleaseDeviceManually: optjson.SetBool(false),
Software: optjson.Slice[*fleet.MacOSSetupSoftware]{Set: true, Value: []*fleet.MacOSSetupSoftware{}},
Script: optjson.String{Set: true},
},
MacOSUpdates: fleet.AppleOSUpdateSettings{MinimumVersion: optjson.String{Set: true}, Deadline: optjson.String{Set: true}},
IOSUpdates: fleet.AppleOSUpdateSettings{MinimumVersion: optjson.String{Set: true}, Deadline: optjson.String{Set: true}},
IPadOSUpdates: fleet.AppleOSUpdateSettings{MinimumVersion: optjson.String{Set: true}, Deadline: optjson.String{Set: true}},
+228 -30
View File
@@ -389,9 +389,32 @@ func getProfilesContents(baseDir string, macProfiles []fleet.MDMProfileSpec, win
return result, nil
}
// fileContent is used to store the name of a file and its content.
type fileContent struct {
Filename string
Content []byte
}
// TODO: as confirmed by Noah and Marko on Slack:
//
// > from Noah: "We want to support existing features w/ fleetctl apply for
// > backwards compatibility GitOps but we dont need to add new features."
//
// We should deprecate ApplyGroup and use it only for `fleetctl apply` (and
// its current minimal use in `preview`), and have a distinct implementation
// that is `gitops`-only, because both uses have subtle differences in
// behaviour that make it hard to reuse a single implementation (e.g. a missing
// key in gitops means "remove what is absent" while in apply it means "leave
// as-is").
//
// For now I'm just passing a "gitops" bool for a quick fix, but we should
// properly plan that separation and refactor so that gitops can be
// significantly cleaned up and simplified going forward.
// ApplyGroup applies the given spec group to Fleet.
func (c *Client) ApplyGroup(
ctx context.Context,
viaGitOps bool,
specs *spec.Group,
baseDir string,
logf func(format string, args ...interface{}),
@@ -554,6 +577,15 @@ func (c *Client) ApplyGroup(
tmMacSetup := extractTmSpecsMacOSSetup(specs.Teams)
tmBootstrapPackages := make(map[string]*fleet.MDMAppleBootstrapPackage, len(tmMacSetup))
tmMacSetupAssistants := make(map[string][]byte, len(tmMacSetup))
// those are gitops-only features
tmMacSetupScript := make(map[string]fileContent, len(tmMacSetup))
tmMacSetupSoftware := make(map[string][]*fleet.MacOSSetupSoftware, len(tmMacSetup))
// this is a set of software packages or VPP apps that are configured as
// install_during_setup, by team. This is a gitops-only setting, so it will
// only be filled when called via this command.
tmSoftwareMacOSSetup := make(map[string]map[fleet.MacOSSetupSoftware]struct{}, len(tmMacSetup))
for k, setup := range tmMacSetup {
if setup.BootstrapPackage.Value != "" {
bp, err := c.ValidateBootstrapPackageFromURL(setup.BootstrapPackage.Value)
@@ -569,6 +601,21 @@ func (c *Client) ApplyGroup(
}
tmMacSetupAssistants[k] = b
}
if setup.Script.Value != "" {
b, err := c.validateMacOSSetupScript(resolveApplyRelativePath(baseDir, setup.Script.Value))
if err != nil {
return nil, nil, nil, fmt.Errorf("applying teams: %w", err)
}
tmMacSetupScript[k] = fileContent{Filename: filepath.Base(setup.Script.Value), Content: b}
}
if viaGitOps {
m, err := extractTeamOrNoTeamMacOSSetupSoftware(baseDir, setup.Software.Value)
if err != nil {
return nil, nil, nil, err
}
tmSoftwareMacOSSetup[k] = m
tmMacSetupSoftware[k] = setup.Software.Value
}
}
tmScripts := extractTmSpecsScripts(specs.Teams)
@@ -590,24 +637,59 @@ func (c *Client) ApplyGroup(
tmSoftwarePackages := extractTmSpecsSoftwarePackages(specs.Teams)
tmSoftwarePackagesPayloads := make(map[string][]fleet.SoftwareInstallerPayload, len(tmSoftwarePackages))
tmSoftwarePackageByPath := make(map[string]map[string]fleet.SoftwarePackageSpec, len(tmSoftwarePackages))
for tmName, software := range tmSoftwarePackages {
softwarePayloads, err := buildSoftwarePackagesPayload(baseDir, software)
installDuringSetupKeys := tmSoftwareMacOSSetup[tmName]
softwarePayloads, err := buildSoftwarePackagesPayload(baseDir, software, installDuringSetupKeys)
if err != nil {
return nil, nil, nil, fmt.Errorf("applying software installers for team %q: %w", tmName, err)
}
tmSoftwarePackagesPayloads[tmName] = softwarePayloads
for _, swSpec := range software {
if swSpec.ReferencedYamlPath != "" {
// can be referenced by macos_setup.software.package_path
if tmSoftwarePackageByPath[tmName] == nil {
tmSoftwarePackageByPath[tmName] = make(map[string]fleet.SoftwarePackageSpec, len(software))
}
tmSoftwarePackageByPath[tmName][swSpec.ReferencedYamlPath] = swSpec
}
}
}
tmSoftwareApps := extractTmSpecsSoftwareApps(specs.Teams)
tmSoftwareAppsPayloads := make(map[string][]fleet.VPPBatchPayload)
tmSoftwareAppsByAppID := make(map[string]map[string]fleet.TeamSpecAppStoreApp, len(tmSoftwareApps))
for tmName, apps := range tmSoftwareApps {
installDuringSetupKeys := tmSoftwareMacOSSetup[tmName]
appPayloads := make([]fleet.VPPBatchPayload, 0, len(apps))
for _, app := range apps {
appPayloads = append(appPayloads, fleet.VPPBatchPayload{AppStoreID: app.AppStoreID, SelfService: app.SelfService})
var installDuringSetup *bool
if installDuringSetupKeys != nil {
_, ok := installDuringSetupKeys[fleet.MacOSSetupSoftware{AppStoreID: app.AppStoreID}]
installDuringSetup = &ok
}
appPayloads = append(appPayloads, fleet.VPPBatchPayload{
AppStoreID: app.AppStoreID,
SelfService: app.SelfService,
InstallDuringSetup: installDuringSetup,
})
// can be referenced by macos_setup.software.app_store_id
if tmSoftwareAppsByAppID[tmName] == nil {
tmSoftwareAppsByAppID[tmName] = make(map[string]fleet.TeamSpecAppStoreApp, len(apps))
}
tmSoftwareAppsByAppID[tmName][app.AppStoreID] = app
}
tmSoftwareAppsPayloads[tmName] = appPayloads
}
// if macos_setup.software has some values, they must exist in the software
// packages or vpp apps.
for tmName, setupSw := range tmMacSetupSoftware {
if err := validateTeamOrNoTeamMacOSSetupSoftware(tmName, setupSw, tmSoftwarePackageByPath[tmName], tmSoftwareAppsByAppID[tmName]); err != nil {
return nil, nil, nil, err
}
}
// Next, apply the teams specs before saving the profiles, so that any
// non-existing team gets created.
var err error
@@ -675,6 +757,19 @@ func (c *Client) ApplyGroup(
}
}
}
if viaGitOps && !opts.DryRun {
for tmName, tmID := range teamIDsByName {
if fc, ok := tmMacSetupScript[tmName]; ok {
if err := c.uploadMacOSSetupScript(fc.Filename, fc.Content, &tmID); err != nil {
return nil, nil, nil, fmt.Errorf("uploading setup experience script for team %q: %w", tmName, err)
}
} else {
if err := c.deleteMacOSSetupScript(&tmID); err != nil {
return nil, nil, nil, fmt.Errorf("deleting setup experience script for team %q: %w", tmName, err)
}
}
}
}
if len(tmScriptsPayloads) > 0 {
for tmName, scripts := range tmScriptsPayloads {
// For non-dry run, currentTeamName and tmName are the same
@@ -702,6 +797,7 @@ func (c *Client) ApplyGroup(
for tmName, apps := range tmSoftwareAppsPayloads {
// For non-dry run, currentTeamName and tmName are the same
currentTeamName := getTeamName(tmName)
logfn("[+] applying %d app store apps for team %s\n", len(apps), tmName)
if err := c.ApplyTeamAppStoreAppsAssociation(currentTeamName, apps, opts.ApplySpecOptions); err != nil {
return nil, nil, nil, fmt.Errorf("applying app store apps for team: %q: %w", tmName, err)
}
@@ -752,7 +848,45 @@ func (c *Client) ApplyGroup(
return teamIDsByName, teamsSoftwareInstallers, teamsScripts, nil
}
func buildSoftwarePackagesPayload(baseDir string, specs []fleet.SoftwarePackageSpec) ([]fleet.SoftwareInstallerPayload, error) {
func extractTeamOrNoTeamMacOSSetupSoftware(baseDir string, software []*fleet.MacOSSetupSoftware) (map[fleet.MacOSSetupSoftware]struct{}, error) {
m := make(map[fleet.MacOSSetupSoftware]struct{}, len(software))
for _, sw := range software {
if sw.AppStoreID != "" && sw.PackagePath != "" {
return nil, errors.New("applying teams: only one of app_store_id or package_path can be set")
}
if sw.PackagePath != "" {
sw.PackagePath = resolveApplyRelativePath(baseDir, sw.PackagePath)
}
m[*sw] = struct{}{}
}
return m, nil
}
func validateTeamOrNoTeamMacOSSetupSoftware(teamName string, macOSSetupSoftware []*fleet.MacOSSetupSoftware, packagesByPath map[string]fleet.SoftwarePackageSpec, vppAppsByAppID map[string]fleet.TeamSpecAppStoreApp) error {
// if macos_setup.software has some values, they must exist in the software
// packages or vpp apps.
for _, ssw := range macOSSetupSoftware {
var valid bool
if ssw.AppStoreID != "" {
// check that it exists in the team's Apps
_, valid = vppAppsByAppID[ssw.AppStoreID]
} else if ssw.PackagePath != "" {
// check that it exists in the team's Software installers (PackagePath is
// already resolved to abs dir)
_, valid = packagesByPath[ssw.PackagePath]
}
if !valid {
label := ssw.AppStoreID
if label == "" {
label = ssw.PackagePath
}
return fmt.Errorf("applying macOS setup experience software for team %q: software %q does not exist for that team", teamName, label)
}
}
return nil
}
func buildSoftwarePackagesPayload(baseDir string, specs []fleet.SoftwarePackageSpec, installDuringSetupKeys map[fleet.MacOSSetupSoftware]struct{}) ([]fleet.SoftwareInstallerPayload, error) {
softwarePayloads := make([]fleet.SoftwareInstallerPayload, len(specs))
for i, si := range specs {
var qc string
@@ -838,15 +972,20 @@ func buildSoftwarePackagesPayload(baseDir string, specs []fleet.SoftwarePackageS
}
}
var installDuringSetup *bool
if installDuringSetupKeys != nil {
_, ok := installDuringSetupKeys[fleet.MacOSSetupSoftware{PackagePath: si.ReferencedYamlPath}]
installDuringSetup = &ok
}
softwarePayloads[i] = fleet.SoftwareInstallerPayload{
URL: si.URL,
SelfService: si.SelfService,
PreInstallQuery: qc,
InstallScript: string(ic),
PostInstallScript: string(pc),
UninstallScript: string(us),
URL: si.URL,
SelfService: si.SelfService,
PreInstallQuery: qc,
InstallScript: string(ic),
PostInstallScript: string(pc),
UninstallScript: string(us),
InstallDuringSetup: installDuringSetup,
}
}
return softwarePayloads, nil
@@ -1478,7 +1617,7 @@ func (c *Client) DoGitOps(
}
// Apply org settings, scripts, enroll secrets, team entities (software, scripts, etc.), and controls.
teamIDsByName, teamsSoftwareInstallers, teamsScripts, err := c.ApplyGroup(ctx, &group, baseDir, logf, appConfig, fleet.ApplyClientSpecOptions{
teamIDsByName, teamsSoftwareInstallers, teamsScripts, err := c.ApplyGroup(ctx, true, &group, baseDir, logf, appConfig, fleet.ApplyClientSpecOptions{
ApplySpecOptions: fleet.ApplySpecOptions{
DryRun: dryRun,
},
@@ -1535,30 +1674,89 @@ func (c *Client) DoGitOps(
return teamAssumptions, nil
}
func (c *Client) doGitOpsNoTeamSoftware(config *spec.GitOps, baseDir string, appconfig *fleet.EnrichedAppConfig, logFn func(format string, args ...interface{}), dryRun bool) ([]fleet.SoftwarePackageResponse, error) {
func (c *Client) doGitOpsNoTeamSoftware(
config *spec.GitOps,
baseDir string,
appconfig *fleet.EnrichedAppConfig,
logFn func(format string, args ...interface{}),
dryRun bool,
) ([]fleet.SoftwarePackageResponse, error) {
if !config.IsNoTeam() || appconfig == nil || !appconfig.License.IsPremium() {
return nil, nil
}
var softwareInstallers []fleet.SoftwarePackageResponse
if config.IsNoTeam() && appconfig != nil && appconfig.License.IsPremium() {
packages := make([]fleet.SoftwarePackageSpec, 0, len(config.Software.Packages))
for _, software := range config.Software.Packages {
if software != nil {
packages = append(packages, *software)
packages := make([]fleet.SoftwarePackageSpec, 0, len(config.Software.Packages))
packagesByPath := make(map[string]fleet.SoftwarePackageSpec, len(config.Software.Packages))
for _, software := range config.Software.Packages {
if software != nil {
packages = append(packages, *software)
if software.ReferencedYamlPath != "" {
// can be referenced by macos_setup.software
packagesByPath[software.ReferencedYamlPath] = *software
}
}
payload, err := buildSoftwarePackagesPayload(baseDir, packages)
if err != nil {
return nil, fmt.Errorf("applying software installers: %w", err)
}
logFn("[+] applying %d software packages for 'No team'\n", len(payload))
softwareInstallers, err = c.ApplyNoTeamSoftwareInstallers(payload, fleet.ApplySpecOptions{DryRun: dryRun})
if err != nil {
return nil, fmt.Errorf("applying software installers: %w", err)
}
}
if dryRun {
logFn("[+] would've applied 'No Team' software packages\n")
} else {
logFn("[+] applied 'No Team' software packages\n")
// marshaling dance to get the macos_setup data - config.Controls.MacOSSetup
// is of type any and contains a generic map[string]any. By
// marshal-unmarshaling it into a properly typed struct, we avoid having to
// do a bunch of error-prone and unmaintainable type-assertions to walk down
// the untyped map.
b, err := json.Marshal(config.Controls.MacOSSetup)
if err != nil {
return nil, fmt.Errorf("applying software installers: json-encode controls.macos_setup: %w", err)
}
var macOSSetup fleet.MacOSSetup
if err := json.Unmarshal(b, &macOSSetup); err != nil {
return nil, fmt.Errorf("applying software installers: json-decode controls.macos_setup: %w", err)
}
// load the no-team macos_setup.script if any
var macosSetupScript *fileContent
if macOSSetup.Script.Value != "" {
b, err := c.validateMacOSSetupScript(resolveApplyRelativePath(baseDir, macOSSetup.Script.Value))
if err != nil {
return nil, fmt.Errorf("applying no team macos_setup.script: %w", err)
}
macosSetupScript = &fileContent{Filename: filepath.Base(macOSSetup.Script.Value), Content: b}
}
noTeamSoftwareMacOSSetup, err := extractTeamOrNoTeamMacOSSetupSoftware(baseDir, macOSSetup.Software.Value)
if err != nil {
return nil, err
}
// TODO: note that VPP apps are not validated nor taken into account at the moment,
// tracked with issue https://github.com/fleetdm/fleet/issues/22970
if err := validateTeamOrNoTeamMacOSSetupSoftware(*config.TeamName, macOSSetup.Software.Value, packagesByPath, nil); err != nil {
return nil, err
}
payload, err := buildSoftwarePackagesPayload(baseDir, packages, noTeamSoftwareMacOSSetup)
if err != nil {
return nil, fmt.Errorf("applying software installers: %w", err)
}
if macosSetupScript != nil {
logFn("[+] applying macos setup experience script for 'No team'\n")
if err := c.uploadMacOSSetupScript(macosSetupScript.Filename, macosSetupScript.Content, nil); err != nil {
return nil, fmt.Errorf("uploading setup experience script for No team: %w", err)
}
} else if err := c.deleteMacOSSetupScript(nil); err != nil {
return nil, fmt.Errorf("deleting setup experience script for No team: %w", err)
}
logFn("[+] applying %d software packages for 'No team'\n", len(payload))
softwareInstallers, err = c.ApplyNoTeamSoftwareInstallers(payload, fleet.ApplySpecOptions{DryRun: dryRun})
if err != nil {
return nil, fmt.Errorf("applying software installers: %w", err)
}
if dryRun {
logFn("[+] would've applied 'No Team' software packages\n")
} else {
logFn("[+] applied 'No Team' software packages\n")
}
return softwareInstallers, nil
}
+79
View File
@@ -1,11 +1,15 @@
package service
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"mime/multipart"
"net/http"
"os"
"strings"
"time"
@@ -139,3 +143,78 @@ func (c *Client) ApplyNoTeamScripts(scripts []fleet.ScriptPayload, opts fleet.Ap
return resp.Scripts, err
}
func (c *Client) validateMacOSSetupScript(fileName string) ([]byte, error) {
if err := c.CheckAppleMDMEnabled(); err != nil {
return nil, err
}
b, err := os.ReadFile(fileName)
if err != nil {
return nil, err
}
return b, nil
}
func (c *Client) deleteMacOSSetupScript(teamID *uint) error {
var query string
if teamID != nil {
query = fmt.Sprintf("team_id=%d", *teamID)
}
verb, path := "DELETE", "/api/latest/fleet/setup_experience/script"
var delResp deleteSetupExperienceScriptResponse
return c.authenticatedRequestWithQuery(nil, verb, path, &delResp, query)
}
func (c *Client) uploadMacOSSetupScript(filename string, data []byte, teamID *uint) error {
// there is no "replace setup experience script" endpoint, and none was
// planned, so to avoid delaying the feature I'm doing DELETE then SET, but
// that's not ideal (will always re-create the script when apply/gitops is
// run with the same yaml). Note though that we also redo software installers
// downloads on each run, so the churn of this one is minor in comparison.
if err := c.deleteMacOSSetupScript(teamID); err != nil {
return err
}
verb, path := "POST", "/api/latest/fleet/setup_experience/script"
var b bytes.Buffer
w := multipart.NewWriter(&b)
fw, err := w.CreateFormFile("script", filename)
if err != nil {
return err
}
if _, err := io.Copy(fw, bytes.NewBuffer(data)); err != nil {
return err
}
// add the team_id field
if teamID != nil {
if err := w.WriteField("team_id", fmt.Sprint(*teamID)); err != nil {
return err
}
}
w.Close()
response, err := c.doContextWithBodyAndHeaders(context.Background(), verb, path, "",
b.Bytes(),
map[string]string{
"Content-Type": w.FormDataContentType(),
"Accept": "application/json",
"Authorization": fmt.Sprintf("Bearer %s", c.token),
},
)
if err != nil {
return fmt.Errorf("do multipart request: %w", err)
}
defer response.Body.Close()
var resp setSetupExperienceScriptResponse
if err := c.parseResponse(verb, path, response, &resp); err != nil {
return fmt.Errorf("parse response: %w", err)
}
return nil
}
@@ -235,6 +235,8 @@ func (s *integrationEnterpriseTestSuite) TestTeamSpecs() {
MacOSSetupAssistant: optjson.String{Set: true},
BootstrapPackage: optjson.String{Set: true},
EnableReleaseDeviceManually: optjson.SetBool(false),
Script: optjson.String{Set: true},
Software: optjson.Slice[*fleet.MacOSSetupSoftware]{Set: true, Value: []*fleet.MacOSSetupSoftware{}},
},
// because the WindowsSettings was marshalled to JSON to be saved in the DB,
// it did get marshalled, and then when unmarshalled it was set (but
@@ -337,6 +339,8 @@ func (s *integrationEnterpriseTestSuite) TestTeamSpecs() {
MacOSSetupAssistant: optjson.String{Set: true},
BootstrapPackage: optjson.String{Set: true},
EnableReleaseDeviceManually: optjson.SetBool(false),
Script: optjson.String{Set: true},
Software: optjson.Slice[*fleet.MacOSSetupSoftware]{Set: true, Value: []*fleet.MacOSSetupSoftware{}},
},
WindowsSettings: fleet.WindowsSettings{
CustomSettings: optjson.Slice[fleet.MDMProfileSpec]{Set: true, Value: []fleet.MDMProfileSpec{}},
@@ -367,6 +371,8 @@ func (s *integrationEnterpriseTestSuite) TestTeamSpecs() {
MacOSSetupAssistant: optjson.String{Set: true},
BootstrapPackage: optjson.String{Set: true},
EnableReleaseDeviceManually: optjson.SetBool(false),
Script: optjson.String{Set: true},
Software: optjson.Slice[*fleet.MacOSSetupSoftware]{Set: true, Value: []*fleet.MacOSSetupSoftware{}},
},
WindowsSettings: fleet.WindowsSettings{
CustomSettings: optjson.Slice[fleet.MDMProfileSpec]{Set: true, Value: []fleet.MDMProfileSpec{}},
@@ -399,6 +405,8 @@ func (s *integrationEnterpriseTestSuite) TestTeamSpecs() {
MacOSSetupAssistant: optjson.String{Set: true},
BootstrapPackage: optjson.String{Set: true},
EnableReleaseDeviceManually: optjson.SetBool(false),
Script: optjson.String{Set: true},
Software: optjson.Slice[*fleet.MacOSSetupSoftware]{Set: true, Value: []*fleet.MacOSSetupSoftware{}},
},
WindowsSettings: fleet.WindowsSettings{
CustomSettings: optjson.Slice[fleet.MDMProfileSpec]{Set: true, Value: []fleet.MDMProfileSpec{}},
@@ -2365,6 +2373,8 @@ func (s *integrationEnterpriseTestSuite) TestWindowsUpdatesTeamConfig() {
MacOSSetupAssistant: optjson.String{Set: true},
BootstrapPackage: optjson.String{Set: true},
EnableReleaseDeviceManually: optjson.SetBool(false),
Script: optjson.String{Set: true},
Software: optjson.Slice[*fleet.MacOSSetupSoftware]{Set: true, Value: []*fleet.MacOSSetupSoftware{}},
},
WindowsSettings: fleet.WindowsSettings{
CustomSettings: optjson.Slice[fleet.MDMProfileSpec]{Set: true, Value: []fleet.MDMProfileSpec{}},
+36 -34
View File
@@ -1423,18 +1423,18 @@ func (s *integrationMDMTestSuite) TestSetupExperienceFlowWithSoftwareAndScriptAu
require.NoError(t, plist.Unmarshal(cmd.Raw, &fullCmd))
// Can be useful for debugging
//switch cmd.Command.RequestType {
//case "InstallProfile":
// fmt.Println(">>>> device received command: ", cmd.CommandUUID, cmd.Command.RequestType, string(fullCmd.Command.InstallProfile.Payload))
//case "InstallEnterpriseApplication":
// if fullCmd.Command.InstallEnterpriseApplication.ManifestURL != nil {
// fmt.Println(">>>> device received command: ", cmd.CommandUUID, cmd.Command.RequestType, *fullCmd.Command.InstallEnterpriseApplication.ManifestURL)
// } else {
// fmt.Println(">>>> device received command: ", cmd.CommandUUID, cmd.Command.RequestType)
// }
//default:
// fmt.Println(">>>> device received command: ", cmd.Command.RequestType)
//}
// switch cmd.Command.RequestType {
// case "InstallProfile":
// fmt.Println(">>>> device received command: ", cmd.CommandUUID, cmd.Command.RequestType, string(fullCmd.Command.InstallProfile.Payload))
// case "InstallEnterpriseApplication":
// if fullCmd.Command.InstallEnterpriseApplication.ManifestURL != nil {
// fmt.Println(">>>> device received command: ", cmd.CommandUUID, cmd.Command.RequestType, *fullCmd.Command.InstallEnterpriseApplication.ManifestURL)
// } else {
// fmt.Println(">>>> device received command: ", cmd.CommandUUID, cmd.Command.RequestType)
// }
// default:
// fmt.Println(">>>> device received command: ", cmd.Command.RequestType)
// }
cmds = append(cmds, &fullCmd)
cmd, err = mdmDevice.Acknowledge(cmd.CommandUUID)
@@ -1450,14 +1450,15 @@ func (s *integrationMDMTestSuite) TestSetupExperienceFlowWithSoftwareAndScriptAu
switch cmd.Command.RequestType {
case "InstallProfile":
installProfileCount++
if strings.Contains(string(cmd.Command.InstallProfile.Payload), "<string>I1</string>") {
switch {
case strings.Contains(string(cmd.Command.InstallProfile.Payload), "<string>I1</string>"):
profileCustomSeen = true
} else if strings.Contains(string(cmd.Command.InstallProfile.Payload), fmt.Sprintf("<string>%s</string>", mobileconfig.FleetdConfigPayloadIdentifier)) {
case strings.Contains(string(cmd.Command.InstallProfile.Payload), fmt.Sprintf("<string>%s</string>", mobileconfig.FleetdConfigPayloadIdentifier)):
profileFleetdSeen = true
} else if strings.Contains(string(cmd.Command.InstallProfile.Payload), fmt.Sprintf("<string>%s</string>", mobileconfig.FleetCARootConfigPayloadIdentifier)) {
case strings.Contains(string(cmd.Command.InstallProfile.Payload), fmt.Sprintf("<string>%s</string>", mobileconfig.FleetCARootConfigPayloadIdentifier)):
profileFleetCASeen = true
} else if strings.Contains(string(cmd.Command.InstallProfile.Payload), fmt.Sprintf("<string>%s</string", mobileconfig.FleetFileVaultPayloadIdentifier)) &&
strings.Contains(string(cmd.Command.InstallProfile.Payload), "ForceEnableInSetupAssistant") {
case strings.Contains(string(cmd.Command.InstallProfile.Payload), fmt.Sprintf("<string>%s</string", mobileconfig.FleetFileVaultPayloadIdentifier)) &&
strings.Contains(string(cmd.Command.InstallProfile.Payload), "ForceEnableInSetupAssistant"):
profileFileVaultSeen = true
}
@@ -1612,18 +1613,18 @@ func (s *integrationMDMTestSuite) TestSetupExperienceFlowWithSoftwareAndScriptFo
require.NoError(t, plist.Unmarshal(cmd.Raw, &fullCmd))
// Can be useful for debugging
//switch cmd.Command.RequestType {
//case "InstallProfile":
// fmt.Println(">>>> device received command: ", cmd.CommandUUID, cmd.Command.RequestType, string(fullCmd.Command.InstallProfile.Payload))
//case "InstallEnterpriseApplication":
// if fullCmd.Command.InstallEnterpriseApplication.ManifestURL != nil {
// fmt.Println(">>>> device received command: ", cmd.CommandUUID, cmd.Command.RequestType, *fullCmd.Command.InstallEnterpriseApplication.ManifestURL)
// } else {
// fmt.Println(">>>> device received command: ", cmd.CommandUUID, cmd.Command.RequestType)
// }
//default:
// fmt.Println(">>>> device received command: ", cmd.Command.RequestType)
//}
// switch cmd.Command.RequestType {
// case "InstallProfile":
// fmt.Println(">>>> device received command: ", cmd.CommandUUID, cmd.Command.RequestType, string(fullCmd.Command.InstallProfile.Payload))
// case "InstallEnterpriseApplication":
// if fullCmd.Command.InstallEnterpriseApplication.ManifestURL != nil {
// fmt.Println(">>>> device received command: ", cmd.CommandUUID, cmd.Command.RequestType, *fullCmd.Command.InstallEnterpriseApplication.ManifestURL)
// } else {
// fmt.Println(">>>> device received command: ", cmd.CommandUUID, cmd.Command.RequestType)
// }
// default:
// fmt.Println(">>>> device received command: ", cmd.Command.RequestType)
// }
cmds = append(cmds, &fullCmd)
cmd, err = mdmDevice.Acknowledge(cmd.CommandUUID)
@@ -1639,14 +1640,15 @@ func (s *integrationMDMTestSuite) TestSetupExperienceFlowWithSoftwareAndScriptFo
switch cmd.Command.RequestType {
case "InstallProfile":
installProfileCount++
if strings.Contains(string(cmd.Command.InstallProfile.Payload), "<string>I1</string>") {
switch {
case strings.Contains(string(cmd.Command.InstallProfile.Payload), "<string>I1</string>"):
profileCustomSeen = true
} else if strings.Contains(string(cmd.Command.InstallProfile.Payload), fmt.Sprintf("<string>%s</string>", mobileconfig.FleetdConfigPayloadIdentifier)) {
case strings.Contains(string(cmd.Command.InstallProfile.Payload), fmt.Sprintf("<string>%s</string>", mobileconfig.FleetdConfigPayloadIdentifier)):
profileFleetdSeen = true
} else if strings.Contains(string(cmd.Command.InstallProfile.Payload), fmt.Sprintf("<string>%s</string>", mobileconfig.FleetCARootConfigPayloadIdentifier)) {
case strings.Contains(string(cmd.Command.InstallProfile.Payload), fmt.Sprintf("<string>%s</string>", mobileconfig.FleetCARootConfigPayloadIdentifier)):
profileFleetCASeen = true
} else if strings.Contains(string(cmd.Command.InstallProfile.Payload), fmt.Sprintf("<string>%s</string", mobileconfig.FleetFileVaultPayloadIdentifier)) &&
strings.Contains(string(cmd.Command.InstallProfile.Payload), "ForceEnableInSetupAssistant") {
case strings.Contains(string(cmd.Command.InstallProfile.Payload), fmt.Sprintf("<string>%s</string", mobileconfig.FleetFileVaultPayloadIdentifier)) &&
strings.Contains(string(cmd.Command.InstallProfile.Payload), "ForceEnableInSetupAssistant"):
profileFileVaultSeen = true
}
+25 -8
View File
@@ -10189,7 +10189,11 @@ func (s *integrationMDMTestSuite) TestBatchAssociateAppStoreApps() {
assert.Contains(t, assoc, fleet.VPPAppID{AdamID: s.appleVPPConfigSrvConfig.Assets[1].AdamID, Platform: fleet.IOSPlatform})
assert.Contains(t, assoc, fleet.VPPAppID{AdamID: s.appleVPPConfigSrvConfig.Assets[1].AdamID, Platform: fleet.IPadOSPlatform})
// Only macOS version should be self-service
assert.Equal(t, fleet.VPPAppTeam{VPPAppID: fleet.VPPAppID{AdamID: s.appleVPPConfigSrvConfig.Assets[1].AdamID, Platform: fleet.MacOSPlatform}, SelfService: true},
assert.Equal(t, fleet.VPPAppTeam{
VPPAppID: fleet.VPPAppID{AdamID: s.appleVPPConfigSrvConfig.Assets[1].AdamID, Platform: fleet.MacOSPlatform},
SelfService: true,
InstallDuringSetup: ptr.Bool(false),
},
assoc[fleet.VPPAppID{AdamID: s.appleVPPConfigSrvConfig.Assets[1].AdamID, Platform: fleet.MacOSPlatform}])
// Reverse self-service associations
@@ -10206,10 +10210,23 @@ func (s *integrationMDMTestSuite) TestBatchAssociateAppStoreApps() {
assoc, err = s.ds.GetAssignedVPPApps(ctx, &tmGood.ID)
require.NoError(t, err)
require.Len(t, assoc, 4)
assert.Equal(t, fleet.VPPAppTeam{VPPAppID: fleet.VPPAppID{AdamID: s.appleVPPConfigSrvConfig.Assets[0].AdamID, Platform: fleet.MacOSPlatform}, SelfService: true}, assoc[fleet.VPPAppID{AdamID: s.appleVPPConfigSrvConfig.Assets[0].AdamID, Platform: fleet.MacOSPlatform}])
assert.Equal(t, fleet.VPPAppTeam{VPPAppID: fleet.VPPAppID{AdamID: s.appleVPPConfigSrvConfig.Assets[1].AdamID, Platform: fleet.IOSPlatform}}, assoc[fleet.VPPAppID{AdamID: s.appleVPPConfigSrvConfig.Assets[1].AdamID, Platform: fleet.IOSPlatform}])
assert.Equal(t, fleet.VPPAppTeam{VPPAppID: fleet.VPPAppID{AdamID: s.appleVPPConfigSrvConfig.Assets[1].AdamID, Platform: fleet.IPadOSPlatform}}, assoc[fleet.VPPAppID{AdamID: s.appleVPPConfigSrvConfig.Assets[1].AdamID, Platform: fleet.IPadOSPlatform}])
assert.Equal(t, fleet.VPPAppTeam{VPPAppID: fleet.VPPAppID{AdamID: s.appleVPPConfigSrvConfig.Assets[1].AdamID, Platform: fleet.MacOSPlatform}}, assoc[fleet.VPPAppID{AdamID: s.appleVPPConfigSrvConfig.Assets[1].AdamID, Platform: fleet.MacOSPlatform}])
assert.Equal(t, fleet.VPPAppTeam{
VPPAppID: fleet.VPPAppID{AdamID: s.appleVPPConfigSrvConfig.Assets[0].AdamID, Platform: fleet.MacOSPlatform},
SelfService: true,
InstallDuringSetup: ptr.Bool(false),
}, assoc[fleet.VPPAppID{AdamID: s.appleVPPConfigSrvConfig.Assets[0].AdamID, Platform: fleet.MacOSPlatform}])
assert.Equal(t, fleet.VPPAppTeam{
VPPAppID: fleet.VPPAppID{AdamID: s.appleVPPConfigSrvConfig.Assets[1].AdamID, Platform: fleet.IOSPlatform},
InstallDuringSetup: ptr.Bool(false),
}, assoc[fleet.VPPAppID{AdamID: s.appleVPPConfigSrvConfig.Assets[1].AdamID, Platform: fleet.IOSPlatform}])
assert.Equal(t, fleet.VPPAppTeam{
VPPAppID: fleet.VPPAppID{AdamID: s.appleVPPConfigSrvConfig.Assets[1].AdamID, Platform: fleet.IPadOSPlatform},
InstallDuringSetup: ptr.Bool(false),
}, assoc[fleet.VPPAppID{AdamID: s.appleVPPConfigSrvConfig.Assets[1].AdamID, Platform: fleet.IPadOSPlatform}])
assert.Equal(t, fleet.VPPAppTeam{
VPPAppID: fleet.VPPAppID{AdamID: s.appleVPPConfigSrvConfig.Assets[1].AdamID, Platform: fleet.MacOSPlatform},
InstallDuringSetup: ptr.Bool(false),
}, assoc[fleet.VPPAppID{AdamID: s.appleVPPConfigSrvConfig.Assets[1].AdamID, Platform: fleet.MacOSPlatform}])
// Associate an app with a team with no team members
s.Do("POST", batchURL, batchAssociateAppStoreAppsRequest{Apps: []fleet.VPPBatchPayload{{AppStoreID: s.appleVPPConfigSrvConfig.Assets[0].AdamID}}}, http.StatusNoContent, "team_name", tmEmpty.Name)
@@ -11724,19 +11741,19 @@ func (s *integrationMDMTestSuite) TestSetupExperience() {
"GET", "/api/latest/fleet/software/titles",
listSoftwareTitlesRequest{},
http.StatusOK, &respListTitles,
"team_id", strconv.Itoa(int(team1.ID)),
"team_id", fmt.Sprint(team1.ID),
)
require.Len(t, respListTitles.SoftwareTitles, 2)
var respGetSetupExperience getSetupExperienceSoftwareResponse
s.DoJSON("GET", "/api/latest/fleet/setup_experience/software", getSetupExperienceSoftwareRequest{}, http.StatusOK, &respGetSetupExperience, "team_id", strconv.Itoa(int(team1.ID)))
s.DoJSON("GET", "/api/latest/fleet/setup_experience/software", getSetupExperienceSoftwareRequest{}, http.StatusOK, &respGetSetupExperience, "team_id", fmt.Sprint(team1.ID))
require.Len(t, respGetSetupExperience.SoftwareTitles, 2)
var respPutSetupExperience putSetupExperienceSoftwareResponse
s.DoJSON("PUT", "/api/latest/fleet/setup_experience/software", putSetupExperienceSoftwareRequest{TeamID: team1.ID, TitleIDs: []uint{respListTitles.SoftwareTitles[0].ID, respListTitles.SoftwareTitles[1].ID}}, http.StatusOK, &respPutSetupExperience)
require.Nil(t, respPutSetupExperience.error())
s.DoJSON("GET", "/api/latest/fleet/setup_experience/software", getSetupExperienceSoftwareRequest{}, http.StatusOK, &respGetSetupExperience, "team_id", strconv.Itoa(int(team1.ID)))
s.DoJSON("GET", "/api/latest/fleet/setup_experience/software", getSetupExperienceSoftwareRequest{}, http.StatusOK, &respGetSetupExperience, "team_id", fmt.Sprint(team1.ID))
require.Len(t, respGetSetupExperience.SoftwareTitles, 2)
desktopToken := uuid.New().String()
@@ -146,6 +146,13 @@ github.com/fleetdm/fleet/v4/server/fleet/MacOSSetup EnableReleaseDeviceManually
github.com/fleetdm/fleet/v4/pkg/optjson/Bool Set bool
github.com/fleetdm/fleet/v4/pkg/optjson/Bool Valid bool
github.com/fleetdm/fleet/v4/pkg/optjson/Bool Value bool
github.com/fleetdm/fleet/v4/server/fleet/MacOSSetup Script optjson.String
github.com/fleetdm/fleet/v4/server/fleet/MacOSSetup Software optjson.Slice[*github.com/fleetdm/fleet/v4/server/fleet.MacOSSetupSoftware]
github.com/fleetdm/fleet/v4/pkg/optjson/Slice[*github.com/fleetdm/fleet/v4/server/fleet.MacOSSetupSoftware] Set bool
github.com/fleetdm/fleet/v4/pkg/optjson/Slice[*github.com/fleetdm/fleet/v4/server/fleet.MacOSSetupSoftware] Valid bool
github.com/fleetdm/fleet/v4/pkg/optjson/Slice[*github.com/fleetdm/fleet/v4/server/fleet.MacOSSetupSoftware] Value []*fleet.MacOSSetupSoftware
github.com/fleetdm/fleet/v4/server/fleet/MacOSSetupSoftware AppStoreID string
github.com/fleetdm/fleet/v4/server/fleet/MacOSSetupSoftware PackagePath string
github.com/fleetdm/fleet/v4/server/fleet/MDM MacOSMigration fleet.MacOSMigration
github.com/fleetdm/fleet/v4/server/fleet/MacOSMigration Enable bool
github.com/fleetdm/fleet/v4/server/fleet/MacOSMigration Mode fleet.MacOSMigrationMode string
@@ -28,6 +28,13 @@ github.com/fleetdm/fleet/v4/server/fleet/MacOSSetup EnableReleaseDeviceManually
github.com/fleetdm/fleet/v4/pkg/optjson/Bool Set bool
github.com/fleetdm/fleet/v4/pkg/optjson/Bool Valid bool
github.com/fleetdm/fleet/v4/pkg/optjson/Bool Value bool
github.com/fleetdm/fleet/v4/server/fleet/MacOSSetup Script optjson.String
github.com/fleetdm/fleet/v4/server/fleet/MacOSSetup Software optjson.Slice[*github.com/fleetdm/fleet/v4/server/fleet.MacOSSetupSoftware]
github.com/fleetdm/fleet/v4/pkg/optjson/Slice[*github.com/fleetdm/fleet/v4/server/fleet.MacOSSetupSoftware] Set bool
github.com/fleetdm/fleet/v4/pkg/optjson/Slice[*github.com/fleetdm/fleet/v4/server/fleet.MacOSSetupSoftware] Valid bool
github.com/fleetdm/fleet/v4/pkg/optjson/Slice[*github.com/fleetdm/fleet/v4/server/fleet.MacOSSetupSoftware] Value []*fleet.MacOSSetupSoftware
github.com/fleetdm/fleet/v4/server/fleet/MacOSSetupSoftware AppStoreID string
github.com/fleetdm/fleet/v4/server/fleet/MacOSSetupSoftware PackagePath string
github.com/fleetdm/fleet/v4/server/fleet/TeamMDM WindowsSettings fleet.WindowsSettings
github.com/fleetdm/fleet/v4/server/fleet/WindowsSettings CustomSettings optjson.Slice[github.com/fleetdm/fleet/v4/server/fleet.MDMProfileSpec]
github.com/fleetdm/fleet/v4/pkg/optjson/Slice[github.com/fleetdm/fleet/v4/server/fleet.MDMProfileSpec] Set bool