diff --git a/changes/issue-11671-macos-migration b/changes/issue-11671-macos-migration new file mode 100644 index 0000000000..33065a7b67 --- /dev/null +++ b/changes/issue-11671-macos-migration @@ -0,0 +1 @@ +- Added `mdm.macos_migration` to app config diff --git a/cmd/fleetctl/testdata/expectedGetConfigAppConfigJson.json b/cmd/fleetctl/testdata/expectedGetConfigAppConfigJson.json index db881bedfd..34f5abeeaf 100644 --- a/cmd/fleetctl/testdata/expectedGetConfigAppConfigJson.json +++ b/cmd/fleetctl/testdata/expectedGetConfigAppConfigJson.json @@ -4,7 +4,8 @@ "spec": { "org_info": { "org_name": "", - "org_logo_url": "" + "org_logo_url": "", + "contact_url": "https://fleetdm.com/company/contact" }, "server_settings": { "server_url": "", @@ -86,6 +87,11 @@ "minimum_version": "", "deadline": "" }, + "macos_migration": { + "enable": false, + "mode": "", + "webhook_url": "" + }, "macos_settings": { "custom_settings": null, "enable_disk_encryption": false diff --git a/cmd/fleetctl/testdata/expectedGetConfigAppConfigYaml.yml b/cmd/fleetctl/testdata/expectedGetConfigAppConfigYaml.yml index 534f395c11..8d6a3598a0 100644 --- a/cmd/fleetctl/testdata/expectedGetConfigAppConfigYaml.yml +++ b/cmd/fleetctl/testdata/expectedGetConfigAppConfigYaml.yml @@ -18,6 +18,10 @@ spec: apple_bm_enabled_and_configured: false enabled_and_configured: false apple_bm_default_team: "" + macos_migration: + enable: false + mode: "" + webhook_url: "" macos_updates: minimum_version: "" deadline: "" @@ -37,6 +41,7 @@ spec: org_info: org_logo_url: "" org_name: "" + contact_url: https://fleetdm.com/company/contact server_settings: deferred_save_host: false enable_analytics: false diff --git a/cmd/fleetctl/testdata/expectedGetConfigIncludeServerConfigJson.json b/cmd/fleetctl/testdata/expectedGetConfigIncludeServerConfigJson.json index e08ffb0f1a..5a98007ce3 100644 --- a/cmd/fleetctl/testdata/expectedGetConfigIncludeServerConfigJson.json +++ b/cmd/fleetctl/testdata/expectedGetConfigIncludeServerConfigJson.json @@ -4,7 +4,8 @@ "spec": { "org_info": { "org_name": "", - "org_logo_url": "" + "org_logo_url": "", + "contact_url": "https://fleetdm.com/company/contact" }, "server_settings": { "server_url": "", @@ -44,6 +45,11 @@ "minimum_version": "", "deadline": "" }, + "macos_migration": { + "enable": false, + "mode": "", + "webhook_url": "" + }, "macos_settings": { "custom_settings": null, "enable_disk_encryption": false diff --git a/cmd/fleetctl/testdata/expectedGetConfigIncludeServerConfigYaml.yml b/cmd/fleetctl/testdata/expectedGetConfigIncludeServerConfigYaml.yml index 234ddd0899..4f076a405f 100644 --- a/cmd/fleetctl/testdata/expectedGetConfigIncludeServerConfigYaml.yml +++ b/cmd/fleetctl/testdata/expectedGetConfigIncludeServerConfigYaml.yml @@ -18,6 +18,10 @@ spec: apple_bm_enabled_and_configured: false apple_bm_terms_expired: false enabled_and_configured: false + macos_migration: + enable: false + mode: "" + webhook_url: "" macos_updates: minimum_version: "" deadline: "" @@ -76,6 +80,7 @@ spec: org_info: org_logo_url: "" org_name: "" + contact_url: https://fleetdm.com/company/contact server_settings: deferred_save_host: false enable_analytics: false diff --git a/cmd/fleetctl/testdata/macosSetupExpectedAppConfigEmpty.yml b/cmd/fleetctl/testdata/macosSetupExpectedAppConfigEmpty.yml index 3ae0550b95..f62a91a144 100644 --- a/cmd/fleetctl/testdata/macosSetupExpectedAppConfigEmpty.yml +++ b/cmd/fleetctl/testdata/macosSetupExpectedAppConfigEmpty.yml @@ -18,6 +18,10 @@ spec: apple_bm_enabled_and_configured: false apple_bm_terms_expired: false enabled_and_configured: true + macos_migration: + enable: false + mode: "" + webhook_url: "" macos_settings: custom_settings: null enable_disk_encryption: false @@ -37,6 +41,7 @@ spec: org_info: org_logo_url: "" org_name: "Fleet" + contact_url: "https://fleetdm.com/company/contact" server_settings: deferred_save_host: false enable_analytics: false diff --git a/cmd/fleetctl/testdata/macosSetupExpectedAppConfigSet.yml b/cmd/fleetctl/testdata/macosSetupExpectedAppConfigSet.yml index 2ba4e9f916..8376f881f4 100644 --- a/cmd/fleetctl/testdata/macosSetupExpectedAppConfigSet.yml +++ b/cmd/fleetctl/testdata/macosSetupExpectedAppConfigSet.yml @@ -18,6 +18,10 @@ spec: apple_bm_enabled_and_configured: false apple_bm_terms_expired: false enabled_and_configured: true + macos_migration: + enable: false + mode: "" + webhook_url: "" macos_settings: custom_settings: null enable_disk_encryption: false @@ -37,6 +41,7 @@ spec: org_info: org_logo_url: "" org_name: Fleet + contact_url: https://fleetdm.com/company/contact server_settings: deferred_save_host: false enable_analytics: false diff --git a/docs/Using-Fleet/REST-API.md b/docs/Using-Fleet/REST-API.md index 179bfc6d10..0199d3b4f9 100644 --- a/docs/Using-Fleet/REST-API.md +++ b/docs/Using-Fleet/REST-API.md @@ -782,7 +782,8 @@ None. { "org_info": { "org_name": "fleet", - "org_logo_url": "" + "org_logo_url": "", + "contact_url": "https://fleetdm.com/company/contact" }, "server_settings": { "server_url": "https://localhost:8080", @@ -841,6 +842,11 @@ None. "metadata_url": "", "idp_name": "" }, + "macos_migration": { + "enable": false, + "mode": "voluntary", + "webhook_url": "https://webhook.example.com" + }, "macos_setup": { "bootstrap_package": "", "enable_end_user_authentication": false, @@ -1020,6 +1026,9 @@ Modifies the Fleet's configuration with the supplied information. | apple_bm_default_team | string | body | _mdm settings_. The default team to use with Apple Business Manager. **Requires Fleet Premium license** | | minimum_version | string | body | _mdm.macos_updates settings_. Hosts that belong to no team and are enrolled into Fleet's MDM will be nudged until their macOS is at or above this version. **Requires Fleet Premium license** | | deadline | string | body | _mdm.macos_updates settings_. Hosts that belong to no team and are enrolled into Fleet's MDM won't be able to dismiss the Nudge window once this deadline is past. **Requires Fleet Premium license** | +| enable | boolean | body | _mdm.macos_migration settings_. Whether to enable the end user migration workflow for devices migrating from your old MDM solution. **Requires Fleet Premium license** | +| mode | string | body | _mdm.macos_migration settings_. The end user migration workflow mode for devices migrating from your old MDM solution. Options are `"voluntary"` or `"forced"`. **Requires Fleet Premium license** | +| webhook_url | string | body | _mdm.macos_migration settings_. The webhook url configured to receive requests to unenroll devices migrating from your old MDM solution. **Requires Fleet Premium license** | | custom_settings | list | body | _mdm.macos_settings settings_. Hosts that belong to no team and are enrolled into Fleet's MDM will have those custom profiles applied. | | enable_disk_encryption | boolean | body | _mdm.macos_settings settings_. Hosts that belong to no team and are enrolled into Fleet's MDM will have disk encryption enabled if set to true. **Requires Fleet Premium license** | | enable_end_user_authentication | boolean | body | _mdm.macos_setup settings_. If set to true, end user authentication will be required during automatic MDM enrollment of new macOS devices. Settings for your IdP provider must also be [configured](https://fleetdm.com/docs/using-fleet/mdm-macos-setup#end-user-authentication). **Requires Fleet Premium license** | @@ -1055,7 +1064,8 @@ Modifies the Fleet's configuration with the supplied information. { "org_info": { "org_name": "Fleet Device Management", - "org_logo_url": "https://fleetdm.com/logo.png" + "org_logo_url": "https://fleetdm.com/logo.png", + "contact_url": "https://fleetdm.com/company/contact" }, "server_settings": { "server_url": "https://localhost:8080", @@ -1116,6 +1126,11 @@ Modifies the Fleet's configuration with the supplied information. "metadata_url": "", "idp_name": "" }, + "macos_migration": { + "enable": false, + "mode": "voluntary", + "webhook_url": "https://webhook.example.com" + }, "macos_setup": { "bootstrap_package": "", "enable_end_user_authentication": false, diff --git a/server/datastore/mysql/schema.sql b/server/datastore/mysql/schema.sql index aba4086532..8427a88f27 100644 --- a/server/datastore/mysql/schema.sql +++ b/server/datastore/mysql/schema.sql @@ -39,7 +39,7 @@ CREATE TABLE `app_config_json` ( UNIQUE KEY `id` (`id`) ) 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\": {\"macos_setup\": {\"bootstrap_package\": null, \"macos_setup_assistant\": null, \"enable_end_user_authentication\": false}, \"macos_updates\": {\"deadline\": \"\", \"minimum_version\": \"\"}, \"macos_settings\": {\"custom_settings\": null, \"enable_disk_encryption\": false}, \"apple_bm_default_team\": \"\", \"apple_bm_terms_expired\": false, \"enabled_and_configured\": false, \"end_user_authentication\": {\"idp_name\": \"\", \"metadata\": \"\", \"entity_id\": \"\", \"issuer_uri\": \"\", \"metadata_url\": \"\"}, \"apple_bm_enabled_and_configured\": false}, \"features\": {\"enable_host_users\": true, \"enable_software_inventory\": false}, \"org_info\": {\"org_name\": \"\", \"org_logo_url\": \"\"}, \"integrations\": {\"jira\": null, \"zendesk\": 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, \"deferred_save_host\": false, \"live_query_disabled\": false}, \"webhook_settings\": {\"interval\": \"0s\", \"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\": \"\"}}','2020-01-01 01:01:01','2020-01-01 01:01:01'); +INSERT INTO `app_config_json` VALUES (1,'{\"mdm\": {\"macos_setup\": {\"bootstrap_package\": null, \"macos_setup_assistant\": null, \"enable_end_user_authentication\": false}, \"macos_updates\": {\"deadline\": \"\", \"minimum_version\": \"\"}, \"macos_settings\": {\"custom_settings\": null, \"enable_disk_encryption\": false}, \"macos_migration\": {\"mode\": \"\", \"enable\": false, \"webhook_url\": \"\"}, \"apple_bm_default_team\": \"\", \"apple_bm_terms_expired\": false, \"enabled_and_configured\": false, \"end_user_authentication\": {\"idp_name\": \"\", \"metadata\": \"\", \"entity_id\": \"\", \"issuer_uri\": \"\", \"metadata_url\": \"\"}, \"apple_bm_enabled_and_configured\": false}, \"features\": {\"enable_host_users\": true, \"enable_software_inventory\": false}, \"org_info\": {\"org_name\": \"\", \"contact_url\": \"\", \"org_logo_url\": \"\"}, \"integrations\": {\"jira\": null, \"zendesk\": 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, \"deferred_save_host\": false, \"live_query_disabled\": false}, \"webhook_settings\": {\"interval\": \"0s\", \"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\": \"\"}}','2020-01-01 01:01:01','2020-01-01 01:01:01'); /*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET character_set_client = utf8 */; CREATE TABLE `carve_blocks` ( diff --git a/server/fleet/app.go b/server/fleet/app.go index 2d8e38e79d..0caa509a59 100644 --- a/server/fleet/app.go +++ b/server/fleet/app.go @@ -142,6 +142,7 @@ type MDM struct { MacOSUpdates MacOSUpdates `json:"macos_updates"` MacOSSettings MacOSSettings `json:"macos_settings"` MacOSSetup MacOSSetup `json:"macos_setup"` + MacOSMigration MacOSMigration `json:"macos_migration"` EndUserAuthentication MDMEndUserAuthentication `json:"end_user_authentication"` ///////////////////////////////////////////////////////////////// @@ -263,6 +264,32 @@ type MacOSSetup struct { MacOSSetupAssistant optjson.String `json:"macos_setup_assistant"` } +// MacOSMigration contains settings related to the MDM migration work flow. +type MacOSMigration struct { + Enable bool `json:"enable"` + Mode MacOSMigrationMode `json:"mode"` + WebhookURL string `json:"webhook_url"` +} + +// MacOSMigrationMode defines the possible modes that can be set if a user enables the MDM migration +// work flow in Fleet. +type MacOSMigrationMode string + +const ( + MacOSMigrationModeForced MacOSMigrationMode = "forced" + MacOSMigrationModeVoluntary MacOSMigrationMode = "voluntary" +) + +// IsValid returns true if the mode is one of the valid modes. +func (s MacOSMigrationMode) IsValid() bool { + switch s { + case MacOSMigrationModeForced, MacOSMigrationModeVoluntary: + return true + default: + return false + } +} + // MDMEndUserAuthentication contains settings related to end user authentication // to gate certain MDM features (eg: enrollment) type MDMEndUserAuthentication struct { @@ -580,8 +607,13 @@ func (c *AppConfig) UnmarshalJSON(b []byte) error { type OrgInfo struct { OrgName string `json:"org_name"` OrgLogoURL string `json:"org_logo_url"` + // ContactURL is the URL displayed for users to contact support. By default, + // https://fleetdm.com/company/contact is used. + ContactURL string `json:"contact_url"` } +const DefaultOrgInfoContactURL = "https://fleetdm.com/company/contact" + // ServerSettings contains general settings about the Fleet application. type ServerSettings struct { ServerURL string `json:"server_url"` diff --git a/server/fleet/app_test.go b/server/fleet/app_test.go index a3805d1ab6..07bf8b647f 100644 --- a/server/fleet/app_test.go +++ b/server/fleet/app_test.go @@ -118,3 +118,10 @@ func TestSSOSettingsIsEmpty(t *testing.T) { require.True(t, (SSOProviderSettings{}).IsEmpty()) require.False(t, (SSOProviderSettings{EntityID: "fleet"}).IsEmpty()) } + +func TestMacOSMigrationModeIsValid(t *testing.T) { + require.True(t, (MacOSMigrationMode("forced")).IsValid()) + require.True(t, (MacOSMigrationMode("voluntary")).IsValid()) + require.False(t, (MacOSMigrationMode("")).IsValid()) + require.False(t, (MacOSMigrationMode("foo")).IsValid()) +} diff --git a/server/service/appconfig.go b/server/service/appconfig.go index e49a5b795a..ba0d915f13 100644 --- a/server/service/appconfig.go +++ b/server/service/appconfig.go @@ -117,6 +117,10 @@ func getAppConfigEndpoint(ctx context.Context, request interface{}, svc fleet.Se } fleetDesktop := fleet.FleetDesktopSettings{TransparencyURL: transparencyURL} + if config.OrgInfo.ContactURL == "" { + config.OrgInfo.ContactURL = fleet.DefaultOrgInfoContactURL + } + features := config.Features response := appConfigResponse{ AppConfig: fleet.AppConfig{ @@ -297,6 +301,10 @@ func (svc *Service) ModifyAppConfig(ctx context.Context, p []byte, applyOpts fle invalid.Append("server_url", "Fleet server URL must be present") } + if appConfig.OrgInfo.ContactURL == "" { + appConfig.OrgInfo.ContactURL = fleet.DefaultOrgInfoContactURL + } + if newAppConfig.AgentOptions != nil { // if there were Agent Options in the new app config, then it replaced the // agent options in the resulting app config, so validate those. @@ -590,6 +598,29 @@ func (svc *Service) validateMDM( `Couldn't enable macos_setup.enable_end_user_authentication because no IdP is configured for MDM features.`) } } + + updatingMacOSMigration := mdm.MacOSMigration.Enable != oldMdm.MacOSMigration.Enable || + mdm.MacOSMigration.Mode != oldMdm.MacOSMigration.Mode || + mdm.MacOSMigration.WebhookURL != oldMdm.MacOSMigration.WebhookURL + + // MacOSMigration validation + if updatingMacOSMigration { + if mdm.MacOSMigration.Enable { + if license.Tier != fleet.TierPremium { + invalid.Append("macos_migration.enable", ErrMissingLicense.Error()) + return + } + if !mdm.MacOSMigration.Mode.IsValid() { + invalid.Append("macos_migration.mode", "mode must be one of 'voluntary' or 'forced'") + } + // TODO: improve url validation generally + if u, err := url.ParseRequestURI(mdm.MacOSMigration.WebhookURL); err != nil { + invalid.Append("macos_migration.webhook_url", err.Error()) + } else if u.Scheme != "https" { + invalid.Append("macos_migration.webhook_url", "webhook_url must be https") + } + } + } } func validateSSOProviderSettings(incoming, existing fleet.SSOProviderSettings, invalid *fleet.InvalidArgumentError) { diff --git a/server/service/integration_mdm_test.go b/server/service/integration_mdm_test.go index a3844a9515..c50fbb4053 100644 --- a/server/service/integration_mdm_test.go +++ b/server/service/integration_mdm_test.go @@ -2666,6 +2666,49 @@ func (s *integrationMDMTestSuite) TestEnqueueMDMCommand() { }, listCmdResp.Results[0]) } +func (s *integrationMDMTestSuite) TestAppConfigMDMMacOSMigration() { + t := s.T() + + checkDefaultAppConfig := func() { + var ac appConfigResponse + s.DoJSON("GET", "/api/v1/fleet/config", nil, http.StatusOK, &ac) + require.False(t, ac.MDM.MacOSMigration.Enable) + require.Empty(t, ac.MDM.MacOSMigration.Mode) + require.Empty(t, ac.MDM.MacOSMigration.WebhookURL) + } + checkDefaultAppConfig() + + var acResp appConfigResponse + // missing webhook_url + s.DoJSON("PATCH", "/api/v1/fleet/config", json.RawMessage(`{ + "mdm": { "macos_migration": { "enable": true, "mode": "voluntary", "webhook_url": "" } } + }`), http.StatusUnprocessableEntity, &acResp) + checkDefaultAppConfig() + + // invalid url scheme for webhook_url + s.DoJSON("PATCH", "/api/v1/fleet/config", json.RawMessage(`{ + "mdm": { "macos_migration": { "enable": true, "mode": "voluntary", "webhook_url": "http://example.com" } } + }`), http.StatusUnprocessableEntity, &acResp) + checkDefaultAppConfig() + + // invalid mode + s.DoJSON("PATCH", "/api/v1/fleet/config", json.RawMessage(`{ + "mdm": { "macos_migration": { "enable": true, "mode": "foobar", "webhook_url": "https://example.com" } } + }`), http.StatusUnprocessableEntity, &acResp) + checkDefaultAppConfig() + + // valid request + s.DoJSON("PATCH", "/api/v1/fleet/config", json.RawMessage(`{ + "mdm": { "macos_migration": { "enable": true, "mode": "voluntary", "webhook_url": "https://example.com" } } + }`), http.StatusOK, &acResp) + + // confirm new app config + s.DoJSON("GET", "/api/v1/fleet/config", nil, http.StatusOK, &acResp) + require.True(t, acResp.MDM.MacOSMigration.Enable) + require.Equal(t, fleet.MacOSMigrationModeVoluntary, acResp.MDM.MacOSMigration.Mode) + require.Equal(t, "https://example.com", acResp.MDM.MacOSMigration.WebhookURL) +} + func (s *integrationMDMTestSuite) TestBootstrapPackage() { t := s.T()