diff --git a/changes/9345-macos-updates-config b/changes/9345-macos-updates-config new file mode 100644 index 0000000000..d8a2b6835e --- /dev/null +++ b/changes/9345-macos-updates-config @@ -0,0 +1 @@ +- Allow to configure a minimum macOS version and a deadline for hosts enrolled into Fleet's MDM. diff --git a/cmd/fleetctl/apply_test.go b/cmd/fleetctl/apply_test.go index 261d4aa558..833faa24d7 100644 --- a/cmd/fleetctl/apply_test.go +++ b/cmd/fleetctl/apply_test.go @@ -5,7 +5,6 @@ import ( "database/sql" "encoding/json" "errors" - "io/ioutil" "os" "testing" "time" @@ -76,7 +75,7 @@ func TestApplyUserRoles(t *testing.T) { return nil } - tmpFile, err := ioutil.TempFile(os.TempDir(), "*.yml") + tmpFile, err := os.CreateTemp(os.TempDir(), "*.yml") require.NoError(t, err) defer os.Remove(tmpFile.Name()) @@ -168,13 +167,25 @@ spec: name: team1 secrets: - secret: AAA + mdm: + macos_updates: + minimum_version: 12.3.1 + deadline: 2011-03-01 `) newAgentOpts := json.RawMessage(`{"config":{"views":{"foo":"bar"}}}`) + newMDMSettings := fleet.TeamMDM{ + MacOSUpdates: fleet.MacOSUpdates{ + MinimumVersion: "12.3.1", + Deadline: "2011-03-01", + }, + } require.Equal(t, "[+] applied 2 teams\n", runAppForTest(t, []string{"apply", "-f", filename})) assert.JSONEq(t, string(agentOpts), string(*teamsByName["team2"].Config.AgentOptions)) assert.JSONEq(t, string(newAgentOpts), string(*teamsByName["team1"].Config.AgentOptions)) assert.Equal(t, []*fleet.EnrollSecret{{Secret: "AAA"}}, enrolledSecretsCalled[uint(42)]) + assert.Equal(t, fleet.TeamMDM{}, teamsByName["team2"].Config.MDM) + assert.Equal(t, newMDMSettings, teamsByName["team1"].Config.MDM) assert.True(t, ds.ApplyEnrollSecretsFuncInvoked) ds.ApplyEnrollSecretsFuncInvoked = false @@ -191,6 +202,7 @@ spec: assert.False(t, ds.ApplyEnrollSecretsFuncInvoked) // agent options not provided, so left unchanged assert.JSONEq(t, string(newAgentOpts), string(*teamsByName["team1"].Config.AgentOptions)) + assert.Equal(t, fleet.TeamMDM{}, teamsByName["team1"].Config.MDM) filename = writeTmpYml(t, ` apiVersion: v1 @@ -202,13 +214,24 @@ spec: views: foo: qux name: team1 + mdm: + macos_updates: + minimum_version: 10.10.10 + deadline: 1992-03-01 secrets: - secret: BBB `) + newMDMSettings = fleet.TeamMDM{ + MacOSUpdates: fleet.MacOSUpdates{ + MinimumVersion: "10.10.10", + Deadline: "1992-03-01", + }, + } newAgentOpts = json.RawMessage(`{"config":{"views":{"foo":"qux"}}}`) require.Equal(t, "[+] applied 1 teams\n", runAppForTest(t, []string{"apply", "-f", filename})) assert.JSONEq(t, string(newAgentOpts), string(*teamsByName["team1"].Config.AgentOptions)) + assert.Equal(t, newMDMSettings, teamsByName["team1"].Config.MDM) assert.Equal(t, []*fleet.EnrollSecret{{Secret: "BBB"}}, enrolledSecretsCalled[uint(42)]) assert.True(t, ds.ApplyEnrollSecretsFuncInvoked) @@ -227,7 +250,7 @@ spec: } func writeTmpYml(t *testing.T, contents string) string { - tmpFile, err := ioutil.TempFile(t.TempDir(), "*.yml") + tmpFile, err := os.CreateTemp(t.TempDir(), "*.yml") require.NoError(t, err) _, err = tmpFile.WriteString(contents) require.NoError(t, err) @@ -235,7 +258,8 @@ func writeTmpYml(t *testing.T, contents string) string { } func TestApplyAppConfig(t *testing.T) { - _, ds := runServerWithMockedDS(t) + license := &fleet.LicenseInfo{Tier: fleet.TierPremium, Expiration: time.Now().Add(24 * time.Hour)} + _, ds := runServerWithMockedDS(t, &service.TestServerOpts{License: license}) ds.ListUsersFunc = func(ctx context.Context, opt fleet.UserListOptions) ([]*fleet.User, error) { return userRoleSpecList, nil @@ -251,6 +275,9 @@ func TestApplyAppConfig(t *testing.T) { } return userRoleSpecList[1], nil } + ds.TeamByNameFunc = func(ctx context.Context, name string) (*fleet.Team, error) { + return &fleet.Team{ID: 123}, nil + } defaultAgentOpts := json.RawMessage(`{"config":{"foo":"bar"}}`) ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) { @@ -274,12 +301,26 @@ spec: features: enable_host_users: false enable_software_inventory: false + mdm: + apple_bm_default_team: "team1" + macos_updates: + minimum_version: 12.1.1 + deadline: 2011-02-01 `) + newMDMSettings := fleet.MDM{ + AppleBMDefaultTeam: "team1", + AppleBMTermsExpired: false, + MacOSUpdates: fleet.MacOSUpdates{ + MinimumVersion: "12.1.1", + Deadline: "2011-02-01", + }, + } assert.Equal(t, "[+] applied fleet config\n", runAppForTest(t, []string{"apply", "-f", name})) require.NotNil(t, savedAppConfig) assert.False(t, savedAppConfig.Features.EnableHostUsers) assert.False(t, savedAppConfig.Features.EnableSoftwareInventory) + assert.Equal(t, newMDMSettings, savedAppConfig.MDM) // agent options were not modified, since they were not provided assert.Equal(t, string(defaultAgentOpts), string(*savedAppConfig.AgentOptions)) @@ -291,6 +332,8 @@ spec: enable_host_users: true enable_software_inventory: true agent_options: + mdm: + macos_updates: `) assert.Equal(t, "[+] applied fleet config\n", runAppForTest(t, []string{"apply", "-f", name})) @@ -299,6 +342,7 @@ spec: assert.True(t, savedAppConfig.Features.EnableSoftwareInventory) // agent options were cleared, provided but empty assert.Nil(t, savedAppConfig.AgentOptions) + assert.Equal(t, newMDMSettings, savedAppConfig.MDM) } func TestApplyAppConfigDryRunIssue(t *testing.T) { @@ -1213,6 +1257,94 @@ spec: [+] would've applied fleet config [+] would've applied 1 teams`, }, + { + desc: "macos_updates deadline set but minimum_version empty", + spec: ` +apiVersion: v1 +kind: team +spec: + team: + name: team1 + mdm: + macos_updates: + deadline: 2022-01-04 +`, + wantErr: `422 Validation Failed: minimum_version is required when deadline is provided`, + }, + { + desc: "macos_updates minimum_version set but deadline empty", + spec: ` +apiVersion: v1 +kind: team +spec: + team: + name: team1 + mdm: + macos_updates: + minimum_version: "12.2" +`, + wantErr: `422 Validation Failed: deadline is required when minimum_version is provided`, + }, + { + desc: "macos_updates.minimum_version with build version", + spec: ` +apiVersion: v1 +kind: team +spec: + team: + name: team1 + mdm: + macos_updates: + minimum_version: "12.2 (ABCD)" + deadline: 1892-01-01 +`, + wantErr: `422 Validation Failed: minimum_version accepts version numbers only. (E.g., "13.0.1.") NOT "Ventura 13" or "13.0.1 (22A400)"`, + }, + { + desc: "macos_updates.deadline with timestamp", + spec: ` +apiVersion: v1 +kind: team +spec: + team: + name: team1 + mdm: + macos_updates: + minimum_version: "12.2" + deadline: "1892-01-01T00:00:00Z" +`, + wantErr: `422 Validation Failed: deadline accepts YYYY-MM-DD format only (E.g., "2023-06-01.")`, + }, + { + desc: "macos_updates.deadline with invalid date", + spec: ` +apiVersion: v1 +kind: team +spec: + team: + name: team1 + mdm: + macos_updates: + minimum_version: "12.2" + deadline: "18-01-01" +`, + wantErr: `422 Validation Failed: deadline accepts YYYY-MM-DD format only (E.g., "2023-06-01.")`, + }, + { + desc: "macos_updates.deadline with incomplete date", + spec: ` +apiVersion: v1 +kind: team +spec: + team: + name: team1 + mdm: + macos_updates: + minimum_version: "12.2" + deadline: "2022-01" +`, + wantErr: `422 Validation Failed: deadline accepts YYYY-MM-DD format only (E.g., "2023-06-01.")`, + }, { desc: "missing required sso entity_id", spec: ` @@ -1340,6 +1472,82 @@ spec: `, wantOutput: `[+] applied fleet config`, }, + { + desc: "app config macos_updates deadline set but minimum_version empty", + spec: ` +apiVersion: v1 +kind: config +spec: + mdm: + macos_updates: + deadline: 2022-01-04 +`, + wantErr: `422 Validation Failed: minimum_version is required when deadline is provided`, + }, + { + desc: "app config macos_updates minimum_version set but deadline empty", + spec: ` +apiVersion: v1 +kind: config +spec: + mdm: + macos_updates: + minimum_version: "12.2" +`, + wantErr: `422 Validation Failed: deadline is required when minimum_version is provided`, + }, + { + desc: "app config macos_updates.minimum_version with build version", + spec: ` +apiVersion: v1 +kind: config +spec: + mdm: + macos_updates: + minimum_version: "12.2 (ABCD)" + deadline: 1892-01-01 +`, + wantErr: `422 Validation Failed: minimum_version accepts version numbers only. (E.g., "13.0.1.") NOT "Ventura 13" or "13.0.1 (22A400)"`, + }, + { + desc: "app config macos_updates.deadline with timestamp", + spec: ` +apiVersion: v1 +kind: config +spec: + mdm: + macos_updates: + minimum_version: "12.2" + deadline: "1892-01-01T00:00:00Z" +`, + wantErr: `422 Validation Failed: deadline accepts YYYY-MM-DD format only (E.g., "2023-06-01.")`, + }, + { + desc: "app config macos_updates.deadline with invalid date", + spec: ` +apiVersion: v1 +kind: config +spec: + mdm: + macos_updates: + minimum_version: "12.2" + deadline: "18-01-01" +`, + wantErr: `422 Validation Failed: deadline accepts YYYY-MM-DD format only (E.g., "2023-06-01.")`, + }, + { + desc: "app config macos_updates.deadline with incomplete date", + spec: ` +apiVersion: v1 +kind: config +spec: + mdm: + macos_updates: + minimum_version: "12.2" + deadline: "2022-01" +`, + wantErr: `422 Validation Failed: deadline accepts YYYY-MM-DD format only (E.g., "2023-06-01.")`, + }, } // NOTE: Integrations required fields are not tested (Jira/Zendesk) because // they require a complex setup to mock the client that would communicate diff --git a/cmd/fleetctl/get_test.go b/cmd/fleetctl/get_test.go index 5ef837e1fe..10663062c8 100644 --- a/cmd/fleetctl/get_test.go +++ b/cmd/fleetctl/get_test.go @@ -148,6 +148,12 @@ func TestGetTeams(t *testing.T) { Features: fleet.Features{ AdditionalQueries: &additionalQueries, }, + MDM: fleet.TeamMDM{ + MacOSUpdates: fleet.MacOSUpdates{ + MinimumVersion: "12.3.1", + Deadline: "2021-12-14", + }, + }, }, }, }, nil @@ -176,6 +182,10 @@ spec: integrations: jira: null zendesk: null + mdm: + macos_updates: + minimum_version: "" + deadline: "" name: team1 user_count: 99 webhook_settings: @@ -208,6 +218,10 @@ spec: integrations: jira: null zendesk: null + mdm: + macos_updates: + minimum_version: "12.3.1" + deadline: "2021-12-14" name: team2 user_count: 87 webhook_settings: @@ -217,8 +231,8 @@ spec: host_batch_size: 0 policy_ids: null ` - expectedJson := `{"kind":"team","apiVersion":"v1","spec":{"team":{"id":42,"created_at":"1999-03-10T02:45:06.371Z","name":"team1","description":"team1 description","webhook_settings":{"failing_policies_webhook":{"enable_failing_policies_webhook":false,"destination_url":"","policy_ids":null,"host_batch_size":0}},"integrations":{"jira":null,"zendesk":null},"features":{"enable_host_users":true,"enable_software_inventory":true},"user_count":99,"host_count":0}}} -{"kind":"team","apiVersion":"v1","spec":{"team":{"id":43,"created_at":"1999-03-10T02:45:06.371Z","name":"team2","description":"team2 description","agent_options":{"config":{"foo":"bar"},"overrides":{"platforms":{"darwin":{"foo":"override"}}}},"webhook_settings":{"failing_policies_webhook":{"enable_failing_policies_webhook":false,"destination_url":"","policy_ids":null,"host_batch_size":0}},"integrations":{"jira":null,"zendesk":null},"features":{"enable_host_users":false,"enable_software_inventory":false,"additional_queries":{"foo":"bar"}},"user_count":87,"host_count":0}}} + expectedJson := `{"kind":"team","apiVersion":"v1","spec":{"team":{"id":42,"created_at":"1999-03-10T02:45:06.371Z","name":"team1","description":"team1 description","webhook_settings":{"failing_policies_webhook":{"enable_failing_policies_webhook":false,"destination_url":"","policy_ids":null,"host_batch_size":0}},"integrations":{"jira":null,"zendesk":null},"features":{"enable_host_users":true,"enable_software_inventory":true},"mdm":{"macos_updates":{"minimum_version":"","deadline":""}},"user_count":99,"host_count":0}}} +{"kind":"team","apiVersion":"v1","spec":{"team":{"id":43,"created_at":"1999-03-10T02:45:06.371Z","name":"team2","description":"team2 description","agent_options":{"config":{"foo":"bar"},"overrides":{"platforms":{"darwin":{"foo":"override"}}}},"webhook_settings":{"failing_policies_webhook":{"enable_failing_policies_webhook":false,"destination_url":"","policy_ids":null,"host_batch_size":0}},"integrations":{"jira":null,"zendesk":null},"features":{"enable_host_users":false,"enable_software_inventory":false,"additional_queries":{"foo":"bar"}},"mdm":{"macos_updates":{"minimum_version":"12.3.1","deadline":"2021-12-14"}},"user_count":87,"host_count":0}}} ` if tt.shouldHaveExpiredBanner { expectedJson = expiredBanner.String() + expectedJson @@ -226,8 +240,8 @@ spec: expectedText = expiredBanner.String() + expectedText } - assert.Equal(t, expectedText, runAppForTest(t, []string{"get", "teams"})) - assert.Equal(t, expectedYaml, runAppForTest(t, []string{"get", "teams", "--yaml"})) + assert.YAMLEq(t, expectedText, runAppForTest(t, []string{"get", "teams"})) + assert.YAMLEq(t, expectedYaml, runAppForTest(t, []string{"get", "teams", "--yaml"})) assert.Equal(t, expectedJson, runAppForTest(t, []string{"get", "teams", "--json"})) }) } @@ -476,6 +490,9 @@ spec: mdm: apple_bm_terms_expired: false apple_bm_default_team: "" + macos_updates: + minimum_version: "" + deadline: "" org_info: org_logo_url: "" org_name: "" @@ -562,6 +579,12 @@ spec: "enable_host_users": true, "enable_software_inventory": false }, + "mdm": { + "macos_updates": { + "minimum_version": "", + "deadline": "" + } + }, "sso_settings": { "entity_id": "", "issuer_uri": "", @@ -596,7 +619,14 @@ spec: "interval": "0s" }, "integrations": { "jira": null, "zendesk": null }, - "mdm": { "apple_bm_terms_expired": false, "apple_bm_default_team": "" } + "mdm": { + "apple_bm_terms_expired": false, + "apple_bm_default_team": "", + "macos_updates": { + "minimum_version": "", + "deadline": "" + } + } } } ` @@ -625,6 +655,9 @@ spec: mdm: apple_bm_default_team: "" apple_bm_terms_expired: false + macos_updates: + minimum_version: "" + deadline: "" license: expiration: "0001-01-01T00:00:00Z" tier: free @@ -757,6 +790,12 @@ spec: "enable_host_users": true, "enable_software_inventory": false }, + "mdm": { + "macos_updates": { + "minimum_version": "", + "deadline": "" + } + }, "sso_settings": { "enable_jit_provisioning": false, "entity_id": "", @@ -798,10 +837,14 @@ spec: "jira": null, "zendesk": null }, - "mdm": { + "mdm": { "apple_bm_default_team": "", - "apple_bm_terms_expired": false - }, + "apple_bm_terms_expired": false, + "macos_updates": { + "minimum_version": "", + "deadline": "" + } + }, "update_interval": { "osquery_detail": "1h0m0s", "osquery_policy": "1h0m0s" diff --git a/docs/Using-Fleet/REST-API.md b/docs/Using-Fleet/REST-API.md index 8be1215a00..8e8f4637db 100644 --- a/docs/Using-Fleet/REST-API.md +++ b/docs/Using-Fleet/REST-API.md @@ -838,7 +838,11 @@ None. }, "mdm": { "apple_bm_default_team": "", - "apple_bm_terms_expired": false + "apple_bm_terms_expired": false, + "macos_updates": { + "minimum_version": "12.3.1", + "deadline": "2022-01-01" + } }, "agent_options": { "spec": { @@ -914,7 +918,11 @@ None. "jira": null }, "mdm": { - "apple_bm_default_team": "" + "apple_bm_default_team": "", + "macos_updates": { + "minimum_version": "12.3.1", + "deadline": "2022-01-01" + } }, "logging": { "debug": false, @@ -1013,7 +1021,9 @@ Modifies the Fleet's configuration with the supplied information. | email | string | body | _integrations.zendesk[] settings_. The Zendesk user email to use for this Zendesk integration. | | api_token | string | body | _integrations.zendesk[] settings_. The Zendesk API token to use for this Zendesk integration. | | group_id | integer | body | _integrations.zendesk[] settings_. The Zendesk group id to use for this integration. Zendesk tickets will be created in this group. | -| apple_bm_default_team | string | body | _mdm settings_. The default team to use with Apple Business Manager. | +| 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** | | additional_queries | boolean | body | Whether or not additional queries are enabled on hosts. | | force | bool | query | Force apply the agent options even if there are validation errors. | | dry_run | bool | query | Validate the configuration and return any validation errors, but do not apply the changes. | @@ -1089,7 +1099,11 @@ Modifies the Fleet's configuration with the supplied information. }, "mdm": { "apple_bm_default_team": "", - "apple_bm_terms_expired": false + "apple_bm_terms_expired": false, + "macos_updates": { + "minimum_version": "12.3.1", + "deadline": "2022-01-01" + } }, "agent_options": { "config": { @@ -1146,7 +1160,11 @@ Modifies the Fleet's configuration with the supplied information. ] }, "mdm": { - "apple_bm_default_team": "" + "apple_bm_default_team": "", + "macos_updates": { + "minimum_version": "12.3.1", + "deadline": "2022-01-01" + } }, "logging": { "debug": false, @@ -5420,27 +5438,32 @@ _Available in Fleet Premium_ #### Parameters -| Name | Type | In | Description | -| --- | --- | --- | --- | -| id | integer | path | **Required.** The desired team's ID. | -| name | string | body | The team's name. | -| host_ids | list | body | A list of hosts that belong to the team. | -| user_ids | list | body | A list of users that are members of the team. | -| webhook_settings | object | body | Webhook settings contains for the team. | -|   failing_policies_webhook | object | body | Failing policies webhook settings. | -|     enable_failing_policies_webhook | boolean | body | Whether or not the failing policies webhook is enabled. | -|     destination_url | string | body | The URL to deliver the webhook requests to. | -|     policy_ids | array | body | List of policy IDs to enable failing policies webhook. | -|     host_batch_size | integer | body | Maximum number of hosts to batch on failing policy webhook requests. The default, 0, means no batching (all hosts failing a policy are sent on one request). | -| integrations | object | body | Integrations settings for the team. Note that integrations referenced here must already exist globally, created by a call to [Modify configuration](#modify-configuration). | -|   jira | array | body | Jira integrations configuration. | -|     url | string | body | The URL of the Jira server to use. | -|     project_key | string | body | The project key of the Jira integration to use. Jira tickets will be created in this project. | -|     enable_failing_policies | boolean | body | Whether or not that Jira integration is enabled for failing policies. Only one failing policy automation can be enabled at a given time (enable_failing_policies_webhook and enable_failing_policies). | -|   zendesk | array | body | Zendesk integrations configuration. | -|     url | string | body | The URL of the Zendesk server to use. | -|     group_id | integer | body | The Zendesk group id to use. Zendesk tickets will be created in this group. | +| Name | Type | In | Description | +| ------------------------------------------------------- | ------- | ---- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| id | integer | path | **Required.** The desired team's ID. | +| name | string | body | The team's name. | +| host_ids | list | body | A list of hosts that belong to the team. | +| user_ids | list | body | A list of users that are members of the team. | +| webhook_settings | object | body | Webhook settings contains for the team. | +|   failing_policies_webhook | object | body | Failing policies webhook settings. | +|     enable_failing_policies_webhook | boolean | body | Whether or not the failing policies webhook is enabled. | +|     destination_url | string | body | The URL to deliver the webhook requests to. | +|     policy_ids | array | body | List of policy IDs to enable failing policies webhook. | +|     host_batch_size | integer | body | Maximum number of hosts to batch on failing policy webhook requests. The default, 0, means no batching (all hosts failing a policy are sent on one request). | +| integrations | object | body | Integrations settings for the team. Note that integrations referenced here must already exist globally, created by a call to [Modify configuration](#modify-configuration). | +|   jira | array | body | Jira integrations configuration. | +|     url | string | body | The URL of the Jira server to use. | +|     project_key | string | body | The project key of the Jira integration to use. Jira tickets will be created in this project. | +|     enable_failing_policies | boolean | body | Whether or not that Jira integration is enabled for failing policies. Only one failing policy automation can be enabled at a given time (enable_failing_policies_webhook and enable_failing_policies). | +|   zendesk | array | body | Zendesk integrations configuration. | +|     url | string | body | The URL of the Zendesk server to use. | +|     group_id | integer | body | The Zendesk group id to use. Zendesk tickets will be created in this group. | |     enable_failing_policies | boolean | body | Whether or not that Zendesk integration is enabled for failing policies. Only one failing policy automation can be enabled at a given time (enable_failing_policies_webhook and enable_failing_policies). | +| mdm | object | body | MDM settings for the team. | +|   macos_updates | object | body | MacOS updates settings. | +|     minimum_version | string | body | Hosts that belong to this team and are enrolled into Fleet's MDM will be nudged until their macOS is at or above this version. | +|     deadline | string | body | Hosts that belong to this team and are enrolled into Fleet's MDM won't be able to dismiss the Nudge window once this deadline is past. | + #### Example (add users to a team) @@ -5493,7 +5516,13 @@ _Available in Fleet Premium_ "policy_ids": null, "host_batch_size": 0 } - } + }, + "mdm": { + "macos_updates": { + "minimum_version": "12.3.1", + "deadline": "2022-01-01" + } + }, } } ``` diff --git a/docs/Using-Fleet/configuration-files/README.md b/docs/Using-Fleet/configuration-files/README.md index 5099e161d2..855c7ea554 100644 --- a/docs/Using-Fleet/configuration-files/README.md +++ b/docs/Using-Fleet/configuration-files/README.md @@ -133,6 +133,10 @@ spec: secrets: - secret: RzTlxPvugG4o4O5IKS/HqEDJUmI1hwBoffff - secret: JZ/C/Z7ucq22dt/zjx2kEuDBN0iLjqfz + mdm: + macos_updates: + minimum_version: 12.3.1 + deadline: 2022-01-04 ``` ### Team settings @@ -168,6 +172,24 @@ The `secrets` section provides the list of enroll secrets that will be valid for - secret: JZ/C/Z7ucq22dt/zjx2kEuDBN0iLjqfz ``` +#### Mobile device management (MDM) settings + +> MDM features are not ready for production and are currently in development. These features are disabled by default. + +The `mdm` section of the configuration YAML lets you control MDM settings for the team in Fleet. + +The documentation for this section is identical to the [MDM settings](#mobile-device-management-mdm-settings) documentation for the organization settings, except that the YAML section where it is set must be as follows. (Note the `kind: team` key and the location of the `mdm` key under `team` must have a `name` key to identify the team to configure.) + +```yaml +apiVersion: v1 +kind: team +spec: + team: + name: Client Platform Engineering + mdm: + # the team-specific mdm options go here +``` + ## Organization settings The `config` YAML file controls Fleet's organization settings. @@ -264,6 +286,9 @@ spec: host_batch_size: 0 mdm: apple_bm_default_team: "" + macos_updates: + minimum_version: "" + deadline: "" ``` ### Settings @@ -1202,7 +1227,41 @@ Set name of default team to use with Apple Business Manager. - Config file format: ```yaml mdm: - team: "Workstations" + apple_bm_default_team: "Workstations" + ``` + +##### mdm.macos_updates + +**Applies only to Fleet Premium**. + +The following options allow to configure the behavior of Nudge for macOS hosts that belong to no team and are enrolled into Fleet's MDM. + +##### mdm.macos_updates.minimum_version + +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 `mdm.macos_updates.deadline` to be set. + +- Default value: "" +- Config file format: + ```yaml + mdm: + macos_updates: + minimum_version: "12.1.1" + ``` + +##### mdm.macos_updates.deadline + +A deadline in the form `YYYY-MM-DD`. 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 `mdm.macos_updates.minimum_version` to be set. + +- Default value: "" +- Config file format: + ```yaml + mdm: + macos_updates: + deadline: "2022-01-01" ``` #### Advanced configuration diff --git a/ee/server/service/teams.go b/ee/server/service/teams.go index 0b3e918139..cbfb42d893 100644 --- a/ee/server/service/teams.go +++ b/ee/server/service/teams.go @@ -103,6 +103,13 @@ func (svc *Service) ModifyTeam(ctx context.Context, teamID uint, payload fleet.T team.Config.WebhookSettings = *payload.WebhookSettings } + if payload.MDM != nil { + if err := payload.MDM.MacOSUpdates.Validate(); err != nil { + return nil, fleet.NewInvalidArgumentError("macos_updates", err.Error()) + } + team.Config.MDM = *payload.MDM + } + if payload.Integrations != nil { // the team integrations must reference an existing global config integration. appCfg, err := svc.ds.AppConfig(ctx) @@ -458,6 +465,9 @@ func (svc *Service) ApplyTeamSpecs(ctx context.Context, specs []*fleet.TeamSpec, if len(spec.Secrets) > fleet.MaxEnrollSecretsCount { return ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("secrets", "too many secrets"), "validate secrets") } + if err := spec.MDM.MacOSUpdates.Validate(); err != nil { + return ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("macos_updates", err.Error())) + } if applyOpts.DryRun { continue @@ -521,6 +531,7 @@ func (svc Service) createTeamFromSpec(ctx context.Context, spec *fleet.TeamSpec, Config: fleet.TeamConfig{ AgentOptions: agentOptions, Features: features, + MDM: spec.MDM, }, Secrets: secrets, }) @@ -546,6 +557,7 @@ func (svc Service) editTeamFromSpec(ctx context.Context, team *fleet.Team, spec return err } team.Config.Features = features + team.Config.MDM = spec.MDM if len(secrets) > 0 { team.Secrets = secrets diff --git a/server/datastore/mysql/schema.sql b/server/datastore/mysql/schema.sql index 8da2af9102..1c6d8fb8ae 100644 --- a/server/datastore/mysql/schema.sql +++ b/server/datastore/mysql/schema.sql @@ -38,7 +38,7 @@ CREATE TABLE `app_config_json` ( UNIQUE KEY `id` (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; /*!40101 SET character_set_client = @saved_cs_client */; -INSERT INTO `app_config_json` VALUES (1,'{\"mdm\": {\"apple_bm_default_team\": \"\", \"apple_bm_terms_expired\": 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_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_updates\": {\"deadline\": \"\", \"minimum_version\": \"\"}, \"apple_bm_default_team\": \"\", \"apple_bm_terms_expired\": 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_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 eba5ca6159..11ca728b6e 100644 --- a/server/fleet/app.go +++ b/server/fleet/app.go @@ -7,6 +7,7 @@ import ( "fmt" "io" "net/url" + "regexp" "sort" "time" @@ -111,12 +112,52 @@ type MDM struct { // API. AppleBMTermsExpired bool `json:"apple_bm_terms_expired"` + MacOSUpdates MacOSUpdates `json:"macos_updates"` + ///////////////////////////////////////////////////////////////// // WARNING: If you add to this struct make sure it's taken into // account in the AppConfig Clone implementation! ///////////////////////////////////////////////////////////////// } +// versionStringRegex is used to validate that a version string is in the x.y.z +// format only (no prerelease or build metadata). +var versionStringRegex = regexp.MustCompile(`^\d+(\.\d+)?(\.\d+)?$`) + +// MacOSUpdates is part of AppConfig and defines the macOS update settings. +type MacOSUpdates struct { + // MinimumVerssion is the required minimum operating system version. + MinimumVersion string `json:"minimum_version"` + // Deadline the required installation date for Nudge to enforce the required + // operating system version. + Deadline string `json:"deadline"` +} + +func (m MacOSUpdates) Validate() error { + // if no settings are provided it's okay to skip further validation + if m.MinimumVersion == "" && m.Deadline == "" { + return nil + } + + if m.MinimumVersion != "" && m.Deadline == "" { + return errors.New("deadline is required when minimum_version is provided") + } + + if m.Deadline != "" && m.MinimumVersion == "" { + return errors.New("minimum_version is required when deadline is provided") + } + + if !versionStringRegex.MatchString(m.MinimumVersion) { + return errors.New(`minimum_version accepts version numbers only. (E.g., "13.0.1.") NOT "Ventura 13" or "13.0.1 (22A400)"`) + } + + if _, err := time.Parse("2006-01-02", m.Deadline); err != nil { + return errors.New(`deadline accepts YYYY-MM-DD format only (E.g., "2023-06-01.")`) + } + + return nil +} + // AppConfig holds server configuration that can be changed via the API. // // Note: management of deprecated fields is done on JSON-marshalling and uses diff --git a/server/fleet/app_test.go b/server/fleet/app_test.go new file mode 100644 index 0000000000..662a901563 --- /dev/null +++ b/server/fleet/app_test.go @@ -0,0 +1,115 @@ +package fleet + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestMacOSUpdatesValidate(t *testing.T) { + t.Run("valid", func(t *testing.T) { + cases := []struct { + name string + m MacOSUpdates + }{ + {"empty", MacOSUpdates{}}, + { + "with full version", + MacOSUpdates{ + MinimumVersion: "10.15.0", + Deadline: "2020-01-01", + }, + }, + { + "without patch version", + MacOSUpdates{ + MinimumVersion: "10.15", + Deadline: "2020-01-01", + }, + }, + { + "only major version", + MacOSUpdates{ + MinimumVersion: "10", + Deadline: "2020-01-01", + }, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + require.NoError(t, tc.m.Validate()) + }) + } + }) + + t.Run("invalid deadline", func(t *testing.T) { + cases := []struct { + name string + m MacOSUpdates + }{ + { + "version but no deadline", + MacOSUpdates{ + MinimumVersion: "10.15.0", + Deadline: "", + }, + }, + { + "deadline with timestamp", + MacOSUpdates{ + MinimumVersion: "10.15.0", + Deadline: "2020-01-01T00:00:00Z", + }, + }, + { + "incomplete date", + MacOSUpdates{ + MinimumVersion: "10.15.0", + Deadline: "2020-01", + }, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + require.Error(t, tc.m.Validate()) + }) + } + }) + + t.Run("invalid version", func(t *testing.T) { + cases := []struct { + name string + m MacOSUpdates + }{ + { + "deadline but no version", + MacOSUpdates{ + MinimumVersion: "", + Deadline: "2020-01-01", + }, + }, + { + "version with build info", + MacOSUpdates{ + MinimumVersion: "10.15.0 (19A583)", + Deadline: "2020-01-01", + }, + }, + { + "version with patch info", + MacOSUpdates{ + MinimumVersion: "10.15.0-patch1", + Deadline: "2020-01-01", + }, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + require.Error(t, tc.m.Validate()) + }) + } + }) +} diff --git a/server/fleet/teams.go b/server/fleet/teams.go index d584030268..efd319a292 100644 --- a/server/fleet/teams.go +++ b/server/fleet/teams.go @@ -19,6 +19,7 @@ type TeamPayload struct { Secrets []*EnrollSecret `json:"secrets"` WebhookSettings *TeamWebhookSettings `json:"webhook_settings"` Integrations *TeamIntegrations `json:"integrations"` + MDM *TeamMDM `json:"mdm"` // Note AgentOptions must be set by a separate endpoint. } @@ -123,12 +124,17 @@ type TeamConfig struct { WebhookSettings TeamWebhookSettings `json:"webhook_settings"` Integrations TeamIntegrations `json:"integrations"` Features Features `json:"features"` + MDM TeamMDM `json:"mdm"` } type TeamWebhookSettings struct { FailingPoliciesWebhook FailingPoliciesWebhookSettings `json:"failing_policies_webhook"` } +type TeamMDM struct { + MacOSUpdates MacOSUpdates `json:"macos_updates"` +} + // Scan implements the sql.Scanner interface func (t *TeamConfig) Scan(val interface{}) error { switch v := val.(type) { @@ -264,4 +270,5 @@ type TeamSpec struct { Secrets []EnrollSecret `json:"secrets"` Features *json.RawMessage `json:"features"` + MDM TeamMDM `json:"mdm"` } diff --git a/server/service/appconfig.go b/server/service/appconfig.go index 4a37db7990..48eceaad35 100644 --- a/server/service/appconfig.go +++ b/server/service/appconfig.go @@ -452,6 +452,22 @@ func (svc *Service) validateMDM( invalid.Append("apple_bm_default_team", "team name not found") } } + + // MacOSUpdates + updatingVersion := mdm.MacOSUpdates.MinimumVersion != "" && + mdm.MacOSUpdates.MinimumVersion != oldMdm.MacOSUpdates.MinimumVersion + updatingDeadline := mdm.MacOSUpdates.Deadline != "" && + mdm.MacOSUpdates.Deadline != oldMdm.MacOSUpdates.Deadline + + if updatingVersion || updatingDeadline { + if !license.IsPremium() { + invalid.Append("macos_updates.minimum_version", ErrMissingLicense.Error()) + return + } + if err := mdm.MacOSUpdates.Validate(); err != nil { + invalid.Append("macos_updates", err.Error()) + } + } } func validateSSOSettings(p fleet.AppConfig, existing *fleet.AppConfig, invalid *fleet.InvalidArgumentError, license *fleet.LicenseInfo) { diff --git a/server/service/integration_enterprise_test.go b/server/service/integration_enterprise_test.go index 3c8ebadf9b..7320a44cfa 100644 --- a/server/service/integration_enterprise_test.go +++ b/server/service/integration_enterprise_test.go @@ -80,12 +80,27 @@ func (s *integrationEnterpriseTestSuite) TestTeamSpecs() { // updates a team, no secret is provided so it will keep the one generated // automatically when the team was created. agentOpts := json.RawMessage(`{"config": {"views": {"foo": "bar"}}, "overrides": {"platforms": {"darwin": {"views": {"bar": "qux"}}}}}`) + mdm := fleet.TeamMDM{ + MacOSUpdates: fleet.MacOSUpdates{ + MinimumVersion: "10.15.0", + Deadline: "2021-01-01", + }, + } features := json.RawMessage(`{ "enable_host_users": false, "enable_software_inventory": false, "additional_queries": {"foo": "bar"} }`) - teamSpecs := applyTeamSpecsRequest{Specs: []*fleet.TeamSpec{{Name: teamName, AgentOptions: agentOpts, Features: &features}}} + teamSpecs := applyTeamSpecsRequest{ + Specs: []*fleet.TeamSpec{ + { + Name: teamName, + AgentOptions: agentOpts, + Features: &features, + MDM: mdm, + }, + }, + } s.Do("POST", "/api/latest/fleet/spec/teams", teamSpecs, http.StatusOK) team, err := s.ds.TeamByName(context.Background(), teamName) @@ -97,6 +112,7 @@ func (s *integrationEnterpriseTestSuite) TestTeamSpecs() { EnableSoftwareInventory: false, AdditionalQueries: ptr.RawMessage(json.RawMessage(`{"foo": "bar"}`)), }, team.Config.Features) + require.Equal(t, mdm, team.Config.MDM) // an activity was created for team spec applied var listActivities listActivitiesResponse @@ -171,7 +187,7 @@ func (s *integrationEnterpriseTestSuite) TestTeamSpecs() { ] }`), http.StatusOK, "force", "true") - team, err = s.ds.TeamByName(context.Background(), "team_with_invalid_key") + _, err = s.ds.TeamByName(context.Background(), "team_with_invalid_key") require.NoError(t, err) // invalid agent options command-line flag @@ -1221,6 +1237,95 @@ func (s *integrationEnterpriseTestSuite) TestExternalIntegrationsTeamConfig() { }`), http.StatusOK) } +func (s *integrationEnterpriseTestSuite) TestMacOSUpdatesConfig() { + t := s.T() + + // Create a team + team := &fleet.Team{ + Name: t.Name(), + Description: "Team description", + Secrets: []*fleet.EnrollSecret{{Secret: "XYZ"}}, + } + var tmResp teamResponse + s.DoJSON("POST", "/api/latest/fleet/teams", team, http.StatusOK, &tmResp) + require.Equal(t, team.Name, tmResp.Team.Name) + team.ID = tmResp.Team.ID + + // modify the team's config + s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/teams/%d", team.ID), fleet.TeamPayload{ + MDM: &fleet.TeamMDM{ + MacOSUpdates: fleet.MacOSUpdates{ + MinimumVersion: "10.15.0", + Deadline: "2021-01-01", + }, + }, + }, http.StatusOK, &tmResp) + require.Equal(t, "10.15.0", tmResp.Team.Config.MDM.MacOSUpdates.MinimumVersion) + require.Equal(t, "2021-01-01", tmResp.Team.Config.MDM.MacOSUpdates.Deadline) + + // only update the deadline + s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/teams/%d", team.ID), fleet.TeamPayload{ + MDM: &fleet.TeamMDM{ + MacOSUpdates: fleet.MacOSUpdates{ + MinimumVersion: "10.15.0", + Deadline: "2025-10-01", + }, + }, + }, http.StatusOK, &tmResp) + require.Equal(t, "10.15.0", tmResp.Team.Config.MDM.MacOSUpdates.MinimumVersion) + require.Equal(t, "2025-10-01", tmResp.Team.Config.MDM.MacOSUpdates.Deadline) + + // sending a nil MacOSUpdate config doesn't modify anything + s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/teams/%d", team.ID), fleet.TeamPayload{MDM: nil}, http.StatusOK, &tmResp) + require.Equal(t, "10.15.0", tmResp.Team.Config.MDM.MacOSUpdates.MinimumVersion) + require.Equal(t, "2025-10-01", tmResp.Team.Config.MDM.MacOSUpdates.Deadline) + + // sending an empty MacOSUpdate empties both fields + s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/teams/%d", team.ID), fleet.TeamPayload{MDM: &fleet.TeamMDM{MacOSUpdates: fleet.MacOSUpdates{}}}, http.StatusOK, &tmResp) + require.Empty(t, tmResp.Team.Config.MDM.MacOSUpdates.MinimumVersion) + require.Empty(t, tmResp.Team.Config.MDM.MacOSUpdates.Deadline) + + // error checks: + + // try to set an invalid deadline + s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/teams/%d", team.ID), fleet.TeamPayload{ + MDM: &fleet.TeamMDM{ + MacOSUpdates: fleet.MacOSUpdates{ + MinimumVersion: "10.15.0", + Deadline: "2021-01-01T00:00:00Z", + }, + }, + }, http.StatusUnprocessableEntity, &tmResp) + + // try to set an invalid minimum version + s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/teams/%d", team.ID), fleet.TeamPayload{ + MDM: &fleet.TeamMDM{ + MacOSUpdates: fleet.MacOSUpdates{ + MinimumVersion: "10.15.0 (19A583)", + Deadline: "2021-01-01T00:00:00Z", + }, + }, + }, http.StatusUnprocessableEntity, &tmResp) + + // try to set a deadline but not a minimum version + s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/teams/%d", team.ID), fleet.TeamPayload{ + MDM: &fleet.TeamMDM{ + MacOSUpdates: fleet.MacOSUpdates{ + Deadline: "2021-01-01T00:00:00Z", + }, + }, + }, http.StatusUnprocessableEntity, &tmResp) + + // try to set a minimum version but not a deadline + s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/teams/%d", team.ID), fleet.TeamPayload{ + MDM: &fleet.TeamMDM{ + MacOSUpdates: fleet.MacOSUpdates{ + MinimumVersion: "10.15.0 (19A583)", + }, + }, + }, http.StatusUnprocessableEntity, &tmResp) +} + func (s *integrationEnterpriseTestSuite) TestListDevicePolicies() { t := s.T() @@ -1419,6 +1524,78 @@ func (s *integrationEnterpriseTestSuite) TestDefaultAppleBMTeam() { require.Equal(t, tm.Name, acResp.MDM.AppleBMDefaultTeam) } +func (s *integrationEnterpriseTestSuite) TestMDMMacOSUpdates() { + t := s.T() + + checkInvalidConfig := func(config string) { + // try to set an invalid config + acResp := appConfigResponse{} + s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(config), http.StatusUnprocessableEntity, &acResp) + + // get the appconfig, nothing changed + acResp = appConfigResponse{} + s.DoJSON("GET", "/api/latest/fleet/config", nil, http.StatusOK, &acResp) + require.Equal(t, fleet.MacOSUpdates{}, acResp.MDM.MacOSUpdates) + } + + // missing minimum_version + checkInvalidConfig(`{"mdm": { + "macos_updates": { + "deadline": "2022-01-01" + } + }}`) + + // missing deadline + checkInvalidConfig(`{"mdm": { + "macos_updates": { + "minimum_version": "12.1.1" + } + }}`) + + // invalid deadline + checkInvalidConfig(`{"mdm": { + "macos_updates": { + "minimum_version": "12.1.1", + "deadline": "2022" + } + }}`) + + // deadline includes timestamp + checkInvalidConfig(`{"mdm": { + "macos_updates": { + "minimum_version": "12.1.1", + "deadline": "2022-01-01T00:00:00Z" + } + }}`) + + // minimum_version includes build info + checkInvalidConfig(`{"mdm": { + "macos_updates": { + "minimum_version": "12.1.1 (ABCD)", + "deadline": "2022-01-01" + } + }}`) + + // valid config + acResp := appConfigResponse{} + s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(`{ + "mdm": { + "macos_updates": { + "minimum_version": "12.3.1", + "deadline": "2022-01-01" + } + } + }`), http.StatusOK, &acResp) + require.Equal(t, "12.3.1", acResp.MDM.MacOSUpdates.MinimumVersion) + require.Equal(t, "2022-01-01", acResp.MDM.MacOSUpdates.Deadline) + + // get the appconfig + acResp = appConfigResponse{} + s.DoJSON("GET", "/api/latest/fleet/config", nil, http.StatusOK, &acResp) + require.Equal(t, "12.3.1", acResp.MDM.MacOSUpdates.MinimumVersion) + require.Equal(t, "2022-01-01", acResp.MDM.MacOSUpdates.Deadline) +} + func (s *integrationEnterpriseTestSuite) TestSSOJITProvisioning() { t := s.T()