diff --git a/changes/22385-cli-gitops-macos-setup-software-and-script b/changes/22385-cli-gitops-macos-setup-software-and-script new file mode 100644 index 000000000000..09ea4359f317 --- /dev/null +++ b/changes/22385-cli-gitops-macos-setup-software-and-script @@ -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. diff --git a/cmd/fleetctl/apply.go b/cmd/fleetctl/apply.go index 4e29f15b18b6..237c46ab0b2f 100644 --- a/cmd/fleetctl/apply.go +++ b/cmd/fleetctl/apply.go @@ -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 } diff --git a/cmd/fleetctl/gitops_test.go b/cmd/fleetctl/gitops_test.go index 22f34287b0b9..cb396e10472d 100644 --- a/cmd/fleetctl/gitops_test.go +++ b/cmd/fleetctl/gitops_test.go @@ -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) diff --git a/cmd/fleetctl/preview.go b/cmd/fleetctl/preview.go index b009c0ca702b..cf5b28039f7c 100644 --- a/cmd/fleetctl/preview.go +++ b/cmd/fleetctl/preview.go @@ -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 } diff --git a/cmd/fleetctl/testdata/expectedGetConfigAppConfigJson.json b/cmd/fleetctl/testdata/expectedGetConfigAppConfigJson.json index ce6017b7da88..c3779641775a 100644 --- a/cmd/fleetctl/testdata/expectedGetConfigAppConfigJson.json +++ b/cmd/fleetctl/testdata/expectedGetConfigAppConfigJson.json @@ -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 diff --git a/cmd/fleetctl/testdata/expectedGetConfigAppConfigYaml.yml b/cmd/fleetctl/testdata/expectedGetConfigAppConfigYaml.yml index 3637ccc5d74c..ff6fbaa22eae 100644 --- a/cmd/fleetctl/testdata/expectedGetConfigAppConfigYaml.yml +++ b/cmd/fleetctl/testdata/expectedGetConfigAppConfigYaml.yml @@ -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: diff --git a/cmd/fleetctl/testdata/expectedGetConfigIncludeServerConfigJson.json b/cmd/fleetctl/testdata/expectedGetConfigIncludeServerConfigJson.json index 598e99c53e58..dea76a995b16 100644 --- a/cmd/fleetctl/testdata/expectedGetConfigIncludeServerConfigJson.json +++ b/cmd/fleetctl/testdata/expectedGetConfigIncludeServerConfigJson.json @@ -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 diff --git a/cmd/fleetctl/testdata/expectedGetConfigIncludeServerConfigYaml.yml b/cmd/fleetctl/testdata/expectedGetConfigIncludeServerConfigYaml.yml index 56d2d13eac93..f6b8136407cd 100644 --- a/cmd/fleetctl/testdata/expectedGetConfigIncludeServerConfigYaml.yml +++ b/cmd/fleetctl/testdata/expectedGetConfigIncludeServerConfigYaml.yml @@ -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: diff --git a/cmd/fleetctl/testdata/expectedGetTeamsJson.json b/cmd/fleetctl/testdata/expectedGetTeamsJson.json index de6669ac1259..2e38e6b7e7a8 100644 --- a/cmd/fleetctl/testdata/expectedGetTeamsJson.json +++ b/cmd/fleetctl/testdata/expectedGetTeamsJson.json @@ -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 diff --git a/cmd/fleetctl/testdata/expectedGetTeamsYaml.yml b/cmd/fleetctl/testdata/expectedGetTeamsYaml.yml index 422456f5b54e..fe37b4161566 100644 --- a/cmd/fleetctl/testdata/expectedGetTeamsYaml.yml +++ b/cmd/fleetctl/testdata/expectedGetTeamsYaml.yml @@ -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 diff --git a/cmd/fleetctl/testdata/macosSetupExpectedAppConfigEmpty.yml b/cmd/fleetctl/testdata/macosSetupExpectedAppConfigEmpty.yml index a158f27c1b52..49c129df695b 100644 --- a/cmd/fleetctl/testdata/macosSetupExpectedAppConfigEmpty.yml +++ b/cmd/fleetctl/testdata/macosSetupExpectedAppConfigEmpty.yml @@ -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 diff --git a/cmd/fleetctl/testdata/macosSetupExpectedAppConfigSet.yml b/cmd/fleetctl/testdata/macosSetupExpectedAppConfigSet.yml index 4d9ffe1f50a0..27e6a2a5459e 100644 --- a/cmd/fleetctl/testdata/macosSetupExpectedAppConfigSet.yml +++ b/cmd/fleetctl/testdata/macosSetupExpectedAppConfigSet.yml @@ -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 diff --git a/cmd/fleetctl/testdata/macosSetupExpectedTeam1And2Empty.yml b/cmd/fleetctl/testdata/macosSetupExpectedTeam1And2Empty.yml index f0aa275b56b3..7c8d9bb4865d 100644 --- a/cmd/fleetctl/testdata/macosSetupExpectedTeam1And2Empty.yml +++ b/cmd/fleetctl/testdata/macosSetupExpectedTeam1And2Empty.yml @@ -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 diff --git a/cmd/fleetctl/testdata/macosSetupExpectedTeam1And2Set.yml b/cmd/fleetctl/testdata/macosSetupExpectedTeam1And2Set.yml index 1d2b07674017..45203bc28362 100644 --- a/cmd/fleetctl/testdata/macosSetupExpectedTeam1And2Set.yml +++ b/cmd/fleetctl/testdata/macosSetupExpectedTeam1And2Set.yml @@ -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 diff --git a/cmd/fleetctl/testdata/macosSetupExpectedTeam1Empty.yml b/cmd/fleetctl/testdata/macosSetupExpectedTeam1Empty.yml index 885d25482758..5e92912a19f1 100644 --- a/cmd/fleetctl/testdata/macosSetupExpectedTeam1Empty.yml +++ b/cmd/fleetctl/testdata/macosSetupExpectedTeam1Empty.yml @@ -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 diff --git a/cmd/fleetctl/testdata/macosSetupExpectedTeam1Set.yml b/cmd/fleetctl/testdata/macosSetupExpectedTeam1Set.yml index 76c45b10b2f2..2c9da92d8fe8 100644 --- a/cmd/fleetctl/testdata/macosSetupExpectedTeam1Set.yml +++ b/cmd/fleetctl/testdata/macosSetupExpectedTeam1Set.yml @@ -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 diff --git a/ee/server/service/software_installers.go b/ee/server/service/software_installers.go index f06260e3043f..045fa48ccdbf 100644 --- a/ee/server/service/software_installers.go +++ b/ee/server/service/software_installers.go @@ -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 diff --git a/ee/server/service/teams.go b/ee/server/service/teams.go index 448abc367401..0952b972f336 100644 --- a/ee/server/service/teams.go +++ b/ee/server/service/teams.go @@ -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, diff --git a/ee/server/service/vpp.go b/ee/server/service/vpp.go index d1f13bd55589..dc1c1c461e74 100644 --- a/ee/server/service/vpp.go +++ b/ee/server/service/vpp.go @@ -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, diff --git a/pkg/spec/gitops.go b/pkg/spec/gitops.go index f7aefa97d192..7ce55900f9d4 100644 --- a/pkg/spec/gitops.go +++ b/pkg/spec/gitops.go @@ -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 diff --git a/server/datastore/mysql/schema.sql b/server/datastore/mysql/schema.sql index a26d3a46915a..0c89a8961d65 100644 --- a/server/datastore/mysql/schema.sql +++ b/server/datastore/mysql/schema.sql @@ -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` ( diff --git a/server/datastore/mysql/setup_experience.go b/server/datastore/mysql/setup_experience.go index 6628ae00a51a..22b3857efb9e 100644 --- a/server/datastore/mysql/setup_experience.go +++ b/server/datastore/mysql/setup_experience.go @@ -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 }) diff --git a/server/datastore/mysql/setup_experience_test.go b/server/datastore/mysql/setup_experience_test.go index baf94153d00e..6ae5af22498f 100644 --- a/server/datastore/mysql/setup_experience_test.go +++ b/server/datastore/mysql/setup_experience_test.go @@ -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 }) } diff --git a/server/datastore/mysql/software_installers.go b/server/datastore/mysql/software_installers.go index 95717e0638a3..04069f9bf780 100644 --- a/server/datastore/mysql/software_installers.go +++ b/server/datastore/mysql/software_installers.go @@ -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 diff --git a/server/datastore/mysql/software_installers_test.go b/server/datastore/mysql/software_installers_test.go index 873e3fea5c46..d33a7941c10e 100644 --- a/server/datastore/mysql/software_installers_test.go +++ b/server/datastore/mysql/software_installers_test.go @@ -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{ diff --git a/server/datastore/mysql/teams_test.go b/server/datastore/mysql/teams_test.go index 175d49f8eed7..df4b7bc2d782 100644 --- a/server/datastore/mysql/teams_test.go +++ b/server/datastore/mysql/teams_test.go @@ -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"}}), diff --git a/server/datastore/mysql/vpp.go b/server/datastore/mysql/vpp.go index 89bd91cade81..03a4c78706c5 100644 --- a/server/datastore/mysql/vpp.go +++ b/server/datastore/mysql/vpp.go @@ -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), diff --git a/server/datastore/mysql/vpp_test.go b/server/datastore/mysql/vpp_test.go index 5e355f8d5827..a2f9ca9b5dbf 100644 --- a/server/datastore/mysql/vpp_test.go +++ b/server/datastore/mysql/vpp_test.go @@ -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 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 now works + // Remove all apps err = ds.SetTeamVPPApps(ctx, &team.ID, []fleet.VPPAppTeam{}) require.NoError(t, err) diff --git a/server/fleet/app.go b/server/fleet/app.go index f0810e352df6..483dd5fe796a 100644 --- a/server/fleet/app.go +++ b/server/fleet/app.go @@ -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 } diff --git a/server/fleet/scripts.go b/server/fleet/scripts.go index 1dcbb343e9bc..28bba6ad8cf7 100644 --- a/server/fleet/scripts.go +++ b/server/fleet/scripts.go @@ -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 { diff --git a/server/fleet/software.go b/server/fleet/software.go index 1b9eecc813c7..0f9eeab4f21f 100644 --- a/server/fleet/software.go +++ b/server/fleet/software.go @@ -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 } diff --git a/server/fleet/software_installer.go b/server/fleet/software_installer.go index 944ec125de58..d715e1ddc5cf 100644 --- a/server/fleet/software_installer.go +++ b/server/fleet/software_installer.go @@ -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 { diff --git a/server/fleet/teams.go b/server/fleet/teams.go index 68cde3072dcc..4ee8b53aad65 100644 --- a/server/fleet/teams.go +++ b/server/fleet/teams.go @@ -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 } diff --git a/server/fleet/vpp.go b/server/fleet/vpp.go index c746844a4dbc..a5ecc83ca6ef 100644 --- a/server/fleet/vpp.go +++ b/server/fleet/vpp.go @@ -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, diff --git a/server/service/appconfig_test.go b/server/service/appconfig_test.go index 44d81c756671..87081047c61e 100644 --- a/server/service/appconfig_test.go +++ b/server/service/appconfig_test.go @@ -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}}, diff --git a/server/service/client.go b/server/service/client.go index 247ba1b93546..52223b60e70e 100644 --- a/server/service/client.go +++ b/server/service/client.go @@ -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 don’t 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}) + } + + // 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 software installers: %w", err) + return nil, fmt.Errorf("applying no team macos_setup.script: %w", err) } + macosSetupScript = &fileContent{Filename: filepath.Base(macOSSetup.Script.Value), Content: b} + } - if dryRun { - logFn("[+] would've applied 'No Team' software packages\n") - } else { - logFn("[+] applied 'No Team' software packages\n") + 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 } diff --git a/server/service/client_scripts.go b/server/service/client_scripts.go index 7a47c162e908..9761fe24b214 100644 --- a/server/service/client_scripts.go +++ b/server/service/client_scripts.go @@ -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 +} diff --git a/server/service/integration_enterprise_test.go b/server/service/integration_enterprise_test.go index d64896ab45c9..a8106dd270bf 100644 --- a/server/service/integration_enterprise_test.go +++ b/server/service/integration_enterprise_test.go @@ -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{}}, diff --git a/server/service/integration_mdm_dep_test.go b/server/service/integration_mdm_dep_test.go index b00cd8cd19fd..34a0b3ac934c 100644 --- a/server/service/integration_mdm_dep_test.go +++ b/server/service/integration_mdm_dep_test.go @@ -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), "I1") { + switch { + case strings.Contains(string(cmd.Command.InstallProfile.Payload), "I1"): profileCustomSeen = true - } else if strings.Contains(string(cmd.Command.InstallProfile.Payload), fmt.Sprintf("%s", mobileconfig.FleetdConfigPayloadIdentifier)) { + case strings.Contains(string(cmd.Command.InstallProfile.Payload), fmt.Sprintf("%s", mobileconfig.FleetdConfigPayloadIdentifier)): profileFleetdSeen = true - } else if strings.Contains(string(cmd.Command.InstallProfile.Payload), fmt.Sprintf("%s", mobileconfig.FleetCARootConfigPayloadIdentifier)) { + case strings.Contains(string(cmd.Command.InstallProfile.Payload), fmt.Sprintf("%s", mobileconfig.FleetCARootConfigPayloadIdentifier)): profileFleetCASeen = true - } else if strings.Contains(string(cmd.Command.InstallProfile.Payload), fmt.Sprintf("%s%s>>> 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), "I1") { + switch { + case strings.Contains(string(cmd.Command.InstallProfile.Payload), "I1"): profileCustomSeen = true - } else if strings.Contains(string(cmd.Command.InstallProfile.Payload), fmt.Sprintf("%s", mobileconfig.FleetdConfigPayloadIdentifier)) { + case strings.Contains(string(cmd.Command.InstallProfile.Payload), fmt.Sprintf("%s", mobileconfig.FleetdConfigPayloadIdentifier)): profileFleetdSeen = true - } else if strings.Contains(string(cmd.Command.InstallProfile.Payload), fmt.Sprintf("%s", mobileconfig.FleetCARootConfigPayloadIdentifier)) { + case strings.Contains(string(cmd.Command.InstallProfile.Payload), fmt.Sprintf("%s", mobileconfig.FleetCARootConfigPayloadIdentifier)): profileFleetCASeen = true - } else if strings.Contains(string(cmd.Command.InstallProfile.Payload), fmt.Sprintf("%s%s