add mdm root key and macos_updates to app and team configs (#9442)

Related to https://github.com/fleetdm/fleet/issues/9345,
https://github.com/fleetdm/fleet/issues/9358 and
https://github.com/fleetdm/fleet/issues/9346 this adds:

1. The ability to configure `mdm.macos_updates` via `PATCH /config` and
`PATCH /teams/{id}`
3. The ability to configure `mdm.macos_updates` by using `fleetctl apply
-f` for teams and global config.
This commit is contained in:
Roberto Dip
2023-01-24 13:20:02 -03:00
committed by GitHub
parent caaec069ff
commit 2d25a3f48d
12 changed files with 750 additions and 42 deletions
+1
View File
@@ -0,0 +1 @@
- Allow to configure a minimum macOS version and a deadline for hosts enrolled into Fleet's MDM.
+212 -4
View File
@@ -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
+51 -8
View File
@@ -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"
+55 -26
View File
@@ -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"
}
},
}
}
```
+60 -1
View File
@@ -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
+12
View File
@@ -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
+1 -1
View File
@@ -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` (
+41
View File
@@ -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
+115
View File
@@ -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())
})
}
})
}
+7
View File
@@ -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"`
}
+16
View File
@@ -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) {
+179 -2
View File
@@ -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()