mirror of
https://github.com/empayre/fleet.git
synced 2024-11-06 00:45:19 +00:00
Implement Windows OS Updates (feature branch). (#15359)
This commit is contained in:
parent
0b5eedb801
commit
2f927df4f0
BIN
assets/images/windows-nudge-screenshot.png
Normal file
BIN
assets/images/windows-nudge-screenshot.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 121 KiB |
1
changes/issue-14027-implement-windows-os-updates
Normal file
1
changes/issue-14027-implement-windows-os-updates
Normal file
@ -0,0 +1 @@
|
||||
- add ability to change and view windows os updates in Fleet UI
|
1
changes/issue-14028-support-windows-os-updates
Normal file
1
changes/issue-14028-support-windows-os-updates
Normal file
@ -0,0 +1 @@
|
||||
* Added support to configure Windows OS updates requirements for hosts enrolled in Fleet MDM.
|
1
changes/issue-14029-apply-windows-os-updates
Normal file
1
changes/issue-14029-apply-windows-os-updates
Normal file
@ -0,0 +1 @@
|
||||
* Added deployment of Windows OS updates settings to the targeted hosts so that they take effect.
|
1
changes/issue-14045-add-windows-update-activites
Normal file
1
changes/issue-14045-add-windows-update-activites
Normal file
@ -0,0 +1 @@
|
||||
- add window os updates activites to Fleet UI.
|
@ -213,6 +213,32 @@ spec:
|
||||
assert.True(t, ds.ApplyEnrollSecretsFuncInvoked)
|
||||
ds.ApplyEnrollSecretsFuncInvoked = false
|
||||
|
||||
// add windows updates settings to team1
|
||||
filename = writeTmpYml(t, `
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: team
|
||||
spec:
|
||||
team:
|
||||
name: team1
|
||||
mdm:
|
||||
windows_updates:
|
||||
deadline_days: 5
|
||||
grace_period_days: 1
|
||||
`)
|
||||
require.Equal(t, "[+] applied 1 teams\n", runAppForTest(t, []string{"apply", "-f", filename}))
|
||||
newMDMSettings = fleet.TeamMDM{
|
||||
MacOSUpdates: fleet.MacOSUpdates{
|
||||
MinimumVersion: optjson.SetString("12.3.1"),
|
||||
Deadline: optjson.SetString("2011-03-01"),
|
||||
},
|
||||
WindowsUpdates: fleet.WindowsUpdates{
|
||||
DeadlineDays: optjson.SetInt(5),
|
||||
GracePeriodDays: optjson.SetInt(1),
|
||||
},
|
||||
}
|
||||
assert.Equal(t, newMDMSettings, teamsByName["team1"].Config.MDM)
|
||||
|
||||
mobileCfgPath := writeTmpMobileconfig(t, "N1")
|
||||
filename = writeTmpYml(t, fmt.Sprintf(`
|
||||
apiVersion: v1
|
||||
@ -231,6 +257,10 @@ spec:
|
||||
MinimumVersion: optjson.SetString("12.3.1"),
|
||||
Deadline: optjson.SetString("2011-03-01"),
|
||||
},
|
||||
WindowsUpdates: fleet.WindowsUpdates{
|
||||
DeadlineDays: optjson.SetInt(5),
|
||||
GracePeriodDays: optjson.SetInt(1),
|
||||
},
|
||||
MacOSSettings: fleet.MacOSSettings{
|
||||
CustomSettings: []string{mobileCfgPath},
|
||||
},
|
||||
@ -268,6 +298,10 @@ spec:
|
||||
MinimumVersion: optjson.SetString("10.10.10"),
|
||||
Deadline: optjson.SetString("1992-03-01"),
|
||||
},
|
||||
WindowsUpdates: fleet.WindowsUpdates{
|
||||
DeadlineDays: optjson.SetInt(5),
|
||||
GracePeriodDays: optjson.SetInt(1),
|
||||
},
|
||||
MacOSSettings: fleet.MacOSSettings{ // macos settings not provided, so not cleared
|
||||
CustomSettings: []string{mobileCfgPath},
|
||||
},
|
||||
@ -325,6 +359,9 @@ spec:
|
||||
macos_updates:
|
||||
minimum_version:
|
||||
deadline:
|
||||
windows_updates:
|
||||
deadline_days:
|
||||
grace_period_days:
|
||||
macos_settings:
|
||||
custom_settings:
|
||||
`)
|
||||
@ -334,6 +371,10 @@ spec:
|
||||
MinimumVersion: optjson.String{Set: true},
|
||||
Deadline: optjson.String{Set: true},
|
||||
},
|
||||
WindowsUpdates: fleet.WindowsUpdates{
|
||||
DeadlineDays: optjson.Int{Set: true},
|
||||
GracePeriodDays: optjson.Int{Set: true},
|
||||
},
|
||||
MacOSSettings: fleet.MacOSSettings{
|
||||
CustomSettings: []string{},
|
||||
},
|
||||
@ -385,6 +426,12 @@ func TestApplyAppConfig(t *testing.T) {
|
||||
ds.TeamByNameFunc = func(ctx context.Context, name string) (*fleet.Team, error) {
|
||||
return &fleet.Team{ID: 123}, nil
|
||||
}
|
||||
ds.SetOrUpdateMDMWindowsConfigProfileFunc = func(ctx context.Context, cp fleet.MDMWindowsConfigProfile) error {
|
||||
return nil
|
||||
}
|
||||
ds.DeleteMDMWindowsConfigProfileByTeamAndNameFunc = func(ctx context.Context, teamID *uint, profileName string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
defaultAgentOpts := json.RawMessage(`{"config":{"foo":"bar"}}`)
|
||||
savedAppConfig := &fleet.AppConfig{
|
||||
@ -413,6 +460,9 @@ spec:
|
||||
macos_updates:
|
||||
minimum_version: 12.1.1
|
||||
deadline: 2011-02-01
|
||||
windows_updates:
|
||||
deadline_days: 5
|
||||
grace_period_days: 1
|
||||
`)
|
||||
|
||||
newMDMSettings := fleet.MDM{
|
||||
@ -422,6 +472,10 @@ spec:
|
||||
MinimumVersion: optjson.SetString("12.1.1"),
|
||||
Deadline: optjson.SetString("2011-02-01"),
|
||||
},
|
||||
WindowsUpdates: fleet.WindowsUpdates{
|
||||
DeadlineDays: optjson.SetInt(5),
|
||||
GracePeriodDays: optjson.SetInt(1),
|
||||
},
|
||||
}
|
||||
assert.Equal(t, "[+] applied fleet config\n", runAppForTest(t, []string{"apply", "-f", name}))
|
||||
require.NotNil(t, savedAppConfig)
|
||||
@ -441,6 +495,7 @@ spec:
|
||||
agent_options:
|
||||
mdm:
|
||||
macos_updates:
|
||||
windows_updates:
|
||||
`)
|
||||
|
||||
assert.Equal(t, "[+] applied fleet config\n", runAppForTest(t, []string{"apply", "-f", name}))
|
||||
@ -449,6 +504,33 @@ spec:
|
||||
assert.True(t, savedAppConfig.Features.EnableSoftwareInventory)
|
||||
// agent options were cleared, provided but empty
|
||||
assert.Nil(t, savedAppConfig.AgentOptions)
|
||||
// MDM settings unchanged, not provided
|
||||
assert.Equal(t, newMDMSettings, savedAppConfig.MDM)
|
||||
|
||||
name = writeTmpYml(t, `---
|
||||
apiVersion: v1
|
||||
kind: config
|
||||
spec:
|
||||
mdm:
|
||||
windows_updates:
|
||||
deadline_days:
|
||||
grace_period_days:
|
||||
`)
|
||||
|
||||
newMDMSettings = fleet.MDM{
|
||||
AppleBMDefaultTeam: "team1",
|
||||
AppleBMTermsExpired: false,
|
||||
MacOSUpdates: fleet.MacOSUpdates{
|
||||
MinimumVersion: optjson.SetString("12.1.1"),
|
||||
Deadline: optjson.SetString("2011-02-01"),
|
||||
},
|
||||
WindowsUpdates: fleet.WindowsUpdates{
|
||||
DeadlineDays: optjson.Int{Set: true},
|
||||
GracePeriodDays: optjson.Int{Set: true},
|
||||
},
|
||||
}
|
||||
assert.Equal(t, "[+] applied fleet config\n", runAppForTest(t, []string{"apply", "-f", name}))
|
||||
require.NotNil(t, savedAppConfig)
|
||||
assert.Equal(t, newMDMSettings, savedAppConfig.MDM)
|
||||
}
|
||||
|
||||
@ -927,6 +1009,12 @@ func TestApplyAsGitOps(t *testing.T) {
|
||||
ds.InsertMDMAppleBootstrapPackageFunc = func(ctx context.Context, bp *fleet.MDMAppleBootstrapPackage) error {
|
||||
return nil
|
||||
}
|
||||
ds.SetOrUpdateMDMWindowsConfigProfileFunc = func(ctx context.Context, cp fleet.MDMWindowsConfigProfile) error {
|
||||
return nil
|
||||
}
|
||||
ds.DeleteMDMWindowsConfigProfileByTeamAndNameFunc = func(ctx context.Context, teamID *uint, profileName string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Apply global config.
|
||||
name := writeTmpYml(t, `---
|
||||
@ -977,6 +1065,9 @@ spec:
|
||||
macos_updates:
|
||||
minimum_version: 10.10.10
|
||||
deadline: 2020-02-02
|
||||
windows_updates:
|
||||
deadline_days: 1
|
||||
grace_period_days: 0
|
||||
macos_settings:
|
||||
custom_settings:
|
||||
- %s
|
||||
@ -1001,6 +1092,10 @@ spec:
|
||||
MinimumVersion: optjson.SetString("10.10.10"),
|
||||
Deadline: optjson.SetString("2020-02-02"),
|
||||
},
|
||||
WindowsUpdates: fleet.WindowsUpdates{
|
||||
DeadlineDays: optjson.SetInt(1),
|
||||
GracePeriodDays: optjson.SetInt(0),
|
||||
},
|
||||
MacOSSettings: fleet.MacOSSettings{
|
||||
CustomSettings: []string{mobileConfigPath},
|
||||
},
|
||||
@ -1039,6 +1134,10 @@ spec:
|
||||
MinimumVersion: optjson.SetString("10.10.10"),
|
||||
Deadline: optjson.SetString("2020-02-02"),
|
||||
},
|
||||
WindowsUpdates: fleet.WindowsUpdates{
|
||||
DeadlineDays: optjson.SetInt(1),
|
||||
GracePeriodDays: optjson.SetInt(0),
|
||||
},
|
||||
MacOSSettings: fleet.MacOSSettings{
|
||||
CustomSettings: []string{mobileConfigPath},
|
||||
},
|
||||
@ -1061,6 +1160,9 @@ spec:
|
||||
macos_updates:
|
||||
minimum_version: 10.10.10
|
||||
deadline: 1992-03-01
|
||||
windows_updates:
|
||||
deadline_days: 0
|
||||
grace_period_days: 1
|
||||
macos_settings:
|
||||
custom_settings:
|
||||
- %s
|
||||
@ -1083,6 +1185,10 @@ spec:
|
||||
MinimumVersion: optjson.SetString("10.10.10"),
|
||||
Deadline: optjson.SetString("1992-03-01"),
|
||||
},
|
||||
WindowsUpdates: fleet.WindowsUpdates{
|
||||
DeadlineDays: optjson.SetInt(0),
|
||||
GracePeriodDays: optjson.SetInt(1),
|
||||
},
|
||||
}, savedTeam.Config.MDM)
|
||||
assert.Equal(t, []*fleet.EnrollSecret{{Secret: "BBB"}}, teamEnrollSecrets)
|
||||
assert.True(t, ds.ApplyEnrollSecretsFuncInvoked)
|
||||
@ -1118,6 +1224,10 @@ spec:
|
||||
MinimumVersion: optjson.SetString("10.10.10"),
|
||||
Deadline: optjson.SetString("1992-03-01"),
|
||||
},
|
||||
WindowsUpdates: fleet.WindowsUpdates{
|
||||
DeadlineDays: optjson.SetInt(0),
|
||||
GracePeriodDays: optjson.SetInt(1),
|
||||
},
|
||||
MacOSSetup: fleet.MacOSSetup{
|
||||
MacOSSetupAssistant: optjson.SetString(emptySetupAsst),
|
||||
},
|
||||
@ -1150,6 +1260,10 @@ spec:
|
||||
MinimumVersion: optjson.SetString("10.10.10"),
|
||||
Deadline: optjson.SetString("1992-03-01"),
|
||||
},
|
||||
WindowsUpdates: fleet.WindowsUpdates{
|
||||
DeadlineDays: optjson.SetInt(0),
|
||||
GracePeriodDays: optjson.SetInt(1),
|
||||
},
|
||||
MacOSSetup: fleet.MacOSSetup{
|
||||
MacOSSetupAssistant: optjson.SetString(emptySetupAsst),
|
||||
BootstrapPackage: optjson.SetString(bootstrapURL),
|
||||
@ -2196,6 +2310,12 @@ func TestApplySpecs(t *testing.T) {
|
||||
ds.SaveAppConfigFunc = func(ctx context.Context, config *fleet.AppConfig) error {
|
||||
return nil
|
||||
}
|
||||
ds.SetOrUpdateMDMWindowsConfigProfileFunc = func(ctx context.Context, cp fleet.MDMWindowsConfigProfile) error {
|
||||
return nil
|
||||
}
|
||||
ds.DeleteMDMWindowsConfigProfileByTeamAndNameFunc = func(ctx context.Context, teamID *uint, profileName string) error {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
@ -2665,6 +2785,124 @@ spec:
|
||||
`,
|
||||
wantErr: `422 Validation Failed: deadline accepts YYYY-MM-DD format only (E.g., "2023-06-01.")`,
|
||||
},
|
||||
{
|
||||
desc: "windows_updates.deadline_days but grace period empty",
|
||||
spec: `
|
||||
apiVersion: v1
|
||||
kind: team
|
||||
spec:
|
||||
team:
|
||||
name: team1
|
||||
mdm:
|
||||
windows_updates:
|
||||
deadline_days: 5
|
||||
`,
|
||||
wantErr: `422 Validation Failed: grace_period_days is required when deadline_days is provided`,
|
||||
},
|
||||
{
|
||||
desc: "windows_updates.grace_period_days but deadline empty",
|
||||
spec: `
|
||||
apiVersion: v1
|
||||
kind: team
|
||||
spec:
|
||||
team:
|
||||
name: team1
|
||||
mdm:
|
||||
windows_updates:
|
||||
grace_period_days: 5
|
||||
`,
|
||||
wantErr: `422 Validation Failed: deadline_days is required when grace_period_days is provided`,
|
||||
},
|
||||
{
|
||||
desc: "windows_updates.deadline_days out of range",
|
||||
spec: `
|
||||
apiVersion: v1
|
||||
kind: team
|
||||
spec:
|
||||
team:
|
||||
name: team1
|
||||
mdm:
|
||||
windows_updates:
|
||||
deadline_days: 9999
|
||||
grace_period_days: 1
|
||||
`,
|
||||
wantErr: `422 Validation Failed: deadline_days must be an integer between 0 and 30`,
|
||||
},
|
||||
{
|
||||
desc: "windows_updates.grace_period_days out of range",
|
||||
spec: `
|
||||
apiVersion: v1
|
||||
kind: team
|
||||
spec:
|
||||
team:
|
||||
name: team1
|
||||
mdm:
|
||||
windows_updates:
|
||||
deadline_days: 1
|
||||
grace_period_days: 9999
|
||||
`,
|
||||
wantErr: `422 Validation Failed: grace_period_days must be an integer between 0 and 7`,
|
||||
},
|
||||
{
|
||||
desc: "windows_updates.deadline_days not a number",
|
||||
spec: `
|
||||
apiVersion: v1
|
||||
kind: team
|
||||
spec:
|
||||
team:
|
||||
name: team1
|
||||
mdm:
|
||||
windows_updates:
|
||||
deadline_days: abc
|
||||
grace_period_days: 1
|
||||
`,
|
||||
wantErr: `400 Bad Request: invalid value type at 'specs.mdm.windows_updates.deadline_days': expected int but got string`,
|
||||
},
|
||||
{
|
||||
desc: "windows_updates.grace_period_days not a number",
|
||||
spec: `
|
||||
apiVersion: v1
|
||||
kind: team
|
||||
spec:
|
||||
team:
|
||||
name: team1
|
||||
mdm:
|
||||
windows_updates:
|
||||
deadline_days: 1
|
||||
grace_period_days: true
|
||||
`,
|
||||
wantErr: `400 Bad Request: invalid value type at 'specs.mdm.windows_updates.grace_period_days': expected int but got bool`,
|
||||
},
|
||||
{
|
||||
desc: "windows_updates valid",
|
||||
spec: `
|
||||
apiVersion: v1
|
||||
kind: team
|
||||
spec:
|
||||
team:
|
||||
name: team1
|
||||
mdm:
|
||||
windows_updates:
|
||||
deadline_days: 5
|
||||
grace_period_days: 1
|
||||
`,
|
||||
wantOutput: `[+] applied 1 teams`,
|
||||
},
|
||||
{
|
||||
desc: "windows_updates unset valid",
|
||||
spec: `
|
||||
apiVersion: v1
|
||||
kind: team
|
||||
spec:
|
||||
team:
|
||||
name: team1
|
||||
mdm:
|
||||
windows_updates:
|
||||
deadline_days:
|
||||
grace_period_days:
|
||||
`,
|
||||
wantOutput: `[+] applied 1 teams`,
|
||||
},
|
||||
{
|
||||
desc: "missing required sso entity_id",
|
||||
spec: `
|
||||
@ -2868,6 +3106,108 @@ spec:
|
||||
`,
|
||||
wantErr: `422 Validation Failed: deadline accepts YYYY-MM-DD format only (E.g., "2023-06-01.")`,
|
||||
},
|
||||
{
|
||||
desc: "app config windows_updates.deadline_days but grace period empty",
|
||||
spec: `
|
||||
apiVersion: v1
|
||||
kind: config
|
||||
spec:
|
||||
mdm:
|
||||
windows_updates:
|
||||
deadline_days: 5
|
||||
`,
|
||||
wantErr: `422 Validation Failed: grace_period_days is required when deadline_days is provided`,
|
||||
},
|
||||
{
|
||||
desc: "app config windows_updates.grace_period_days but deadline empty",
|
||||
spec: `
|
||||
apiVersion: v1
|
||||
kind: config
|
||||
spec:
|
||||
mdm:
|
||||
windows_updates:
|
||||
grace_period_days: 5
|
||||
`,
|
||||
wantErr: `422 Validation Failed: deadline_days is required when grace_period_days is provided`,
|
||||
},
|
||||
{
|
||||
desc: "app config windows_updates.deadline_days out of range",
|
||||
spec: `
|
||||
apiVersion: v1
|
||||
kind: config
|
||||
spec:
|
||||
mdm:
|
||||
windows_updates:
|
||||
deadline_days: 9999
|
||||
grace_period_days: 1
|
||||
`,
|
||||
wantErr: `422 Validation Failed: deadline_days must be an integer between 0 and 30`,
|
||||
},
|
||||
{
|
||||
desc: "app config windows_updates.grace_period_days out of range",
|
||||
spec: `
|
||||
apiVersion: v1
|
||||
kind: config
|
||||
spec:
|
||||
mdm:
|
||||
windows_updates:
|
||||
deadline_days: 1
|
||||
grace_period_days: 9999
|
||||
`,
|
||||
wantErr: `422 Validation Failed: grace_period_days must be an integer between 0 and 7`,
|
||||
},
|
||||
{
|
||||
desc: "app config windows_updates.deadline_days not a number",
|
||||
spec: `
|
||||
apiVersion: v1
|
||||
kind: config
|
||||
spec:
|
||||
mdm:
|
||||
windows_updates:
|
||||
deadline_days: abc
|
||||
grace_period_days: 1
|
||||
`,
|
||||
wantErr: `400 Bad request: failed to decode app config`,
|
||||
},
|
||||
{
|
||||
desc: "app config windows_updates.grace_period_days not a number",
|
||||
spec: `
|
||||
apiVersion: v1
|
||||
kind: config
|
||||
spec:
|
||||
mdm:
|
||||
windows_updates:
|
||||
deadline_days: 1
|
||||
grace_period_days: true
|
||||
`,
|
||||
wantErr: `400 Bad request: failed to decode app config`,
|
||||
},
|
||||
{
|
||||
desc: "app config windows_updates valid",
|
||||
spec: `
|
||||
apiVersion: v1
|
||||
kind: config
|
||||
spec:
|
||||
mdm:
|
||||
windows_updates:
|
||||
deadline_days: 5
|
||||
grace_period_days: 1
|
||||
`,
|
||||
wantOutput: `[+] applied fleet config`,
|
||||
},
|
||||
{
|
||||
desc: "app config windows_updates unset valid",
|
||||
spec: `
|
||||
apiVersion: v1
|
||||
kind: config
|
||||
spec:
|
||||
mdm:
|
||||
windows_updates:
|
||||
deadline_days:
|
||||
grace_period_days:
|
||||
`,
|
||||
wantOutput: `[+] applied fleet config`,
|
||||
},
|
||||
{
|
||||
desc: "app config macos_settings.enable_disk_encryption without a value",
|
||||
spec: `
|
||||
|
@ -161,6 +161,10 @@ func TestGetTeams(t *testing.T) {
|
||||
MinimumVersion: optjson.SetString("12.3.1"),
|
||||
Deadline: optjson.SetString("2021-12-14"),
|
||||
},
|
||||
WindowsUpdates: fleet.WindowsUpdates{
|
||||
DeadlineDays: optjson.SetInt(7),
|
||||
GracePeriodDays: optjson.SetInt(3),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -560,6 +564,12 @@ func TestGetConfig(t *testing.T) {
|
||||
VulnerabilitySettings: fleet.VulnerabilitySettings{DatabasesPath: "/some/path"},
|
||||
SMTPSettings: &fleet.SMTPSettings{},
|
||||
SSOSettings: &fleet.SSOSettings{},
|
||||
MDM: fleet.MDM{
|
||||
WindowsUpdates: fleet.WindowsUpdates{
|
||||
DeadlineDays: optjson.SetInt(7),
|
||||
GracePeriodDays: optjson.SetInt(3),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
@ -1989,6 +1999,10 @@ func TestGetTeamsYAMLAndApply(t *testing.T) {
|
||||
MinimumVersion: optjson.SetString("12.3.1"),
|
||||
Deadline: optjson.SetString("2021-12-14"),
|
||||
},
|
||||
WindowsUpdates: fleet.WindowsUpdates{
|
||||
DeadlineDays: optjson.SetInt(7),
|
||||
GracePeriodDays: optjson.SetInt(3),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
@ -1,120 +1,124 @@
|
||||
{
|
||||
"kind": "config",
|
||||
"apiVersion": "v1",
|
||||
"spec": {
|
||||
"org_info": {
|
||||
"org_name": "",
|
||||
"org_logo_url": "",
|
||||
"org_logo_url_light_background": "",
|
||||
"contact_url": "https://fleetdm.com/company/contact"
|
||||
},
|
||||
"server_settings": {
|
||||
"server_url": "",
|
||||
"live_query_disabled": false,
|
||||
"query_reports_disabled": false,
|
||||
"enable_analytics": false,
|
||||
"deferred_save_host": false
|
||||
},
|
||||
"smtp_settings": {
|
||||
"enable_smtp": false,
|
||||
"configured": false,
|
||||
"sender_address": "",
|
||||
"server": "",
|
||||
"port": 0,
|
||||
"authentication_type": "",
|
||||
"user_name": "",
|
||||
"password": "",
|
||||
"enable_ssl_tls": false,
|
||||
"authentication_method": "",
|
||||
"domain": "",
|
||||
"verify_ssl_certs": false,
|
||||
"enable_start_tls": false
|
||||
},
|
||||
"host_expiry_settings": {
|
||||
"host_expiry_enabled": false,
|
||||
"host_expiry_window": 0
|
||||
},
|
||||
"features": {
|
||||
"enable_host_users": true,
|
||||
"enable_software_inventory": false
|
||||
},
|
||||
"sso_settings": {
|
||||
"entity_id": "",
|
||||
"issuer_uri": "",
|
||||
"idp_image_url": "",
|
||||
"metadata": "",
|
||||
"metadata_url": "",
|
||||
"idp_name": "",
|
||||
"enable_jit_provisioning": false,
|
||||
"enable_jit_role_sync": false,
|
||||
"enable_sso": false,
|
||||
"enable_sso_idp_login": false
|
||||
},
|
||||
"fleet_desktop": {
|
||||
"transparency_url": "https://fleetdm.com/transparency"
|
||||
},
|
||||
"vulnerability_settings": {
|
||||
"databases_path": "/some/path"
|
||||
},
|
||||
"webhook_settings": {
|
||||
"host_status_webhook": {
|
||||
"enable_host_status_webhook": false,
|
||||
"destination_url": "",
|
||||
"host_percentage": 0,
|
||||
"days_count": 0
|
||||
},
|
||||
"failing_policies_webhook": {
|
||||
"enable_failing_policies_webhook": false,
|
||||
"destination_url": "",
|
||||
"policy_ids": null,
|
||||
"host_batch_size": 0
|
||||
},
|
||||
"vulnerabilities_webhook": {
|
||||
"enable_vulnerabilities_webhook": false,
|
||||
"destination_url": "",
|
||||
"host_batch_size": 0
|
||||
},
|
||||
"interval": "0s"
|
||||
},
|
||||
"integrations": {
|
||||
"jira": null,
|
||||
"zendesk": null
|
||||
},
|
||||
"mdm": {
|
||||
"apple_bm_terms_expired": false,
|
||||
"apple_bm_enabled_and_configured": false,
|
||||
"enabled_and_configured": false,
|
||||
"apple_bm_default_team": "",
|
||||
"windows_enabled_and_configured": false,
|
||||
"enable_disk_encryption": false,
|
||||
"macos_updates": {
|
||||
"minimum_version": null,
|
||||
"deadline": null
|
||||
},
|
||||
"macos_migration": {
|
||||
"enable": false,
|
||||
"mode": "",
|
||||
"webhook_url": ""
|
||||
},
|
||||
"macos_settings": {
|
||||
"custom_settings": null
|
||||
},
|
||||
"macos_setup": {
|
||||
"bootstrap_package": null,
|
||||
"enable_end_user_authentication": false,
|
||||
"macos_setup_assistant": null
|
||||
},
|
||||
"windows_settings": {
|
||||
"custom_settings": null
|
||||
},
|
||||
"end_user_authentication": {
|
||||
"entity_id": "",
|
||||
"issuer_uri": "",
|
||||
"metadata": "",
|
||||
"metadata_url": "",
|
||||
"idp_name": ""
|
||||
}
|
||||
},
|
||||
"scripts": null
|
||||
}
|
||||
"kind": "config",
|
||||
"apiVersion": "v1",
|
||||
"spec": {
|
||||
"org_info": {
|
||||
"org_name": "",
|
||||
"org_logo_url": "",
|
||||
"org_logo_url_light_background": "",
|
||||
"contact_url": "https://fleetdm.com/company/contact"
|
||||
},
|
||||
"server_settings": {
|
||||
"server_url": "",
|
||||
"live_query_disabled": false,
|
||||
"query_reports_disabled": false,
|
||||
"enable_analytics": false,
|
||||
"deferred_save_host": false
|
||||
},
|
||||
"smtp_settings": {
|
||||
"enable_smtp": false,
|
||||
"configured": false,
|
||||
"sender_address": "",
|
||||
"server": "",
|
||||
"port": 0,
|
||||
"authentication_type": "",
|
||||
"user_name": "",
|
||||
"password": "",
|
||||
"enable_ssl_tls": false,
|
||||
"authentication_method": "",
|
||||
"domain": "",
|
||||
"verify_ssl_certs": false,
|
||||
"enable_start_tls": false
|
||||
},
|
||||
"host_expiry_settings": {
|
||||
"host_expiry_enabled": false,
|
||||
"host_expiry_window": 0
|
||||
},
|
||||
"features": {
|
||||
"enable_host_users": true,
|
||||
"enable_software_inventory": false
|
||||
},
|
||||
"sso_settings": {
|
||||
"entity_id": "",
|
||||
"issuer_uri": "",
|
||||
"idp_image_url": "",
|
||||
"metadata": "",
|
||||
"metadata_url": "",
|
||||
"idp_name": "",
|
||||
"enable_jit_provisioning": false,
|
||||
"enable_jit_role_sync": false,
|
||||
"enable_sso": false,
|
||||
"enable_sso_idp_login": false
|
||||
},
|
||||
"fleet_desktop": {
|
||||
"transparency_url": "https://fleetdm.com/transparency"
|
||||
},
|
||||
"vulnerability_settings": {
|
||||
"databases_path": "/some/path"
|
||||
},
|
||||
"webhook_settings": {
|
||||
"host_status_webhook": {
|
||||
"enable_host_status_webhook": false,
|
||||
"destination_url": "",
|
||||
"host_percentage": 0,
|
||||
"days_count": 0
|
||||
},
|
||||
"failing_policies_webhook": {
|
||||
"enable_failing_policies_webhook": false,
|
||||
"destination_url": "",
|
||||
"policy_ids": null,
|
||||
"host_batch_size": 0
|
||||
},
|
||||
"vulnerabilities_webhook": {
|
||||
"enable_vulnerabilities_webhook": false,
|
||||
"destination_url": "",
|
||||
"host_batch_size": 0
|
||||
},
|
||||
"interval": "0s"
|
||||
},
|
||||
"integrations": {
|
||||
"jira": null,
|
||||
"zendesk": null
|
||||
},
|
||||
"mdm": {
|
||||
"apple_bm_terms_expired": false,
|
||||
"apple_bm_enabled_and_configured": false,
|
||||
"enabled_and_configured": false,
|
||||
"apple_bm_default_team": "",
|
||||
"windows_enabled_and_configured": false,
|
||||
"enable_disk_encryption": false,
|
||||
"macos_updates": {
|
||||
"minimum_version": null,
|
||||
"deadline": null
|
||||
},
|
||||
"windows_updates": {
|
||||
"deadline_days": 7,
|
||||
"grace_period_days": 3
|
||||
},
|
||||
"macos_migration": {
|
||||
"enable": false,
|
||||
"mode": "",
|
||||
"webhook_url": ""
|
||||
},
|
||||
"macos_settings": {
|
||||
"custom_settings": null
|
||||
},
|
||||
"macos_setup": {
|
||||
"bootstrap_package": null,
|
||||
"enable_end_user_authentication": false,
|
||||
"macos_setup_assistant": null
|
||||
},
|
||||
"windows_settings": {
|
||||
"custom_settings": null
|
||||
},
|
||||
"end_user_authentication": {
|
||||
"entity_id": "",
|
||||
"issuer_uri": "",
|
||||
"metadata": "",
|
||||
"metadata_url": "",
|
||||
"idp_name": ""
|
||||
}
|
||||
},
|
||||
"scripts": null
|
||||
}
|
||||
}
|
||||
|
@ -27,6 +27,9 @@ spec:
|
||||
macos_updates:
|
||||
minimum_version: null
|
||||
deadline: null
|
||||
windows_updates:
|
||||
deadline_days: 7
|
||||
grace_period_days: 3
|
||||
macos_settings:
|
||||
custom_settings:
|
||||
macos_setup:
|
||||
|
@ -1,182 +1,186 @@
|
||||
{
|
||||
"kind": "config",
|
||||
"apiVersion": "v1",
|
||||
"spec": {
|
||||
"org_info": {
|
||||
"org_name": "",
|
||||
"org_logo_url": "",
|
||||
"org_logo_url_light_background": "",
|
||||
"contact_url": "https://fleetdm.com/company/contact"
|
||||
},
|
||||
"server_settings": {
|
||||
"server_url": "",
|
||||
"live_query_disabled": false,
|
||||
"query_reports_disabled": false,
|
||||
"enable_analytics": false,
|
||||
"deferred_save_host": false
|
||||
},
|
||||
"smtp_settings": {
|
||||
"enable_smtp": false,
|
||||
"configured": false,
|
||||
"sender_address": "",
|
||||
"server": "",
|
||||
"port": 0,
|
||||
"authentication_type": "",
|
||||
"user_name": "",
|
||||
"password": "",
|
||||
"enable_ssl_tls": false,
|
||||
"authentication_method": "",
|
||||
"domain": "",
|
||||
"verify_ssl_certs": false,
|
||||
"enable_start_tls": false
|
||||
},
|
||||
"host_expiry_settings": {
|
||||
"host_expiry_enabled": false,
|
||||
"host_expiry_window": 0
|
||||
},
|
||||
"features": {
|
||||
"enable_host_users": true,
|
||||
"enable_software_inventory": false
|
||||
},
|
||||
"mdm": {
|
||||
"apple_bm_default_team": "",
|
||||
"apple_bm_terms_expired": false,
|
||||
"apple_bm_enabled_and_configured": false,
|
||||
"enabled_and_configured": false,
|
||||
"windows_enabled_and_configured": false,
|
||||
"enable_disk_encryption": false,
|
||||
"macos_updates": {
|
||||
"minimum_version": null,
|
||||
"deadline": null
|
||||
},
|
||||
"macos_migration": {
|
||||
"enable": false,
|
||||
"mode": "",
|
||||
"webhook_url": ""
|
||||
},
|
||||
"macos_settings": {
|
||||
"custom_settings": null
|
||||
},
|
||||
"macos_setup": {
|
||||
"bootstrap_package": null,
|
||||
"enable_end_user_authentication": false,
|
||||
"macos_setup_assistant": null
|
||||
},
|
||||
"windows_settings": {
|
||||
"custom_settings": null
|
||||
},
|
||||
"end_user_authentication": {
|
||||
"entity_id": "",
|
||||
"issuer_uri": "",
|
||||
"metadata": "",
|
||||
"metadata_url": "",
|
||||
"idp_name": ""
|
||||
}
|
||||
},
|
||||
"scripts": null,
|
||||
"sso_settings": {
|
||||
"enable_jit_provisioning": false,
|
||||
"enable_jit_role_sync": false,
|
||||
"entity_id": "",
|
||||
"issuer_uri": "",
|
||||
"idp_image_url": "",
|
||||
"metadata": "",
|
||||
"metadata_url": "",
|
||||
"idp_name": "",
|
||||
"enable_sso": false,
|
||||
"enable_sso_idp_login": false
|
||||
},
|
||||
"fleet_desktop": {
|
||||
"transparency_url": "https://fleetdm.com/transparency"
|
||||
},
|
||||
"vulnerability_settings": {
|
||||
"databases_path": "/some/path"
|
||||
},
|
||||
"webhook_settings": {
|
||||
"host_status_webhook": {
|
||||
"enable_host_status_webhook": false,
|
||||
"destination_url": "",
|
||||
"host_percentage": 0,
|
||||
"days_count": 0
|
||||
},
|
||||
"failing_policies_webhook": {
|
||||
"enable_failing_policies_webhook": false,
|
||||
"destination_url": "",
|
||||
"policy_ids": null,
|
||||
"host_batch_size": 0
|
||||
},
|
||||
"vulnerabilities_webhook": {
|
||||
"enable_vulnerabilities_webhook": false,
|
||||
"destination_url": "",
|
||||
"host_batch_size": 0
|
||||
},
|
||||
"interval": "0s"
|
||||
},
|
||||
"integrations": {
|
||||
"jira": null,
|
||||
"zendesk": null
|
||||
},
|
||||
"update_interval": {
|
||||
"osquery_detail": "1h0m0s",
|
||||
"osquery_policy": "1h0m0s"
|
||||
},
|
||||
"vulnerabilities": {
|
||||
"databases_path": "",
|
||||
"periodicity": "0s",
|
||||
"cpe_database_url": "",
|
||||
"cpe_translations_url": "",
|
||||
"cve_feed_prefix_url": "",
|
||||
"current_instance_checks": "",
|
||||
"disable_data_sync": false,
|
||||
"recent_vulnerability_max_age": "0s",
|
||||
"disable_win_os_vulnerabilities": false
|
||||
},
|
||||
"license": {
|
||||
"tier": "free",
|
||||
"expiration": "0001-01-01T00:00:00Z"
|
||||
},
|
||||
"logging": {
|
||||
"debug": true,
|
||||
"json": false,
|
||||
"result": {
|
||||
"plugin": "filesystem",
|
||||
"config": {
|
||||
"enable_log_compression": false,
|
||||
"enable_log_rotation": false,
|
||||
"result_log_file": "/dev/null",
|
||||
"status_log_file": "/dev/null",
|
||||
"audit_log_file": "/dev/null",
|
||||
"max_size": 500,
|
||||
"max_age": 0,
|
||||
"max_backups": 0
|
||||
}
|
||||
},
|
||||
"status": {
|
||||
"plugin": "filesystem",
|
||||
"config": {
|
||||
"enable_log_compression": false,
|
||||
"enable_log_rotation": false,
|
||||
"result_log_file": "/dev/null",
|
||||
"status_log_file": "/dev/null",
|
||||
"audit_log_file": "/dev/null",
|
||||
"max_size": 500,
|
||||
"max_age": 0,
|
||||
"max_backups": 0
|
||||
}
|
||||
},
|
||||
"audit": {
|
||||
"plugin": "filesystem",
|
||||
"config": {
|
||||
"enable_log_compression": false,
|
||||
"enable_log_rotation": false,
|
||||
"result_log_file": "/dev/null",
|
||||
"status_log_file": "/dev/null",
|
||||
"audit_log_file": "/dev/null",
|
||||
"max_size": 500,
|
||||
"max_age": 0,
|
||||
"max_backups": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"kind": "config",
|
||||
"apiVersion": "v1",
|
||||
"spec": {
|
||||
"org_info": {
|
||||
"org_name": "",
|
||||
"org_logo_url": "",
|
||||
"org_logo_url_light_background": "",
|
||||
"contact_url": "https://fleetdm.com/company/contact"
|
||||
},
|
||||
"server_settings": {
|
||||
"server_url": "",
|
||||
"live_query_disabled": false,
|
||||
"query_reports_disabled": false,
|
||||
"enable_analytics": false,
|
||||
"deferred_save_host": false
|
||||
},
|
||||
"smtp_settings": {
|
||||
"enable_smtp": false,
|
||||
"configured": false,
|
||||
"sender_address": "",
|
||||
"server": "",
|
||||
"port": 0,
|
||||
"authentication_type": "",
|
||||
"user_name": "",
|
||||
"password": "",
|
||||
"enable_ssl_tls": false,
|
||||
"authentication_method": "",
|
||||
"domain": "",
|
||||
"verify_ssl_certs": false,
|
||||
"enable_start_tls": false
|
||||
},
|
||||
"host_expiry_settings": {
|
||||
"host_expiry_enabled": false,
|
||||
"host_expiry_window": 0
|
||||
},
|
||||
"features": {
|
||||
"enable_host_users": true,
|
||||
"enable_software_inventory": false
|
||||
},
|
||||
"mdm": {
|
||||
"apple_bm_default_team": "",
|
||||
"apple_bm_terms_expired": false,
|
||||
"apple_bm_enabled_and_configured": false,
|
||||
"enabled_and_configured": false,
|
||||
"windows_enabled_and_configured": false,
|
||||
"enable_disk_encryption": false,
|
||||
"macos_updates": {
|
||||
"minimum_version": null,
|
||||
"deadline": null
|
||||
},
|
||||
"windows_updates": {
|
||||
"deadline_days": 7,
|
||||
"grace_period_days": 3
|
||||
},
|
||||
"macos_migration": {
|
||||
"enable": false,
|
||||
"mode": "",
|
||||
"webhook_url": ""
|
||||
},
|
||||
"macos_settings": {
|
||||
"custom_settings": null
|
||||
},
|
||||
"macos_setup": {
|
||||
"bootstrap_package": null,
|
||||
"enable_end_user_authentication": false,
|
||||
"macos_setup_assistant": null
|
||||
},
|
||||
"windows_settings": {
|
||||
"custom_settings": null
|
||||
},
|
||||
"end_user_authentication": {
|
||||
"entity_id": "",
|
||||
"issuer_uri": "",
|
||||
"metadata": "",
|
||||
"metadata_url": "",
|
||||
"idp_name": ""
|
||||
}
|
||||
},
|
||||
"scripts": null,
|
||||
"sso_settings": {
|
||||
"enable_jit_provisioning": false,
|
||||
"enable_jit_role_sync": false,
|
||||
"entity_id": "",
|
||||
"issuer_uri": "",
|
||||
"idp_image_url": "",
|
||||
"metadata": "",
|
||||
"metadata_url": "",
|
||||
"idp_name": "",
|
||||
"enable_sso": false,
|
||||
"enable_sso_idp_login": false
|
||||
},
|
||||
"fleet_desktop": {
|
||||
"transparency_url": "https://fleetdm.com/transparency"
|
||||
},
|
||||
"vulnerability_settings": {
|
||||
"databases_path": "/some/path"
|
||||
},
|
||||
"webhook_settings": {
|
||||
"host_status_webhook": {
|
||||
"enable_host_status_webhook": false,
|
||||
"destination_url": "",
|
||||
"host_percentage": 0,
|
||||
"days_count": 0
|
||||
},
|
||||
"failing_policies_webhook": {
|
||||
"enable_failing_policies_webhook": false,
|
||||
"destination_url": "",
|
||||
"policy_ids": null,
|
||||
"host_batch_size": 0
|
||||
},
|
||||
"vulnerabilities_webhook": {
|
||||
"enable_vulnerabilities_webhook": false,
|
||||
"destination_url": "",
|
||||
"host_batch_size": 0
|
||||
},
|
||||
"interval": "0s"
|
||||
},
|
||||
"integrations": {
|
||||
"jira": null,
|
||||
"zendesk": null
|
||||
},
|
||||
"update_interval": {
|
||||
"osquery_detail": "1h0m0s",
|
||||
"osquery_policy": "1h0m0s"
|
||||
},
|
||||
"vulnerabilities": {
|
||||
"databases_path": "",
|
||||
"periodicity": "0s",
|
||||
"cpe_database_url": "",
|
||||
"cpe_translations_url": "",
|
||||
"cve_feed_prefix_url": "",
|
||||
"current_instance_checks": "",
|
||||
"disable_data_sync": false,
|
||||
"recent_vulnerability_max_age": "0s",
|
||||
"disable_win_os_vulnerabilities": false
|
||||
},
|
||||
"license": {
|
||||
"tier": "free",
|
||||
"expiration": "0001-01-01T00:00:00Z"
|
||||
},
|
||||
"logging": {
|
||||
"debug": true,
|
||||
"json": false,
|
||||
"result": {
|
||||
"plugin": "filesystem",
|
||||
"config": {
|
||||
"enable_log_compression": false,
|
||||
"enable_log_rotation": false,
|
||||
"result_log_file": "/dev/null",
|
||||
"status_log_file": "/dev/null",
|
||||
"audit_log_file": "/dev/null",
|
||||
"max_size": 500,
|
||||
"max_age": 0,
|
||||
"max_backups": 0
|
||||
}
|
||||
},
|
||||
"status": {
|
||||
"plugin": "filesystem",
|
||||
"config": {
|
||||
"enable_log_compression": false,
|
||||
"enable_log_rotation": false,
|
||||
"result_log_file": "/dev/null",
|
||||
"status_log_file": "/dev/null",
|
||||
"audit_log_file": "/dev/null",
|
||||
"max_size": 500,
|
||||
"max_age": 0,
|
||||
"max_backups": 0
|
||||
}
|
||||
},
|
||||
"audit": {
|
||||
"plugin": "filesystem",
|
||||
"config": {
|
||||
"enable_log_compression": false,
|
||||
"enable_log_rotation": false,
|
||||
"result_log_file": "/dev/null",
|
||||
"status_log_file": "/dev/null",
|
||||
"audit_log_file": "/dev/null",
|
||||
"max_size": 500,
|
||||
"max_age": 0,
|
||||
"max_backups": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -27,6 +27,9 @@ spec:
|
||||
macos_updates:
|
||||
minimum_version: null
|
||||
deadline: null
|
||||
windows_updates:
|
||||
deadline_days: 7
|
||||
grace_period_days: 3
|
||||
macos_settings:
|
||||
custom_settings:
|
||||
macos_setup:
|
||||
|
@ -29,6 +29,10 @@
|
||||
"minimum_version": null,
|
||||
"deadline": null
|
||||
},
|
||||
"windows_updates": {
|
||||
"deadline_days": null,
|
||||
"grace_period_days": null
|
||||
},
|
||||
"macos_settings": {
|
||||
"custom_settings": null
|
||||
},
|
||||
@ -93,6 +97,10 @@
|
||||
"minimum_version": "12.3.1",
|
||||
"deadline": "2021-12-14"
|
||||
},
|
||||
"windows_updates": {
|
||||
"deadline_days": 7,
|
||||
"grace_period_days": 3
|
||||
},
|
||||
"macos_settings": {
|
||||
"custom_settings": null
|
||||
},
|
||||
|
@ -11,6 +11,9 @@ spec:
|
||||
macos_updates:
|
||||
minimum_version: null
|
||||
deadline: null
|
||||
windows_updates:
|
||||
deadline_days: null
|
||||
grace_period_days: null
|
||||
macos_settings:
|
||||
custom_settings:
|
||||
windows_settings:
|
||||
@ -43,6 +46,9 @@ spec:
|
||||
macos_updates:
|
||||
minimum_version: "12.3.1"
|
||||
deadline: "2021-12-14"
|
||||
windows_updates:
|
||||
deadline_days: 7
|
||||
grace_period_days: 3
|
||||
macos_settings:
|
||||
custom_settings:
|
||||
windows_settings:
|
||||
|
@ -35,6 +35,9 @@ spec:
|
||||
macos_updates:
|
||||
deadline: null
|
||||
minimum_version: null
|
||||
windows_updates:
|
||||
deadline_days: null
|
||||
grace_period_days: null
|
||||
end_user_authentication:
|
||||
idp_name: ""
|
||||
issuer_uri: ""
|
||||
|
@ -35,6 +35,9 @@ spec:
|
||||
macos_updates:
|
||||
deadline: null
|
||||
minimum_version: null
|
||||
windows_updates:
|
||||
deadline_days: null
|
||||
grace_period_days: null
|
||||
end_user_authentication:
|
||||
idp_name: ""
|
||||
issuer_uri: ""
|
||||
|
@ -19,6 +19,9 @@ spec:
|
||||
macos_updates:
|
||||
deadline: null
|
||||
minimum_version: null
|
||||
windows_updates:
|
||||
deadline_days: null
|
||||
grace_period_days: null
|
||||
scripts: null
|
||||
name: tm1
|
||||
---
|
||||
@ -41,5 +44,8 @@ spec:
|
||||
macos_updates:
|
||||
deadline: null
|
||||
minimum_version: null
|
||||
windows_updates:
|
||||
deadline_days: null
|
||||
grace_period_days: null
|
||||
scripts: null
|
||||
name: tm2
|
||||
|
@ -19,6 +19,9 @@ spec:
|
||||
macos_updates:
|
||||
deadline: null
|
||||
minimum_version: null
|
||||
windows_updates:
|
||||
deadline_days: null
|
||||
grace_period_days: null
|
||||
scripts: null
|
||||
name: tm1
|
||||
---
|
||||
@ -41,5 +44,8 @@ spec:
|
||||
macos_updates:
|
||||
deadline: null
|
||||
minimum_version: null
|
||||
windows_updates:
|
||||
deadline_days: null
|
||||
grace_period_days: null
|
||||
scripts: null
|
||||
name: tm2
|
||||
|
@ -17,6 +17,9 @@ spec:
|
||||
macos_updates:
|
||||
deadline: null
|
||||
minimum_version: null
|
||||
windows_updates:
|
||||
deadline_days: null
|
||||
grace_period_days: null
|
||||
windows_settings:
|
||||
custom_settings: null
|
||||
scripts: null
|
||||
|
@ -23,6 +23,7 @@ import (
|
||||
"github.com/fleetdm/fleet/v4/server/fleet"
|
||||
apple_mdm "github.com/fleetdm/fleet/v4/server/mdm/apple"
|
||||
"github.com/fleetdm/fleet/v4/server/mdm/apple/mobileconfig"
|
||||
microsoft_mdm "github.com/fleetdm/fleet/v4/server/mdm/microsoft"
|
||||
"github.com/fleetdm/fleet/v4/server/sso"
|
||||
"github.com/fleetdm/fleet/v4/server/worker"
|
||||
kitlog "github.com/go-kit/kit/log"
|
||||
@ -1020,3 +1021,30 @@ func (svc *Service) GetMDMDiskEncryptionSummary(ctx context.Context, teamID *uin
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (svc *Service) mdmWindowsEnableOSUpdates(ctx context.Context, teamID *uint, updates fleet.WindowsUpdates) error {
|
||||
var contents bytes.Buffer
|
||||
params := windowsOSUpdatesProfileOptions{
|
||||
Deadline: updates.DeadlineDays.Value,
|
||||
GracePeriod: updates.GracePeriodDays.Value,
|
||||
}
|
||||
if err := windowsOSUpdatesProfileTemplate.Execute(&contents, params); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "enabling Windows OS updates")
|
||||
}
|
||||
|
||||
err := svc.ds.SetOrUpdateMDMWindowsConfigProfile(ctx, fleet.MDMWindowsConfigProfile{
|
||||
TeamID: teamID,
|
||||
Name: microsoft_mdm.FleetWindowsOSUpdatesProfileName,
|
||||
SyncML: contents.Bytes(),
|
||||
})
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "create Windows OS updates profile")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (svc *Service) mdmWindowsDisableOSUpdates(ctx context.Context, teamID *uint) error {
|
||||
err := svc.ds.DeleteMDMWindowsConfigProfileByTeamAndName(ctx, teamID, microsoft_mdm.FleetWindowsOSUpdatesProfileName)
|
||||
return ctxerr.Wrap(ctx, err, "delete Windows OS updates profile")
|
||||
}
|
||||
|
@ -91,8 +91,82 @@ var fileVaultProfileTemplate = template.Must(template.New("").Option("missingkey
|
||||
</dict>
|
||||
</plist>`))
|
||||
|
||||
// TODO(mna): we have a potential issue here with profile names - we need to
|
||||
// make sure they are unique for a given team, but there is no validation of
|
||||
// Fleet-reserved profile names, only of identifiers. A user could create a
|
||||
// "Disk encryption" profile for a custom profile, and then later on Fleet
|
||||
// would fail to enable disk encryption. See https://github.com/fleetdm/fleet/issues/15133.
|
||||
type windowsOSUpdatesProfileOptions struct {
|
||||
Deadline int
|
||||
GracePeriod int
|
||||
}
|
||||
|
||||
var windowsOSUpdatesProfileTemplate = template.Must(template.New("").Option("missingkey=error").Parse(`
|
||||
<Replace>
|
||||
<Item>
|
||||
<Target>
|
||||
<LocURI>./Device/Vendor/MSFT/Policy/Config/Update/ConfigureDeadlineForFeatureUpdates</LocURI>
|
||||
</Target>
|
||||
<Meta>
|
||||
<Type xmlns="syncml:metinf">text/plain</Type>
|
||||
<Format xmlns="syncml:metinf">int</Format>
|
||||
</Meta>
|
||||
<Data>{{ .Deadline }}</Data>
|
||||
</Item>
|
||||
</Replace>
|
||||
<Replace>
|
||||
<Item>
|
||||
<Target>
|
||||
<LocURI>./Device/Vendor/MSFT/Policy/Config/Update/ConfigureDeadlineForQualityUpdates</LocURI>
|
||||
</Target>
|
||||
<Meta>
|
||||
<Type xmlns="syncml:metinf">text/plain</Type>
|
||||
<Format xmlns="syncml:metinf">int</Format>
|
||||
</Meta>
|
||||
<Data>{{ .Deadline }}</Data>
|
||||
</Item>
|
||||
</Replace>
|
||||
<Replace>
|
||||
<Item>
|
||||
<Target>
|
||||
<LocURI>./Device/Vendor/MSFT/Policy/Config/Update/ConfigureDeadlineGracePeriod</LocURI>
|
||||
</Target>
|
||||
<Meta>
|
||||
<Type xmlns="syncml:metinf">text/plain</Type>
|
||||
<Format xmlns="syncml:metinf">int</Format>
|
||||
</Meta>
|
||||
<Data>{{ .GracePeriod }}</Data>
|
||||
</Item>
|
||||
</Replace>
|
||||
<Replace>
|
||||
<Item>
|
||||
<Target>
|
||||
<LocURI>./Device/Vendor/MSFT/Policy/Config/Update/AllowAutoUpdate</LocURI>
|
||||
</Target>
|
||||
<Meta>
|
||||
<Type xmlns="syncml:metinf">text/plain</Type>
|
||||
<Format xmlns="syncml:metinf">int</Format>
|
||||
</Meta>
|
||||
<Data>1</Data>
|
||||
</Item>
|
||||
</Replace>
|
||||
<Replace>
|
||||
<Item>
|
||||
<Target>
|
||||
<LocURI>./Device/Vendor/MSFT/Policy/Config/Update/SetDisablePauseUXAccess</LocURI>
|
||||
</Target>
|
||||
<Meta>
|
||||
<Type xmlns="syncml:metinf">text/plain</Type>
|
||||
<Format xmlns="syncml:metinf">int</Format>
|
||||
</Meta>
|
||||
<Data>1</Data>
|
||||
</Item>
|
||||
</Replace>
|
||||
<Replace>
|
||||
<Item>
|
||||
<Target>
|
||||
<LocURI>./Device/Vendor/MSFT/Policy/Config/Update/ConfigureDeadlineNoAutoReboot</LocURI>
|
||||
</Target>
|
||||
<Meta>
|
||||
<Type xmlns="syncml:metinf">text/plain</Type>
|
||||
<Format xmlns="syncml:metinf">int</Format>
|
||||
</Meta>
|
||||
<Data>1</Data>
|
||||
</Item>
|
||||
</Replace>
|
||||
`))
|
||||
|
@ -74,6 +74,8 @@ func NewService(
|
||||
DeleteMDMAppleSetupAssistant: eeservice.DeleteMDMAppleSetupAssistant,
|
||||
MDMAppleSyncDEPProfiles: eeservice.mdmAppleSyncDEPProfiles,
|
||||
DeleteMDMAppleBootstrapPackage: eeservice.DeleteMDMAppleBootstrapPackage,
|
||||
MDMWindowsEnableOSUpdates: eeservice.mdmWindowsEnableOSUpdates,
|
||||
MDMWindowsDisableOSUpdates: eeservice.mdmWindowsDisableOSUpdates,
|
||||
})
|
||||
|
||||
return eeservice, nil
|
||||
|
@ -137,7 +137,7 @@ func (svc *Service) ModifyTeam(ctx context.Context, teamID uint, payload fleet.T
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var macOSMinVersionUpdated, macOSDiskEncryptionUpdated, macOSEnableEndUserAuthUpdated bool
|
||||
var macOSMinVersionUpdated, windowsUpdatesUpdated, macOSDiskEncryptionUpdated, macOSEnableEndUserAuthUpdated bool
|
||||
if payload.MDM != nil {
|
||||
if payload.MDM.MacOSUpdates != nil {
|
||||
if err := payload.MDM.MacOSUpdates.Validate(); err != nil {
|
||||
@ -150,6 +150,16 @@ func (svc *Service) ModifyTeam(ctx context.Context, teamID uint, payload fleet.T
|
||||
}
|
||||
}
|
||||
|
||||
if payload.MDM.WindowsUpdates != nil {
|
||||
if err := payload.MDM.WindowsUpdates.Validate(); err != nil {
|
||||
return nil, fleet.NewInvalidArgumentError("windows_updates", err.Error())
|
||||
}
|
||||
if payload.MDM.WindowsUpdates.DeadlineDays.Set || payload.MDM.WindowsUpdates.GracePeriodDays.Set {
|
||||
windowsUpdatesUpdated = !team.Config.MDM.WindowsUpdates.Equal(*payload.MDM.WindowsUpdates)
|
||||
team.Config.MDM.WindowsUpdates = *payload.MDM.WindowsUpdates
|
||||
}
|
||||
}
|
||||
|
||||
if payload.MDM.EnableDiskEncryption.Valid {
|
||||
macOSDiskEncryptionUpdated = team.Config.MDM.EnableDiskEncryption != payload.MDM.EnableDiskEncryption.Value
|
||||
if macOSDiskEncryptionUpdated && !appCfg.MDM.EnabledAndConfigured {
|
||||
@ -223,6 +233,36 @@ func (svc *Service) ModifyTeam(ctx context.Context, teamID uint, payload fleet.T
|
||||
return nil, ctxerr.Wrap(ctx, err, "create activity for team macos min version edited")
|
||||
}
|
||||
}
|
||||
if windowsUpdatesUpdated {
|
||||
var deadline, grace *int
|
||||
if team.Config.MDM.WindowsUpdates.DeadlineDays.Valid {
|
||||
deadline = &team.Config.MDM.WindowsUpdates.DeadlineDays.Value
|
||||
}
|
||||
if team.Config.MDM.WindowsUpdates.GracePeriodDays.Valid {
|
||||
grace = &team.Config.MDM.WindowsUpdates.GracePeriodDays.Value
|
||||
}
|
||||
|
||||
if deadline != nil {
|
||||
if err := svc.mdmWindowsEnableOSUpdates(ctx, &team.ID, team.Config.MDM.WindowsUpdates); err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "enable team windows OS updates")
|
||||
}
|
||||
} else if err := svc.mdmWindowsDisableOSUpdates(ctx, &team.ID); err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "disable team windows OS updates")
|
||||
}
|
||||
|
||||
if err := svc.ds.NewActivity(
|
||||
ctx,
|
||||
authz.UserFromContext(ctx),
|
||||
fleet.ActivityTypeEditedWindowsUpdates{
|
||||
TeamID: &team.ID,
|
||||
TeamName: &team.Name,
|
||||
DeadlineDays: deadline,
|
||||
GracePeriodDays: grace,
|
||||
},
|
||||
); err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "create activity for team macos min version edited")
|
||||
}
|
||||
}
|
||||
if macOSDiskEncryptionUpdated {
|
||||
var act fleet.ActivityDetails
|
||||
if team.Config.MDM.EnableDiskEncryption {
|
||||
@ -710,6 +750,9 @@ func (svc *Service) ApplyTeamSpecs(ctx context.Context, specs []*fleet.TeamSpec,
|
||||
if err := spec.MDM.MacOSUpdates.Validate(); err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("macos_updates", err.Error()))
|
||||
}
|
||||
if err := spec.MDM.WindowsUpdates.Validate(); err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("windows_updates", err.Error()))
|
||||
}
|
||||
|
||||
if create {
|
||||
|
||||
@ -826,6 +869,7 @@ func (svc *Service) createTeamFromSpec(
|
||||
MDM: fleet.TeamMDM{
|
||||
EnableDiskEncryption: enableDiskEncryption,
|
||||
MacOSUpdates: spec.MDM.MacOSUpdates,
|
||||
WindowsUpdates: spec.MDM.WindowsUpdates,
|
||||
MacOSSettings: macOSSettings,
|
||||
MacOSSetup: macOSSetup,
|
||||
},
|
||||
@ -883,6 +927,9 @@ func (svc *Service) editTeamFromSpec(
|
||||
if spec.MDM.MacOSUpdates.Deadline.Set || spec.MDM.MacOSUpdates.MinimumVersion.Set {
|
||||
team.Config.MDM.MacOSUpdates = spec.MDM.MacOSUpdates
|
||||
}
|
||||
if spec.MDM.WindowsUpdates.DeadlineDays.Set || spec.MDM.WindowsUpdates.GracePeriodDays.Set {
|
||||
team.Config.MDM.WindowsUpdates = spec.MDM.WindowsUpdates
|
||||
}
|
||||
|
||||
oldEnableDiskEncryption := team.Config.MDM.EnableDiskEncryption
|
||||
if err := svc.applyTeamMacOSSettings(ctx, spec, &team.Config.MDM.MacOSSettings); err != nil {
|
||||
@ -913,6 +960,9 @@ func (svc *Service) editTeamFromSpec(
|
||||
didUpdateBootstrapPackage = oldMacOSSetup.BootstrapPackage.Value != spec.MDM.MacOSSetup.BootstrapPackage.Value
|
||||
team.Config.MDM.MacOSSetup.BootstrapPackage = spec.MDM.MacOSSetup.BootstrapPackage
|
||||
}
|
||||
// TODO(mna): doesn't look like we create an activity for macos updates when
|
||||
// modified via spec? Doing the same for Windows, but should we?
|
||||
|
||||
if !appCfg.MDM.EnabledAndConfigured &&
|
||||
((didUpdateSetupAssistant && team.Config.MDM.MacOSSetup.MacOSSetupAssistant.Value != "") ||
|
||||
(didUpdateBootstrapPackage && team.Config.MDM.MacOSSetup.BootstrapPackage.Value != "")) {
|
||||
|
@ -150,6 +150,10 @@ const DEFAULT_CONFIG_MOCK: IConfig = {
|
||||
mode: "",
|
||||
webhook_url: "",
|
||||
},
|
||||
windows_updates: {
|
||||
deadline_days: null,
|
||||
grace_period_days: null,
|
||||
},
|
||||
end_user_authentication: {
|
||||
entity_id: "",
|
||||
issuer_uri: "",
|
||||
|
@ -9,6 +9,8 @@ const baseClass = "data-error";
|
||||
interface IDataErrorProps {
|
||||
/** the description text displayed under the header */
|
||||
description?: string;
|
||||
/** Excludes the link that asks user to create an issue. Defaults to `false` */
|
||||
excludeIssueLink?: boolean;
|
||||
children?: React.ReactNode;
|
||||
card?: boolean;
|
||||
className?: string;
|
||||
@ -18,6 +20,7 @@ const DEFAULT_DESCRIPTION = "Refresh the page or log in again.";
|
||||
|
||||
const DataError = ({
|
||||
description = DEFAULT_DESCRIPTION,
|
||||
excludeIssueLink = false,
|
||||
children,
|
||||
card,
|
||||
className,
|
||||
@ -37,14 +40,16 @@ const DataError = ({
|
||||
{children || (
|
||||
<>
|
||||
<span className="info__data">{description}</span>
|
||||
<span className="info__data">
|
||||
If this keeps happening, please
|
||||
<CustomLink
|
||||
url="https://github.com/fleetdm/fleet/issues/new/choose"
|
||||
text="file an issue"
|
||||
newTab
|
||||
/>
|
||||
</span>
|
||||
{!excludeIssueLink && (
|
||||
<span className="info__data">
|
||||
If this keeps happening, please
|
||||
<CustomLink
|
||||
url="https://github.com/fleetdm/fleet/issues/new/choose"
|
||||
text="file an issue"
|
||||
newTab
|
||||
/>
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
|
@ -1,5 +1,8 @@
|
||||
import React from "react";
|
||||
import { Meta, StoryObj } from "@storybook/react";
|
||||
|
||||
import LastUpdatedText from "components/LastUpdatedText";
|
||||
|
||||
import SectionHeader from ".";
|
||||
|
||||
const meta: Meta<typeof SectionHeader> = {
|
||||
@ -13,3 +16,14 @@ export default meta;
|
||||
type Story = StoryObj<typeof SectionHeader>;
|
||||
|
||||
export const Basic: Story = {};
|
||||
|
||||
export const WithSubTitle: Story = {
|
||||
args: {
|
||||
subTitle: (
|
||||
<LastUpdatedText
|
||||
lastUpdatedAt={new Date().toISOString()}
|
||||
whatToRetrieve={"operating systems"}
|
||||
/>
|
||||
),
|
||||
},
|
||||
};
|
||||
|
@ -4,10 +4,16 @@ const baseClass = "section-header";
|
||||
|
||||
interface ISectionHeaderProps {
|
||||
title: string;
|
||||
subTitle?: React.ReactNode;
|
||||
}
|
||||
|
||||
const SectionHeader = ({ title }: ISectionHeaderProps) => {
|
||||
return <h2 className={baseClass}>{title}</h2>;
|
||||
const SectionHeader = ({ title, subTitle }: ISectionHeaderProps) => {
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
<h2>{title}</h2>
|
||||
{subTitle && <div className={`${baseClass}__sub-title`}>{subTitle}</div>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SectionHeader;
|
||||
|
@ -1,8 +1,14 @@
|
||||
.section-header {
|
||||
margin: 0 0 $pad-large;
|
||||
padding-bottom: $pad-small;
|
||||
font-size: $medium;
|
||||
font-weight: $regular;
|
||||
color: $core-fleet-black;
|
||||
border-bottom: solid 1px $ui-fleet-black-10;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $pad-small;
|
||||
padding-bottom: $pad-medium;
|
||||
border-bottom: 1px solid $ui-fleet-black-10;
|
||||
margin-bottom: $pad-xxlarge;
|
||||
|
||||
h2 {
|
||||
margin: 0;
|
||||
font-weight: normal;
|
||||
font-size: 18px; // TODO: update font variables to include 18px;
|
||||
}
|
||||
}
|
||||
|
@ -56,6 +56,7 @@ export enum ActivityType {
|
||||
AddedScript = "added_script",
|
||||
DeletedScript = "deleted_script",
|
||||
EditedScript = "edited_script",
|
||||
EditedWindowsUpdates = "edited_windows_updates",
|
||||
}
|
||||
export interface IActivity {
|
||||
created_at: string;
|
||||
@ -101,4 +102,6 @@ export interface IActivityDetails {
|
||||
name?: string;
|
||||
script_execution_id?: string;
|
||||
script_name?: string;
|
||||
deadline_days?: number;
|
||||
grace_period_days?: number;
|
||||
}
|
||||
|
@ -37,8 +37,8 @@ export interface IMdmConfig {
|
||||
windows_enabled_and_configured: boolean;
|
||||
end_user_authentication: IEndUserAuthentication;
|
||||
macos_updates: {
|
||||
minimum_version: string;
|
||||
deadline: string;
|
||||
minimum_version: string | null;
|
||||
deadline: string | null;
|
||||
};
|
||||
macos_settings: {
|
||||
custom_settings: null;
|
||||
@ -50,6 +50,10 @@ export interface IMdmConfig {
|
||||
macos_setup_assistant: string | null;
|
||||
};
|
||||
macos_migration: IMacOsMigrationSettings;
|
||||
windows_updates: {
|
||||
deadline_days: number | null;
|
||||
grace_period_days: number | null;
|
||||
};
|
||||
}
|
||||
|
||||
export interface IDeviceGlobalConfig {
|
||||
|
@ -46,8 +46,8 @@ export interface ITeam extends ITeamSummary {
|
||||
mdm?: {
|
||||
enable_disk_encryption: boolean;
|
||||
macos_updates: {
|
||||
minimum_version: string;
|
||||
deadline: string;
|
||||
minimum_version: string | null;
|
||||
deadline: string | null;
|
||||
};
|
||||
macos_settings: {
|
||||
custom_settings: null; // TODO: types?
|
||||
@ -58,6 +58,10 @@ export interface ITeam extends ITeamSummary {
|
||||
enable_end_user_authentication: boolean;
|
||||
macos_setup_assistant: string | null; // TODO: types?
|
||||
};
|
||||
windows_updates: {
|
||||
deadline_days: number | null;
|
||||
grace_period_days: number | null;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -675,6 +675,27 @@ const TAGGED_TEMPLATES = {
|
||||
</>
|
||||
);
|
||||
},
|
||||
editedWindowsUpdates: (activity: IActivity) => {
|
||||
return (
|
||||
<>
|
||||
{" "}
|
||||
updated the Windows OS update options (
|
||||
<b>
|
||||
Deadline: {activity.details?.deadline_days} days / Grace period:{" "}
|
||||
{activity.details?.grace_period_days} days
|
||||
</b>
|
||||
) on hosts assigned to{" "}
|
||||
{activity.details?.team_name ? (
|
||||
<>
|
||||
the <b>{activity.details.team_name}</b> team
|
||||
</>
|
||||
) : (
|
||||
"no team"
|
||||
)}
|
||||
.
|
||||
</>
|
||||
);
|
||||
},
|
||||
deletedMultipleSavedQuery: (activity: IActivity) => {
|
||||
return <> deleted multiple queries.</>;
|
||||
},
|
||||
@ -812,6 +833,9 @@ const getDetail = (
|
||||
case ActivityType.EditedScript: {
|
||||
return TAGGED_TEMPLATES.editedScript(activity);
|
||||
}
|
||||
case ActivityType.EditedWindowsUpdates: {
|
||||
return TAGGED_TEMPLATES.editedWindowsUpdates(activity);
|
||||
}
|
||||
case ActivityType.DeletedMultipleSavedQuery: {
|
||||
return TAGGED_TEMPLATES.deletedMultipleSavedQuery(activity);
|
||||
}
|
||||
|
@ -8,6 +8,7 @@ import { NotificationContext } from "context/notification";
|
||||
import PATHS from "router/paths";
|
||||
|
||||
import CustomLink from "components/CustomLink";
|
||||
import SectionHeader from "components/SectionHeader";
|
||||
import Spinner from "components/Spinner";
|
||||
import DataError from "components/DataError";
|
||||
|
||||
@ -152,7 +153,7 @@ const CustomSettings = ({
|
||||
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
<h2>Custom settings</h2>
|
||||
<SectionHeader title="Custom settings" />
|
||||
<p className={`${baseClass}__description`}>
|
||||
Create and upload configuration profiles to apply custom settings.{" "}
|
||||
<CustomLink
|
||||
|
@ -1,14 +1,4 @@
|
||||
.custom-settings {
|
||||
|
||||
h2 {
|
||||
margin-top: 0;
|
||||
padding-bottom: $pad-small;
|
||||
font-size: $medium;
|
||||
font-weight: $regular;
|
||||
color: $core-fleet-black;
|
||||
border-bottom: solid 1px $ui-fleet-black-10;
|
||||
}
|
||||
|
||||
&__description {
|
||||
font-size: $x-small;
|
||||
margin: $pad-xxlarge 0;
|
||||
|
@ -13,6 +13,7 @@ import CustomLink from "components/CustomLink";
|
||||
import Checkbox from "components/forms/fields/Checkbox";
|
||||
import PremiumFeatureMessage from "components/PremiumFeatureMessage";
|
||||
import Spinner from "components/Spinner";
|
||||
import SectionHeader from "components/SectionHeader";
|
||||
|
||||
import DiskEncryptionTable from "./components/DiskEncryptionTable";
|
||||
|
||||
@ -114,7 +115,7 @@ const DiskEncryption = ({
|
||||
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
<h2>Disk encryption</h2>
|
||||
<SectionHeader title="Disk encryption" />
|
||||
{!isPremiumTier ? (
|
||||
<PremiumFeatureMessage
|
||||
className={`${baseClass}__premium-feature-message`}
|
||||
|
@ -1,14 +1,4 @@
|
||||
.disk-encryption {
|
||||
h2 {
|
||||
margin-top: 0;
|
||||
padding-bottom: $pad-small;
|
||||
margin-bottom: $pad-xxlarge;
|
||||
font-size: $medium;
|
||||
font-weight: $regular;
|
||||
color: $core-fleet-black;
|
||||
border-bottom: solid 1px $ui-fleet-black-10;
|
||||
}
|
||||
|
||||
&__premium-feature-message {
|
||||
margin-top: 80px;
|
||||
text-align: center;
|
||||
|
@ -1,48 +1,79 @@
|
||||
import React, { useContext } from "react";
|
||||
import React, { useContext, useEffect, useState } from "react";
|
||||
import { InjectedRouter } from "react-router";
|
||||
|
||||
import { IConfig } from "interfaces/config";
|
||||
import { AppContext } from "context/app";
|
||||
|
||||
import OperatingSystems from "pages/DashboardPage/cards/OperatingSystems";
|
||||
import useInfoCard from "pages/DashboardPage/components/InfoCard";
|
||||
|
||||
import PremiumFeatureMessage from "components/PremiumFeatureMessage";
|
||||
|
||||
import OsMinVersionForm from "./components/OsMinVersionForm";
|
||||
import NudgePreview from "./components/NudgePreview";
|
||||
import TurnOnMdmMessage from "../components/TurnOnMdmMessage/TurnOnMdmMessage";
|
||||
import CurrentVersionSection from "./components/CurrentVersionSection";
|
||||
import TargetSection from "./components/TargetSection";
|
||||
|
||||
export type OSUpdatesSupportedPlatform = "darwin" | "windows";
|
||||
|
||||
const baseClass = "os-updates";
|
||||
|
||||
const getSelectedPlatform = (
|
||||
appConfig: IConfig | null
|
||||
): OSUpdatesSupportedPlatform => {
|
||||
// We dont have the data ready yet so we default to mac.
|
||||
// This is usually when the users first comes to this page.
|
||||
if (appConfig === null) return "darwin";
|
||||
|
||||
// if the mac mdm is enable and configured we check the app config to see if
|
||||
// the mdm for mac is enabled. If it is, it does not matter if windows is
|
||||
// enabled and configured and we will always return "mac".
|
||||
return appConfig.mdm.enabled_and_configured ? "darwin" : "windows";
|
||||
};
|
||||
|
||||
interface IOSUpdates {
|
||||
router: InjectedRouter;
|
||||
teamIdForApi: number;
|
||||
teamIdForApi?: number;
|
||||
}
|
||||
|
||||
const OSUpdates = ({ router, teamIdForApi }: IOSUpdates) => {
|
||||
const { config, isPremiumTier } = useContext(AppContext);
|
||||
|
||||
const OperatingSystemCard = useInfoCard({
|
||||
title: "macOS versions",
|
||||
children: (
|
||||
<OperatingSystems
|
||||
currentTeamId={teamIdForApi}
|
||||
selectedPlatform="darwin"
|
||||
showTitle
|
||||
showDescription={false}
|
||||
includeNameColumn={false}
|
||||
setShowTitle={() => {
|
||||
return null;
|
||||
}}
|
||||
/>
|
||||
),
|
||||
});
|
||||
// the default platform is mac and we later update this value when we have
|
||||
// done more checks.
|
||||
const [
|
||||
selectedPlatform,
|
||||
setSelectedPlatform,
|
||||
] = useState<OSUpdatesSupportedPlatform>("darwin");
|
||||
|
||||
if (!config?.mdm.enabled_and_configured) {
|
||||
// we have to use useEffect here as we need to update our selected platform
|
||||
// state when the app config is updated. This is usually when we get the app
|
||||
// config response from the server and it is no longer `null`.
|
||||
useEffect(() => {
|
||||
setSelectedPlatform(getSelectedPlatform(config));
|
||||
}, [config]);
|
||||
|
||||
if (config === null || teamIdForApi === undefined) return null;
|
||||
|
||||
// mdm is not enabled for mac or windows.
|
||||
if (
|
||||
!config.mdm.enabled_and_configured &&
|
||||
!config.mdm.windows_enabled_and_configured
|
||||
) {
|
||||
return <TurnOnMdmMessage router={router} />;
|
||||
}
|
||||
|
||||
return isPremiumTier ? (
|
||||
// Not premium shows premium message
|
||||
if (!isPremiumTier) {
|
||||
return (
|
||||
<PremiumFeatureMessage
|
||||
className={`${baseClass}__premium-feature-message`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const handleSelectPlatform = (platform: OSUpdatesSupportedPlatform) => {
|
||||
setSelectedPlatform(platform);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
<p className={`${baseClass}__description`}>
|
||||
Remotely encourage the installation of macOS updates on hosts assigned
|
||||
@ -50,22 +81,17 @@ const OSUpdates = ({ router, teamIdForApi }: IOSUpdates) => {
|
||||
</p>
|
||||
<div className={`${baseClass}__content`}>
|
||||
<div className={`${baseClass}__form-table-content`}>
|
||||
<div className={`${baseClass}__os-versions-card`}>
|
||||
{OperatingSystemCard}
|
||||
</div>
|
||||
<div className={`${baseClass}__os-version-form`}>
|
||||
<OsMinVersionForm currentTeamId={teamIdForApi} key={teamIdForApi} />
|
||||
</div>
|
||||
<CurrentVersionSection currentTeamId={teamIdForApi} />
|
||||
<TargetSection
|
||||
currentTeamId={teamIdForApi}
|
||||
onSelectAccordionItem={handleSelectPlatform}
|
||||
/>
|
||||
</div>
|
||||
<div className={`${baseClass}__nudge-preview`}>
|
||||
<NudgePreview />
|
||||
<NudgePreview platform={selectedPlatform} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<PremiumFeatureMessage
|
||||
className={`${baseClass}__premium-feature-message`}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -9,13 +9,13 @@
|
||||
max-width: $break-xxl;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
justify-content: center;
|
||||
gap: $pad-xxlarge;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: $small;
|
||||
margin: 0;
|
||||
}
|
||||
&__form-table-content, &__nudge-preview {
|
||||
flex-grow: 1;
|
||||
max-width: 640px;
|
||||
}
|
||||
|
||||
&__os-versions-card {
|
||||
@ -30,5 +30,10 @@
|
||||
&__content {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
&__form-table-content,
|
||||
&__nudge-preview {
|
||||
max-width: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,99 @@
|
||||
import React from "react";
|
||||
import { useQuery } from "react-query";
|
||||
import { AxiosError } from "axios";
|
||||
|
||||
import { IOperatingSystemVersion } from "interfaces/operating_system";
|
||||
import {
|
||||
getOSVersions,
|
||||
IOSVersionsResponse,
|
||||
} from "services/entities/operating_systems";
|
||||
|
||||
import LastUpdatedText from "components/LastUpdatedText";
|
||||
import SectionHeader from "components/SectionHeader";
|
||||
import DataError from "components/DataError";
|
||||
|
||||
import OSVersionTable from "../OSVersionTable";
|
||||
import { OSUpdatesSupportedPlatform } from "../../OSUpdates";
|
||||
import OSVersionsEmptyState from "../OSVersionsEmptyState";
|
||||
|
||||
/** This overrides the `platform` attribute on IOperatingSystemVersion so that only our filtered platforms (currently
|
||||
* "darwin" and "windows") values are included */
|
||||
export type IFilteredOperatingSystemVersion = Omit<
|
||||
IOperatingSystemVersion,
|
||||
"platform"
|
||||
> & {
|
||||
platform: OSUpdatesSupportedPlatform;
|
||||
};
|
||||
|
||||
const baseClass = "os-updates-current-version-section";
|
||||
|
||||
interface ICurrentVersionSectionProps {
|
||||
currentTeamId: number;
|
||||
}
|
||||
|
||||
const CurrentVersionSection = ({
|
||||
currentTeamId,
|
||||
}: ICurrentVersionSectionProps) => {
|
||||
const { data, isError, isLoading: isLoadingOsVersions } = useQuery<
|
||||
IOSVersionsResponse,
|
||||
AxiosError
|
||||
>(["os_versions", currentTeamId], () => getOSVersions(), {
|
||||
retry: false,
|
||||
refetchOnWindowFocus: false,
|
||||
});
|
||||
|
||||
const generateSubTitleText = () => {
|
||||
return (
|
||||
<LastUpdatedText
|
||||
lastUpdatedAt={data?.counts_updated_at}
|
||||
whatToRetrieve={"operating systems"}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
if (!data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const renderTable = () => {
|
||||
if (isError) {
|
||||
return (
|
||||
<DataError
|
||||
description="Refresh the page to try again."
|
||||
excludeIssueLink
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (!data.os_versions) {
|
||||
return <OSVersionsEmptyState />;
|
||||
}
|
||||
|
||||
// We only want to show windows and mac versions atm.
|
||||
const filteredOSVersionData = data.os_versions.filter((osVersion) => {
|
||||
return (
|
||||
osVersion.platform === "windows" || osVersion.platform === "darwin"
|
||||
);
|
||||
}) as IFilteredOperatingSystemVersion[];
|
||||
|
||||
return (
|
||||
<OSVersionTable
|
||||
osVersionData={filteredOSVersionData}
|
||||
currentTeamId={currentTeamId}
|
||||
isLoading={isLoadingOsVersions}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
<SectionHeader
|
||||
title="Current versions"
|
||||
subTitle={generateSubTitleText()}
|
||||
/>
|
||||
{renderTable()}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CurrentVersionSection;
|
@ -0,0 +1,17 @@
|
||||
.os-updates-current-version-section {
|
||||
margin-bottom: $pad-xxlarge;
|
||||
|
||||
&__title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $pad-small;
|
||||
padding-bottom: $pad-medium;
|
||||
border-bottom: 1px solid $ui-fleet-black-10;
|
||||
margin-bottom: $pad-xxlarge;
|
||||
|
||||
h2 {
|
||||
font-weight: normal;
|
||||
font-size: 18px; // TODO: update font variables to include 18px;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1 @@
|
||||
export { default } from "./CurrentVersionSection";
|
@ -1,26 +1,25 @@
|
||||
import React, { useContext, useState } from "react";
|
||||
import { useQuery } from "react-query";
|
||||
import { isEmpty } from "lodash";
|
||||
import classnames from "classnames";
|
||||
|
||||
import { APP_CONTEXT_NO_TEAM_ID } from "interfaces/team";
|
||||
import { NotificationContext } from "context/notification";
|
||||
import { AppContext } from "context/app";
|
||||
import configAPI from "services/entities/config";
|
||||
import teamsAPI, { ILoadTeamResponse } from "services/entities/teams";
|
||||
import teamsAPI from "services/entities/teams";
|
||||
|
||||
// @ts-ignore
|
||||
import InputField from "components/forms/fields/InputField";
|
||||
import Button from "components/buttons/Button";
|
||||
import validatePresence from "components/forms/validators/validate_presence";
|
||||
import { APP_CONTEXT_NO_TEAM_ID } from "interfaces/team";
|
||||
|
||||
const baseClass = "os-min-version-form";
|
||||
const baseClass = "mac-os-target-form";
|
||||
|
||||
interface IMinOsVersionFormData {
|
||||
interface IMacOSTargetFormData {
|
||||
minOsVersion: string;
|
||||
deadline: string;
|
||||
}
|
||||
|
||||
interface IMinOsVersionFormErrors {
|
||||
interface IMacOSTargetFormErrors {
|
||||
minOsVersion?: string;
|
||||
deadline?: string;
|
||||
}
|
||||
@ -33,8 +32,8 @@ const validateDeadline = (value: string) => {
|
||||
return /^\d{4}-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])$/.test(value);
|
||||
};
|
||||
|
||||
const validateForm = (formData: IMinOsVersionFormData) => {
|
||||
const errors: IMinOsVersionFormErrors = {};
|
||||
const validateForm = (formData: IMacOSTargetFormData) => {
|
||||
const errors: IMacOSTargetFormErrors = {};
|
||||
|
||||
if (!validatePresence(formData.minOsVersion)) {
|
||||
errors.minOsVersion = "The minimum version is required.";
|
||||
@ -62,49 +61,32 @@ const createMdmConfigData = (minOsVersion: string, deadline: string) => {
|
||||
};
|
||||
};
|
||||
|
||||
interface IOsMinVersionForm {
|
||||
currentTeamId?: number;
|
||||
interface IMacOSTargetFormProps {
|
||||
currentTeamId: number;
|
||||
defaultMinOsVersion: string;
|
||||
defaultDeadline: string;
|
||||
inAccordion?: boolean;
|
||||
}
|
||||
|
||||
const OsMinVersionForm = ({
|
||||
currentTeamId = APP_CONTEXT_NO_TEAM_ID,
|
||||
}: IOsMinVersionForm) => {
|
||||
const MacOSTargetForm = ({
|
||||
currentTeamId,
|
||||
defaultMinOsVersion,
|
||||
defaultDeadline,
|
||||
inAccordion = false,
|
||||
}: IMacOSTargetFormProps) => {
|
||||
const { renderFlash } = useContext(NotificationContext);
|
||||
const { config } = useContext(AppContext);
|
||||
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [minOsVersion, setMinOsVersion] = useState(
|
||||
currentTeamId === APP_CONTEXT_NO_TEAM_ID
|
||||
? config?.mdm.macos_updates.minimum_version ?? ""
|
||||
: ""
|
||||
);
|
||||
const [deadline, setDeadline] = useState(
|
||||
currentTeamId === APP_CONTEXT_NO_TEAM_ID
|
||||
? config?.mdm.macos_updates.deadline ?? ""
|
||||
: ""
|
||||
);
|
||||
const [minOsVersion, setMinOsVersion] = useState(defaultMinOsVersion);
|
||||
const [deadline, setDeadline] = useState(defaultDeadline);
|
||||
const [minOsVersionError, setMinOsVersionError] = useState<
|
||||
string | undefined
|
||||
>();
|
||||
const [deadlineError, setDeadlineError] = useState<string | undefined>();
|
||||
|
||||
useQuery<ILoadTeamResponse, Error>(
|
||||
["apple mdm config", currentTeamId],
|
||||
|
||||
// NOTE: this method should never be called with 0 as we sure to have
|
||||
// a value for current team from the "enabled" option. We add it here
|
||||
// to fulfill correct typing.
|
||||
() => teamsAPI.load(currentTeamId || 0),
|
||||
{
|
||||
refetchOnWindowFocus: false,
|
||||
staleTime: Infinity,
|
||||
enabled: currentTeamId > APP_CONTEXT_NO_TEAM_ID,
|
||||
onSuccess: (data) => {
|
||||
setMinOsVersion(data.team?.mdm?.macos_updates?.minimum_version ?? "");
|
||||
setDeadline(data.team?.mdm?.macos_updates?.deadline ?? "");
|
||||
},
|
||||
}
|
||||
);
|
||||
const classNames = classnames(baseClass, {
|
||||
[`${baseClass}__accordion-form`]: inAccordion,
|
||||
});
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
@ -141,7 +123,7 @@ const OsMinVersionForm = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<form className={baseClass} onSubmit={handleSubmit}>
|
||||
<form className={classNames} onSubmit={handleSubmit}>
|
||||
<InputField
|
||||
label="Minimum version"
|
||||
tooltip="The end user sees the window until their macOS is at or above this version."
|
||||
@ -167,4 +149,4 @@ const OsMinVersionForm = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default OsMinVersionForm;
|
||||
export default MacOSTargetForm;
|
@ -0,0 +1,6 @@
|
||||
.mac-os-target-form {
|
||||
&__accordion-form {
|
||||
padding: $pad-large;
|
||||
background-color: $ui-fleet-blue-10;
|
||||
}
|
||||
}
|
@ -0,0 +1 @@
|
||||
export { default } from "./MacOSTargetForm";
|
@ -2,14 +2,19 @@ import React from "react";
|
||||
|
||||
import CustomLink from "components/CustomLink";
|
||||
|
||||
import OsUpdateScreenshot from "../../../../../../assets/images/nudge-screenshot.png";
|
||||
import { OSUpdatesSupportedPlatform } from "../../OSUpdates";
|
||||
|
||||
import MacOSUpdateScreenshot from "../../../../../../assets/images/nudge-screenshot.png";
|
||||
import WindowsUpdateScreenshot from "../../../../../../assets/images/windows-nudge-screenshot.png";
|
||||
|
||||
const baseClass = "nudge-preview";
|
||||
|
||||
const NudgePreview = () => {
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
<h2>End user experience</h2>
|
||||
interface INudgeDescriptionProps {
|
||||
platform: OSUpdatesSupportedPlatform;
|
||||
}
|
||||
const NudgeDescription = ({ platform }: INudgeDescriptionProps) => {
|
||||
return platform === "darwin" ? (
|
||||
<>
|
||||
<p>
|
||||
When a minimum version is saved, the end user sees the below window
|
||||
until their macOS version is at or above the minimum version.
|
||||
@ -20,11 +25,49 @@ const NudgePreview = () => {
|
||||
url="https://fleetdm.com/docs/using-fleet/mdm-macos-updates"
|
||||
newTab
|
||||
/>
|
||||
<img
|
||||
className={`${baseClass}__preview-img`}
|
||||
src={OsUpdateScreenshot}
|
||||
alt="OS update preview screenshot"
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<p>
|
||||
When a new Windows update is published, the update will be downloaded
|
||||
and installed automatically before 8am and after 5pm (end user’s local
|
||||
time). Before the deadline passes, users will be able to defer restarts.
|
||||
After the deadline passes restart will be forced regardless of active
|
||||
hours.
|
||||
</p>
|
||||
<CustomLink
|
||||
text="Learn more about Windows updates in Fleet"
|
||||
url="Links to: https://fleetdm.com/docs/using-fleet/mdm-windows-updates"
|
||||
newTab
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
type INudgeImageProps = INudgeDescriptionProps;
|
||||
|
||||
const NudgeImage = ({ platform }: INudgeImageProps) => {
|
||||
return (
|
||||
<img
|
||||
className={`${baseClass}__preview-img`}
|
||||
src={
|
||||
platform === "darwin" ? MacOSUpdateScreenshot : WindowsUpdateScreenshot
|
||||
}
|
||||
alt="OS update preview screenshot"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
interface INudgePreviewProps {
|
||||
platform: OSUpdatesSupportedPlatform;
|
||||
}
|
||||
|
||||
const NudgePreview = ({ platform }: INudgePreviewProps) => {
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
<h2>End user experience</h2>
|
||||
<NudgeDescription platform={platform} />
|
||||
<NudgeImage platform={platform} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -1,8 +1,11 @@
|
||||
.nudge-preview {
|
||||
box-sizing: border-box;
|
||||
background-color: $ui-off-white;
|
||||
border-radius: $border-radius;
|
||||
border: 1px solid $ui-fleet-black-10;
|
||||
padding: $pad-xxlarge;
|
||||
max-width: 640px;
|
||||
flex-grow: 1;
|
||||
|
||||
&__preview-img {
|
||||
margin-top: $pad-xxlarge;
|
||||
@ -11,4 +14,8 @@
|
||||
max-width: 540px;
|
||||
margin: 40px auto 0;
|
||||
}
|
||||
|
||||
@media (max-width: $break-md) {
|
||||
max-width: none;
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,23 @@
|
||||
import React from "react";
|
||||
|
||||
import Icon from "components/Icon";
|
||||
|
||||
import { OSUpdatesSupportedPlatform } from "../../OSUpdates";
|
||||
|
||||
const baseClass = "os-type-cell";
|
||||
|
||||
interface IOSTypeCellProps {
|
||||
platform: OSUpdatesSupportedPlatform;
|
||||
versionName: string;
|
||||
}
|
||||
|
||||
const OSTypeCell = ({ platform, versionName }: IOSTypeCellProps) => {
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
<Icon name={platform} />
|
||||
<span>{versionName}</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default OSTypeCell;
|
@ -0,0 +1,5 @@
|
||||
.os-type-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $pad-small;
|
||||
}
|
@ -0,0 +1 @@
|
||||
export { default } from "./OSTypeCell";
|
@ -0,0 +1,48 @@
|
||||
import React from "react";
|
||||
|
||||
import { IOperatingSystemVersion } from "interfaces/operating_system";
|
||||
|
||||
import TableContainer from "components/TableContainer";
|
||||
|
||||
import { generateTableHeaders } from "./OSVersionTableConfig";
|
||||
import OSVersionsEmptyState from "../OSVersionsEmptyState";
|
||||
|
||||
const baseClass = "os-version-table";
|
||||
|
||||
interface IOSVersionTableProps {
|
||||
osVersionData: IOperatingSystemVersion[];
|
||||
currentTeamId: number;
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
||||
const DEFAULT_SORT_HEADER = "hosts_count";
|
||||
const DEFAULT_SORT_DIRECTION = "desc";
|
||||
|
||||
const OSVersionTable = ({
|
||||
osVersionData,
|
||||
currentTeamId,
|
||||
isLoading,
|
||||
}: IOSVersionTableProps) => {
|
||||
const columns = generateTableHeaders(currentTeamId);
|
||||
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
<TableContainer
|
||||
columns={columns}
|
||||
data={osVersionData}
|
||||
isLoading={isLoading}
|
||||
resultsTitle=""
|
||||
emptyComponent={OSVersionsEmptyState}
|
||||
showMarkAllPages={false}
|
||||
isAllPagesSelected={false}
|
||||
defaultSortHeader={DEFAULT_SORT_HEADER}
|
||||
defaultSortDirection={DEFAULT_SORT_DIRECTION}
|
||||
disableTableHeader
|
||||
disableCount
|
||||
disablePagination
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default OSVersionTable;
|
@ -0,0 +1,85 @@
|
||||
import React from "react";
|
||||
|
||||
import { IOperatingSystemVersion } from "interfaces/operating_system";
|
||||
|
||||
import HeaderCell from "components/TableContainer/DataTable/HeaderCell";
|
||||
import ViewAllHostsLink from "components/ViewAllHostsLink";
|
||||
import TextCell from "components/TableContainer/DataTable/TextCell";
|
||||
|
||||
import OSTypeCell from "../OSTypeCell";
|
||||
import { IFilteredOperatingSystemVersion } from "../CurrentVersionSection/CurrentVersionSection";
|
||||
|
||||
interface IOSTypeCellProps {
|
||||
row: {
|
||||
original: IFilteredOperatingSystemVersion;
|
||||
};
|
||||
}
|
||||
|
||||
interface IHostCellProps {
|
||||
row: {
|
||||
original: IOperatingSystemVersion;
|
||||
};
|
||||
}
|
||||
|
||||
interface IHeaderProps {
|
||||
column: {
|
||||
title: string;
|
||||
isSortedDesc: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export const generateTableHeaders = (teamId: number) => {
|
||||
return [
|
||||
{
|
||||
title: "OS type",
|
||||
Header: "OS type",
|
||||
disableSortBy: true,
|
||||
accessor: "platform",
|
||||
Cell: ({ row }: IOSTypeCellProps) => (
|
||||
<OSTypeCell
|
||||
platform={row.original.platform}
|
||||
versionName={row.original.name_only}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "Version",
|
||||
Header: "Version",
|
||||
disableSortBy: true,
|
||||
accessor: "version",
|
||||
},
|
||||
{
|
||||
title: "Hosts",
|
||||
accessor: "hosts_count",
|
||||
disableSortBy: false,
|
||||
Header: (cellProps: IHeaderProps) => (
|
||||
<HeaderCell
|
||||
value={cellProps.column.title}
|
||||
isSortedDesc={cellProps.column.isSortedDesc}
|
||||
/>
|
||||
),
|
||||
Cell: ({ row }: IHostCellProps): JSX.Element => {
|
||||
const { hosts_count, name_only, version } = row.original;
|
||||
return (
|
||||
<span className="hosts-cell__wrapper">
|
||||
<span className="hosts-cell__count">
|
||||
<TextCell value={hosts_count} />
|
||||
</span>
|
||||
<span className="hosts-cell__link">
|
||||
<ViewAllHostsLink
|
||||
queryParams={{
|
||||
os_name: name_only,
|
||||
os_version: version,
|
||||
team_id: teamId,
|
||||
}}
|
||||
condensed
|
||||
className="os-hosts-link"
|
||||
/>
|
||||
</span>
|
||||
</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
};
|
@ -0,0 +1,18 @@
|
||||
.os-version-table {
|
||||
.hosts-cell__wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.os-hosts-link {
|
||||
opacity: 0;
|
||||
transition: 250ms;
|
||||
}
|
||||
|
||||
tr:hover {
|
||||
.os-hosts-link {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1 @@
|
||||
export { default } from "./OSVersionTable";
|
@ -0,0 +1,22 @@
|
||||
import React from "react";
|
||||
|
||||
import EmptyTable from "components/EmptyTable";
|
||||
|
||||
const baseClass = "os-versions-empty-state";
|
||||
|
||||
const OSVersionsEmptyState = () => {
|
||||
return (
|
||||
<EmptyTable
|
||||
className={`${baseClass}__empty-table`}
|
||||
header="No OS versions detected."
|
||||
info={
|
||||
<span>
|
||||
This report is updated every hour to protect
|
||||
<br /> the performance of your devices.
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default OSVersionsEmptyState;
|
@ -0,0 +1,3 @@
|
||||
.os-versions-empty-state {
|
||||
margin: 0 auto $pad-xxlarge;
|
||||
}
|
@ -0,0 +1 @@
|
||||
export { default } from "./OSVersionsEmptyState";
|
@ -1 +0,0 @@
|
||||
export { default } from "./OsMinVersionForm";
|
@ -0,0 +1,103 @@
|
||||
import React from "react";
|
||||
import {
|
||||
Accordion,
|
||||
AccordionItem,
|
||||
AccordionItemButton,
|
||||
AccordionItemHeading,
|
||||
AccordionItemPanel,
|
||||
AccordionItemState,
|
||||
} from "react-accessible-accordion";
|
||||
import classnames from "classnames";
|
||||
|
||||
import Icon from "components/Icon";
|
||||
|
||||
import MacOSTargetForm from "../MacOSTargetForm";
|
||||
import WindowsTargetForm from "../WindowsTargetForm";
|
||||
import { OSUpdatesSupportedPlatform } from "../../OSUpdates";
|
||||
|
||||
const baseClass = "platforms-accordion";
|
||||
|
||||
const generateIconClassNames = (expanded?: boolean) => {
|
||||
return classnames(`${baseClass}__item-icon`, {
|
||||
[`${baseClass}__item-closed`]: !expanded,
|
||||
});
|
||||
};
|
||||
|
||||
interface IPlatformsAccordionProps {
|
||||
currentTeamId: number;
|
||||
defaultMacOSVersion: string;
|
||||
defaultMacOSDeadline: string;
|
||||
defaultWindowsDeadlineDays: string;
|
||||
defaultWindowsGracePeriodDays: string;
|
||||
onSelectAccordionItem: (platform: OSUpdatesSupportedPlatform) => void;
|
||||
}
|
||||
|
||||
const PlatformsAccordion = ({
|
||||
currentTeamId,
|
||||
defaultMacOSDeadline,
|
||||
defaultMacOSVersion,
|
||||
defaultWindowsDeadlineDays,
|
||||
defaultWindowsGracePeriodDays,
|
||||
onSelectAccordionItem,
|
||||
}: IPlatformsAccordionProps) => {
|
||||
return (
|
||||
<Accordion
|
||||
className={`${baseClass}__accordion`}
|
||||
preExpanded={["mac"]}
|
||||
onChange={(selected) =>
|
||||
onSelectAccordionItem(selected[0] as OSUpdatesSupportedPlatform)
|
||||
}
|
||||
>
|
||||
<AccordionItem uuid="mac">
|
||||
<AccordionItemHeading>
|
||||
<AccordionItemButton className={`${baseClass}__accordion-button`}>
|
||||
<span>macOS</span>
|
||||
<AccordionItemState>
|
||||
{({ expanded }) => (
|
||||
<Icon
|
||||
name="chevron-up"
|
||||
className={generateIconClassNames(expanded)}
|
||||
/>
|
||||
)}
|
||||
</AccordionItemState>
|
||||
</AccordionItemButton>
|
||||
</AccordionItemHeading>
|
||||
<AccordionItemPanel className={`${baseClass}__accordion-panel`}>
|
||||
<MacOSTargetForm
|
||||
currentTeamId={currentTeamId}
|
||||
defaultMinOsVersion={defaultMacOSVersion}
|
||||
defaultDeadline={defaultMacOSDeadline}
|
||||
key={currentTeamId}
|
||||
inAccordion
|
||||
/>
|
||||
</AccordionItemPanel>
|
||||
</AccordionItem>
|
||||
<AccordionItem uuid="windows">
|
||||
<AccordionItemHeading>
|
||||
<AccordionItemButton className={`${baseClass}__accordion-button`}>
|
||||
<span>Windows</span>
|
||||
<AccordionItemState>
|
||||
{({ expanded }) => (
|
||||
<Icon
|
||||
name="chevron-up"
|
||||
className={generateIconClassNames(expanded)}
|
||||
/>
|
||||
)}
|
||||
</AccordionItemState>
|
||||
</AccordionItemButton>
|
||||
</AccordionItemHeading>
|
||||
<AccordionItemPanel className={`${baseClass}__accordion-panel`}>
|
||||
<WindowsTargetForm
|
||||
currentTeamId={currentTeamId}
|
||||
defaultDeadlineDays={defaultWindowsDeadlineDays}
|
||||
defaultGracePeriodDays={defaultWindowsGracePeriodDays}
|
||||
key={currentTeamId}
|
||||
inAccordion
|
||||
/>
|
||||
</AccordionItemPanel>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
);
|
||||
};
|
||||
|
||||
export default PlatformsAccordion;
|
@ -0,0 +1,32 @@
|
||||
.platforms-accordion {
|
||||
&__accordion {
|
||||
// this was an arbitrary min width to make sure the accordion stays the same
|
||||
// width regardless of which tab is open.
|
||||
min-width: 530px;
|
||||
}
|
||||
|
||||
&__accordion-button {
|
||||
padding: $pad-medium 0;
|
||||
border-bottom: 1px solid $ui-fleet-black-10;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
>span {
|
||||
font-weight: $bold;
|
||||
font-size: $x-small;
|
||||
}
|
||||
}
|
||||
|
||||
&__accordion-panel {
|
||||
border-bottom: 1px solid $ui-fleet-black-10;
|
||||
}
|
||||
|
||||
&__item-icon {
|
||||
transition: transform 0.25s ease;
|
||||
}
|
||||
|
||||
&__item-closed {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
@ -0,0 +1 @@
|
||||
export { default } from "./PlatformsAccordion";
|
@ -0,0 +1,155 @@
|
||||
import React, { useContext } from "react";
|
||||
import { useQuery } from "react-query";
|
||||
|
||||
import {
|
||||
API_NO_TEAM_ID,
|
||||
APP_CONTEXT_NO_TEAM_ID,
|
||||
ITeamConfig,
|
||||
} from "interfaces/team";
|
||||
import { IConfig } from "interfaces/config";
|
||||
import { AppContext } from "context/app";
|
||||
import teamsAPI, { ILoadTeamResponse } from "services/entities/teams";
|
||||
|
||||
import Spinner from "components/Spinner";
|
||||
import SectionHeader from "components/SectionHeader";
|
||||
|
||||
import MacOSTargetForm from "../MacOSTargetForm";
|
||||
import WindowsTargetForm from "../WindowsTargetForm";
|
||||
import PlatformsAccordion from "../PlatformsAccordion";
|
||||
import { OSUpdatesSupportedPlatform } from "../../OSUpdates";
|
||||
|
||||
const baseClass = "os-updates-target-section";
|
||||
|
||||
const getDefaultMacOSVersion = (
|
||||
currentTeam: number,
|
||||
appConfig: IConfig,
|
||||
teamConfig?: ITeamConfig
|
||||
) => {
|
||||
return currentTeam === API_NO_TEAM_ID
|
||||
? appConfig?.mdm.macos_updates.minimum_version ?? ""
|
||||
: teamConfig?.mdm?.macos_updates.minimum_version ?? "";
|
||||
};
|
||||
|
||||
const getDefaultMacOSDeadline = (
|
||||
currentTeam: number,
|
||||
appConfig: IConfig,
|
||||
teamConfig?: ITeamConfig
|
||||
) => {
|
||||
return currentTeam === API_NO_TEAM_ID
|
||||
? appConfig?.mdm.macos_updates.deadline || ""
|
||||
: teamConfig?.mdm?.macos_updates.deadline || "";
|
||||
};
|
||||
|
||||
const getDefaultWindowsDeadlineDays = (
|
||||
currentTeam: number,
|
||||
appConfig: IConfig,
|
||||
teamConfig?: ITeamConfig
|
||||
) => {
|
||||
return currentTeam === API_NO_TEAM_ID
|
||||
? appConfig.mdm.windows_updates.deadline_days?.toString() ?? ""
|
||||
: teamConfig?.mdm?.windows_updates.deadline_days?.toString() ?? "";
|
||||
};
|
||||
|
||||
const getDefaultWindowsGracePeriodDays = (
|
||||
currentTeam: number,
|
||||
appConfig: IConfig,
|
||||
teamConfig?: ITeamConfig
|
||||
) => {
|
||||
return currentTeam === API_NO_TEAM_ID
|
||||
? appConfig.mdm.windows_updates.grace_period_days?.toString() ?? ""
|
||||
: teamConfig?.mdm?.windows_updates.grace_period_days?.toString() ?? "";
|
||||
};
|
||||
|
||||
interface ITargetSectionProps {
|
||||
currentTeamId: number;
|
||||
onSelectAccordionItem: (platform: OSUpdatesSupportedPlatform) => void;
|
||||
}
|
||||
|
||||
const TargetSection = ({
|
||||
currentTeamId,
|
||||
onSelectAccordionItem,
|
||||
}: ITargetSectionProps) => {
|
||||
const { config } = useContext(AppContext);
|
||||
|
||||
// We make the call at this component as multiple children components need
|
||||
// this data.
|
||||
const { data: teamData, isLoading: isLoadingTeam, isError } = useQuery<
|
||||
ILoadTeamResponse,
|
||||
Error,
|
||||
ITeamConfig
|
||||
>(["team-config", currentTeamId], () => teamsAPI.load(currentTeamId), {
|
||||
refetchOnWindowFocus: false,
|
||||
enabled: currentTeamId > APP_CONTEXT_NO_TEAM_ID,
|
||||
select: (data) => data.team,
|
||||
});
|
||||
|
||||
if (!config) return null;
|
||||
|
||||
const isMacMdmEnabled = config.mdm.enabled_and_configured;
|
||||
const isWindowsMdmEnabled = config.mdm.windows_enabled_and_configured;
|
||||
|
||||
// Loading state rendering
|
||||
if (isLoadingTeam) {
|
||||
return <Spinner />;
|
||||
}
|
||||
|
||||
const defaultMacOSVersion = getDefaultMacOSVersion(
|
||||
currentTeamId,
|
||||
config,
|
||||
teamData
|
||||
);
|
||||
const defaultMacOSDeadline = getDefaultMacOSDeadline(
|
||||
currentTeamId,
|
||||
config,
|
||||
teamData
|
||||
);
|
||||
const defaultWindowsDeadlineDays = getDefaultWindowsDeadlineDays(
|
||||
currentTeamId,
|
||||
config,
|
||||
teamData
|
||||
);
|
||||
const defaultWindowsGracePeriodDays = getDefaultWindowsGracePeriodDays(
|
||||
currentTeamId,
|
||||
config,
|
||||
teamData
|
||||
);
|
||||
|
||||
const renderTargetForms = () => {
|
||||
if (isMacMdmEnabled && isWindowsMdmEnabled) {
|
||||
return (
|
||||
<PlatformsAccordion
|
||||
currentTeamId={currentTeamId}
|
||||
defaultMacOSVersion={defaultMacOSVersion}
|
||||
defaultMacOSDeadline={defaultMacOSDeadline}
|
||||
defaultWindowsDeadlineDays={defaultWindowsDeadlineDays}
|
||||
defaultWindowsGracePeriodDays={defaultWindowsGracePeriodDays}
|
||||
onSelectAccordionItem={onSelectAccordionItem}
|
||||
/>
|
||||
);
|
||||
} else if (isMacMdmEnabled) {
|
||||
return (
|
||||
<MacOSTargetForm
|
||||
currentTeamId={currentTeamId}
|
||||
defaultMinOsVersion={defaultMacOSVersion}
|
||||
defaultDeadline={defaultMacOSDeadline}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<WindowsTargetForm
|
||||
currentTeamId={currentTeamId}
|
||||
defaultDeadlineDays={defaultWindowsDeadlineDays}
|
||||
defaultGracePeriodDays={defaultWindowsGracePeriodDays}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
<SectionHeader title="Target" />
|
||||
{renderTargetForms()}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TargetSection;
|
@ -0,0 +1,15 @@
|
||||
.os-updates-target-section {
|
||||
&__title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $pad-small;
|
||||
padding-bottom: $pad-medium;
|
||||
border-bottom: 1px solid $ui-fleet-black-10;
|
||||
margin-bottom: $pad-xxlarge;
|
||||
|
||||
h2 {
|
||||
font-weight: normal;
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1 @@
|
||||
export { default } from "./TargetSection";
|
@ -0,0 +1,168 @@
|
||||
import React, { useContext, useState } from "react";
|
||||
import { isEmpty } from "lodash";
|
||||
import classnames from "classnames";
|
||||
|
||||
import { APP_CONTEXT_NO_TEAM_ID } from "interfaces/team";
|
||||
import { NotificationContext } from "context/notification";
|
||||
import configAPI from "services/entities/config";
|
||||
import teamsAPI from "services/entities/teams";
|
||||
|
||||
// @ts-ignore
|
||||
import InputField from "components/forms/fields/InputField";
|
||||
import Button from "components/buttons/Button";
|
||||
import validatePresence from "components/forms/validators/validate_presence";
|
||||
|
||||
const baseClass = "windows-target-form";
|
||||
|
||||
interface IWindowsTargetFormData {
|
||||
deadlineDays: string;
|
||||
gracePeriodDays: string;
|
||||
}
|
||||
|
||||
interface IWindowsTargetFormErrors {
|
||||
deadlineDays?: string;
|
||||
gracePeriodDays?: string;
|
||||
}
|
||||
|
||||
// validates that a string is a number from 0 to 30
|
||||
const validateDeadlineDays = (value: string) => {
|
||||
if (value === "") return false;
|
||||
|
||||
const parsedValue = Number(value);
|
||||
return Number.isInteger(parsedValue) && parsedValue >= 0 && parsedValue <= 30;
|
||||
};
|
||||
|
||||
// validates string is a number from 0 to 7
|
||||
const validateGracePeriodDays = (value: string) => {
|
||||
if (value === "") return false;
|
||||
|
||||
const parsedValue = Number(value);
|
||||
return Number.isInteger(parsedValue) && parsedValue >= 0 && parsedValue <= 7;
|
||||
};
|
||||
|
||||
const validateForm = (formData: IWindowsTargetFormData) => {
|
||||
const errors: IWindowsTargetFormErrors = {};
|
||||
|
||||
if (!validatePresence(formData.deadlineDays)) {
|
||||
errors.deadlineDays = "The deadline days is required.";
|
||||
} else if (!validateDeadlineDays(formData.deadlineDays)) {
|
||||
errors.deadlineDays = "Deadline must meet criteria below.";
|
||||
}
|
||||
|
||||
if (!validatePresence(formData.gracePeriodDays)) {
|
||||
errors.gracePeriodDays = "The grace period days is required.";
|
||||
} else if (!validateGracePeriodDays(formData.gracePeriodDays)) {
|
||||
errors.gracePeriodDays = "Grace period must meet criteria below.";
|
||||
}
|
||||
|
||||
return errors;
|
||||
};
|
||||
|
||||
const createMdmConfigData = (deadlineDays: string, gracePeriodDays: string) => {
|
||||
return {
|
||||
mdm: {
|
||||
windows_updates: {
|
||||
deadline_days: parseInt(deadlineDays, 10),
|
||||
grace_period_days: parseInt(gracePeriodDays, 10),
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
interface IWindowsTargetFormProps {
|
||||
currentTeamId: number;
|
||||
defaultDeadlineDays: string;
|
||||
defaultGracePeriodDays: string;
|
||||
inAccordion?: boolean;
|
||||
}
|
||||
|
||||
const WindowsTargetForm = ({
|
||||
currentTeamId,
|
||||
defaultDeadlineDays,
|
||||
defaultGracePeriodDays,
|
||||
inAccordion = false,
|
||||
}: IWindowsTargetFormProps) => {
|
||||
const { renderFlash } = useContext(NotificationContext);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [deadlineDays, setDeadlineDays] = useState(
|
||||
defaultDeadlineDays.toString()
|
||||
);
|
||||
const [gracePeriodDays, setGracePeriodDays] = useState(
|
||||
defaultGracePeriodDays.toString()
|
||||
);
|
||||
const [deadlineDaysError, setDeadlineDaysError] = useState<
|
||||
string | undefined
|
||||
>();
|
||||
const [gracePeriodDaysError, setGracePeriodDaysError] = useState<
|
||||
string | undefined
|
||||
>();
|
||||
|
||||
const classNames = classnames(baseClass, {
|
||||
[`${baseClass}__accordion-form`]: inAccordion,
|
||||
});
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
const errors = validateForm({
|
||||
deadlineDays,
|
||||
gracePeriodDays,
|
||||
});
|
||||
|
||||
setDeadlineDaysError(errors.deadlineDays);
|
||||
setGracePeriodDaysError(errors.gracePeriodDays);
|
||||
|
||||
if (isEmpty(errors)) {
|
||||
setIsSaving(true);
|
||||
const updateData = createMdmConfigData(deadlineDays, gracePeriodDays);
|
||||
try {
|
||||
currentTeamId === APP_CONTEXT_NO_TEAM_ID
|
||||
? await configAPI.update(updateData)
|
||||
: await teamsAPI.update(updateData, currentTeamId);
|
||||
renderFlash(
|
||||
"success",
|
||||
"Successfully updated Windows OS update options."
|
||||
);
|
||||
} catch {
|
||||
renderFlash("error", "Couldn’t update. Please try again.");
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeadlineDaysChange = (val: string) => {
|
||||
setDeadlineDays(val);
|
||||
};
|
||||
|
||||
const handleGracePeriodDays = (val: string) => {
|
||||
setGracePeriodDays(val);
|
||||
};
|
||||
|
||||
return (
|
||||
<form className={classNames} onSubmit={handleSubmit}>
|
||||
<InputField
|
||||
label="Deadline"
|
||||
tooltip="Number of days the end user has before updates are installed and the host is forced to restart."
|
||||
hint="Number of days from 0 to 30."
|
||||
placeholder="5"
|
||||
value={deadlineDays}
|
||||
error={deadlineDaysError}
|
||||
onChange={handleDeadlineDaysChange}
|
||||
/>
|
||||
<InputField
|
||||
label="Grace period"
|
||||
tooltip="Number of days after the deadline the end user has before the host is forced to restart (only if end user was offline when deadline passed)."
|
||||
hint="Number of days from 0 to 7."
|
||||
placeholder="2"
|
||||
value={gracePeriodDays}
|
||||
error={gracePeriodDaysError}
|
||||
onChange={handleGracePeriodDays}
|
||||
/>
|
||||
<Button type="submit" isLoading={isSaving}>
|
||||
Save
|
||||
</Button>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
export default WindowsTargetForm;
|
@ -0,0 +1,7 @@
|
||||
.windows-target-form {
|
||||
|
||||
&__accordion-form {
|
||||
padding: $pad-large;
|
||||
background-color: $ui-fleet-blue-10;
|
||||
}
|
||||
}
|
@ -0,0 +1 @@
|
||||
export { default } from "./WindowsTargetForm";
|
@ -8,6 +8,8 @@ import mdmAPI from "services/entities/mdm";
|
||||
import { NotificationContext } from "context/notification";
|
||||
|
||||
import Spinner from "components/Spinner";
|
||||
import SectionHeader from "components/SectionHeader";
|
||||
|
||||
import BootstrapPackagePreview from "./components/BootstrapPackagePreview";
|
||||
import PackageUploader from "./components/BootstrapPackageUploader";
|
||||
import UploadedPackageView from "./components/UploadedPackageView";
|
||||
@ -65,7 +67,7 @@ const BootstrapPackage = ({ currentTeamId }: IBootstrapPackageProps) => {
|
||||
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
<h2>Bootstrap package</h2>
|
||||
<SectionHeader title="Bootstrap package" />
|
||||
{isLoading ? (
|
||||
<Spinner />
|
||||
) : (
|
||||
|
@ -1,13 +1,4 @@
|
||||
.bootstrap-package {
|
||||
> h2 {
|
||||
margin: 0 0 $pad-large;
|
||||
padding-bottom: $pad-small;
|
||||
font-size: $medium;
|
||||
font-weight: $regular;
|
||||
color: $core-fleet-black;
|
||||
border-bottom: solid 1px $ui-fleet-black-10;
|
||||
}
|
||||
|
||||
&__content {
|
||||
max-width: $break-xxl;
|
||||
margin: 0 auto;
|
||||
|
@ -1,16 +1,17 @@
|
||||
import React from "react";
|
||||
import { InjectedRouter } from "react-router";
|
||||
import PATHS from "router/paths";
|
||||
import { useQuery } from "react-query";
|
||||
|
||||
import configAPI from "services/entities/config";
|
||||
import teamsAPI, { ILoadTeamResponse } from "services/entities/teams";
|
||||
import { IConfig, IMdmConfig } from "interfaces/config";
|
||||
import { ITeamConfig } from "interfaces/team";
|
||||
|
||||
import SectionHeader from "components/SectionHeader/SectionHeader";
|
||||
import EndUserExperiencePreview from "pages/ManageControlsPage/components/EndUserExperiencePreview";
|
||||
import { useQuery } from "react-query";
|
||||
import { ITeamConfig } from "interfaces/team";
|
||||
import Spinner from "components/Spinner";
|
||||
import EndUserExperiencePreview from "pages/ManageControlsPage/components/EndUserExperiencePreview";
|
||||
|
||||
import RequireEndUserAuth from "./components/RequireEndUserAuth/RequireEndUserAuth";
|
||||
import EndUserAuthForm from "./components/EndUserAuthForm/EndUserAuthForm";
|
||||
|
||||
|
@ -21,12 +21,13 @@ export interface IGetOSVersionsRequest {
|
||||
export interface IGetOSVersionsQueryKey extends IGetOSVersionsRequest {
|
||||
scope: string;
|
||||
}
|
||||
|
||||
export interface IOSVersionsResponse {
|
||||
counts_updated_at: string;
|
||||
os_versions: IOperatingSystemVersion[];
|
||||
}
|
||||
|
||||
export const getOSVersions = async ({
|
||||
export const getOSVersions = ({
|
||||
id,
|
||||
platform,
|
||||
teamId,
|
||||
|
@ -41,10 +41,14 @@ export interface IUpdateTeamFormData {
|
||||
webhook_settings: Partial<ITeamWebhookSettings>;
|
||||
integrations: IIntegrations;
|
||||
mdm: {
|
||||
macos_updates: {
|
||||
macos_updates?: {
|
||||
minimum_version: string;
|
||||
deadline: string;
|
||||
};
|
||||
windows_updates?: {
|
||||
deadline_days: number;
|
||||
grace_period_days: number;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -93,6 +93,45 @@ func (b *Bool) UnmarshalJSON(data []byte) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Int represents an optional integer value.
|
||||
type Int struct {
|
||||
Set bool
|
||||
Valid bool
|
||||
Value int
|
||||
}
|
||||
|
||||
func SetInt(v int) Int {
|
||||
return Int{Set: true, Valid: true, Value: v}
|
||||
}
|
||||
|
||||
func (i Int) MarshalJSON() ([]byte, error) {
|
||||
if !i.Valid {
|
||||
return []byte("null"), nil
|
||||
}
|
||||
return json.Marshal(i.Value)
|
||||
}
|
||||
|
||||
func (i *Int) UnmarshalJSON(data []byte) error {
|
||||
// If this method was called, the value was set.
|
||||
i.Set = true
|
||||
i.Valid = false
|
||||
|
||||
if bytes.Equal(data, []byte("null")) {
|
||||
// The key was set to null, blank the value
|
||||
i.Value = 0
|
||||
return nil
|
||||
}
|
||||
|
||||
// The key isn't set to null
|
||||
var v int
|
||||
if err := json.Unmarshal(data, &v); err != nil {
|
||||
return err
|
||||
}
|
||||
i.Value = v
|
||||
i.Valid = true
|
||||
return nil
|
||||
}
|
||||
|
||||
type Slice[T any] struct {
|
||||
Set bool
|
||||
Valid bool
|
||||
|
@ -90,7 +90,7 @@ func TestString(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestBool(t *testing.T) {
|
||||
t.Run("plain string", func(t *testing.T) {
|
||||
t.Run("plain bool", func(t *testing.T) {
|
||||
cases := []struct {
|
||||
data string
|
||||
wantErr string
|
||||
@ -170,6 +170,90 @@ func TestBool(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestInt(t *testing.T) {
|
||||
t.Run("plain int", func(t *testing.T) {
|
||||
cases := []struct {
|
||||
data string
|
||||
wantErr string
|
||||
wantRes Int
|
||||
marshalAs string
|
||||
}{
|
||||
{`1`, "", Int{Set: true, Valid: true, Value: 1}, `1`},
|
||||
{`-1`, "", Int{Set: true, Valid: true, Value: -1}, `-1`},
|
||||
{`0`, "", Int{Set: true, Valid: true, Value: 0}, `0`},
|
||||
{`1.23`, "cannot unmarshal number 1.23 into Go value of type int", Int{Set: true, Valid: false, Value: 0}, `null`},
|
||||
{`null`, "", Int{Set: true, Valid: false, Value: 0}, `null`},
|
||||
{`"x"`, "cannot unmarshal string into Go value of type int", Int{Set: true, Valid: false, Value: 0}, `null`},
|
||||
{`{"v": "foo"}`, "cannot unmarshal object into Go value of type int", Int{Set: true, Valid: false, Value: 0}, `null`},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
t.Run(c.data, func(t *testing.T) {
|
||||
var i Int
|
||||
err := json.Unmarshal([]byte(c.data), &i)
|
||||
|
||||
if c.wantErr != "" {
|
||||
require.Error(t, err)
|
||||
require.ErrorContains(t, err, c.wantErr)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
require.Equal(t, c.wantRes, i)
|
||||
|
||||
b, err := json.Marshal(i)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, c.marshalAs, string(b))
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("struct", func(t *testing.T) {
|
||||
type N struct {
|
||||
I2 Int `json:"i2"`
|
||||
}
|
||||
type T struct {
|
||||
I Int `json:"i"`
|
||||
B bool `json:"b"`
|
||||
N N `json:"n"`
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
data string
|
||||
wantErr string
|
||||
wantRes T
|
||||
marshalAs string
|
||||
}{
|
||||
{`{}`, "", T{}, `{"i": null, "b": false, "n": {"i2": null}}`},
|
||||
{`{"x": "nope"}`, "", T{}, `{"i": null, "b": false, "n": {"i2": null}}`},
|
||||
{`{"i": 1, "b": true}`, "", T{I: Int{Set: true, Valid: true, Value: 1}, B: true}, `{"i": 1, "b": true, "n": {"i2": null}}`},
|
||||
{`{"i": null, "b": true, "n": {}}`, "", T{I: Int{Set: true, Valid: false, Value: 0}, B: true}, `{"i": null, "b": true, "n": {"i2": null}}`},
|
||||
{`{"i": 1, "b": true, "n": {"i2": 2}}`, "", T{I: Int{Set: true, Valid: true, Value: 1}, B: true, N: N{I2: Int{Set: true, Valid: true, Value: 2}}}, `{"i": 1, "b": true, "n": {"i2": 2}}`},
|
||||
{`{"i": 1, "b": true, "n": {"i2": null}}`, "", T{I: Int{Set: true, Valid: true, Value: 1}, B: true, N: N{I2: Int{Set: true, Valid: false, Value: 0}}}, `{"i": 1, "b": true, "n": {"i2": null}}`},
|
||||
{`{"i": "", "b": true}`, "cannot unmarshal string into Go struct", T{I: Int{Set: true, Valid: false, Value: 0}, B: false}, `{"i": null, "b": false, "n": {"i2": null}}`},
|
||||
{`{"b": true, "n": {"i2": true}}`, "cannot unmarshal bool into Go struct", T{I: Int{Set: false, Valid: false, Value: 0}, B: true, N: N{I2: Int{Set: true, Valid: false, Value: 0}}}, `{"i": null, "b": true, "n": {"i2": null}}`},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
t.Run(c.data, func(t *testing.T) {
|
||||
var tt T
|
||||
err := json.Unmarshal([]byte(c.data), &tt)
|
||||
|
||||
if c.wantErr != "" {
|
||||
require.Error(t, err)
|
||||
require.ErrorContains(t, err, c.wantErr)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
require.Equal(t, c.wantRes, tt)
|
||||
|
||||
b, err := json.Marshal(tt)
|
||||
require.NoError(t, err)
|
||||
require.JSONEq(t, c.marshalAs, string(b))
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestSlice(t *testing.T) {
|
||||
t.Run("slice of ints", func(t *testing.T) {
|
||||
cases := []struct {
|
||||
|
@ -841,7 +841,6 @@ func testUpdateHostTablesOnMDMUnenroll(t *testing.T, ds *Datastore) {
|
||||
func expectAppleProfiles(
|
||||
t *testing.T,
|
||||
ds *Datastore,
|
||||
newSet []*fleet.MDMAppleConfigProfile,
|
||||
tmID *uint,
|
||||
want []*fleet.MDMAppleConfigProfile,
|
||||
) map[string]uint {
|
||||
@ -878,7 +877,7 @@ func testBatchSetMDMAppleProfiles(t *testing.T, ds *Datastore) {
|
||||
ctx := context.Background()
|
||||
err := ds.BatchSetMDMAppleProfiles(ctx, tmID, newSet)
|
||||
require.NoError(t, err)
|
||||
return expectAppleProfiles(t, ds, newSet, tmID, want)
|
||||
return expectAppleProfiles(t, ds, tmID, want)
|
||||
}
|
||||
|
||||
withTeamID := func(p *fleet.MDMAppleConfigProfile, tmID uint) *fleet.MDMAppleConfigProfile {
|
||||
|
@ -8,6 +8,7 @@ import (
|
||||
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
|
||||
"github.com/fleetdm/fleet/v4/server/fleet"
|
||||
"github.com/fleetdm/fleet/v4/server/mdm/apple/mobileconfig"
|
||||
microsoft_mdm "github.com/fleetdm/fleet/v4/server/mdm/microsoft"
|
||||
"github.com/go-kit/kit/log/level"
|
||||
"github.com/jmoiron/sqlx"
|
||||
)
|
||||
@ -100,6 +101,9 @@ func (ds *Datastore) BatchSetMDMProfiles(ctx context.Context, tmID *uint, macPro
|
||||
|
||||
func (ds *Datastore) ListMDMConfigProfiles(ctx context.Context, teamID *uint, opt fleet.ListOptions) ([]*fleet.MDMConfigProfilePayload, *fleet.PaginationMetadata, error) {
|
||||
|
||||
// this lists custom profiles, it explicitly filters out the fleet-reserved
|
||||
// ones (reserved identifiers for Apple profiles, reserved names for Windows).
|
||||
|
||||
var profs []*fleet.MDMConfigProfilePayload
|
||||
|
||||
const selectStmt = `
|
||||
@ -142,7 +146,8 @@ FROM (
|
||||
FROM
|
||||
mdm_windows_configuration_profiles
|
||||
WHERE
|
||||
team_id = ?
|
||||
team_id = ? AND
|
||||
name NOT IN (?)
|
||||
) as combined_profiles
|
||||
`
|
||||
|
||||
@ -156,8 +161,13 @@ FROM (
|
||||
for k := range fleetIdentsMap {
|
||||
fleetIdentifiers = append(fleetIdentifiers, k)
|
||||
}
|
||||
fleetNamesMap := microsoft_mdm.FleetReservedProfileNames()
|
||||
fleetNames := make([]string, 0, len(fleetNamesMap))
|
||||
for k := range fleetNamesMap {
|
||||
fleetNames = append(fleetNames, k)
|
||||
}
|
||||
|
||||
args := []any{globalOrTeamID, fleetIdentifiers, globalOrTeamID}
|
||||
args := []any{globalOrTeamID, fleetIdentifiers, globalOrTeamID, fleetNames}
|
||||
stmt, args := appendListOptionsWithCursorToSQL(selectStmt, args, &opt)
|
||||
|
||||
stmt, args, err := sqlx.In(stmt, args...)
|
||||
|
@ -9,6 +9,7 @@ import (
|
||||
|
||||
"github.com/fleetdm/fleet/v4/server/fleet"
|
||||
"github.com/fleetdm/fleet/v4/server/mdm/apple/mobileconfig"
|
||||
microsoft_mdm "github.com/fleetdm/fleet/v4/server/mdm/microsoft"
|
||||
"github.com/fleetdm/fleet/v4/server/ptr"
|
||||
"github.com/fleetdm/fleet/v4/server/test"
|
||||
"github.com/google/uuid"
|
||||
@ -183,8 +184,8 @@ func testBatchSetMDMProfiles(t *testing.T, ds *Datastore) {
|
||||
ctx := context.Background()
|
||||
err := ds.BatchSetMDMProfiles(ctx, tmID, newAppleSet, newWindowsSet)
|
||||
require.NoError(t, err)
|
||||
expectAppleProfiles(t, ds, newAppleSet, tmID, wantApple)
|
||||
expectWindowsProfiles(t, ds, newWindowsSet, tmID, wantWindows)
|
||||
expectAppleProfiles(t, ds, tmID, wantApple)
|
||||
expectWindowsProfiles(t, ds, tmID, wantWindows)
|
||||
}
|
||||
|
||||
withTeamIDApple := func(p *fleet.MDMAppleConfigProfile, tmID uint) *fleet.MDMAppleConfigProfile {
|
||||
@ -305,7 +306,7 @@ func testListMDMConfigProfiles(t *testing.T, ds *Datastore) {
|
||||
require.Len(t, profs, 0)
|
||||
require.Equal(t, *meta, fleet.PaginationMetadata{})
|
||||
|
||||
// add fleet-managed profiles for the team and globally
|
||||
// add fleet-managed Apple profiles for the team and globally
|
||||
for idf := range mobileconfig.FleetPayloadIdentifiers() {
|
||||
_, err = ds.NewMDMAppleConfigProfile(ctx, *generateCP("name_"+idf, idf, team.ID))
|
||||
require.NoError(t, err)
|
||||
@ -324,6 +325,25 @@ func testListMDMConfigProfiles(t *testing.T, ds *Datastore) {
|
||||
require.Len(t, profs, 0)
|
||||
require.Equal(t, *meta, fleet.PaginationMetadata{})
|
||||
|
||||
// add fleet-managed Windows profiles for the team and globally
|
||||
for name := range microsoft_mdm.FleetReservedProfileNames() {
|
||||
_, err = ds.NewMDMWindowsConfigProfile(ctx, fleet.MDMWindowsConfigProfile{Name: name, TeamID: &team.ID, SyncML: winProf})
|
||||
require.NoError(t, err)
|
||||
_, err = ds.NewMDMWindowsConfigProfile(ctx, fleet.MDMWindowsConfigProfile{Name: name, TeamID: nil, SyncML: winProf})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
// still returns no result
|
||||
profs, meta, err = ds.ListMDMConfigProfiles(ctx, nil, opts)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, profs, 0)
|
||||
require.Equal(t, *meta, fleet.PaginationMetadata{})
|
||||
|
||||
profs, meta, err = ds.ListMDMConfigProfiles(ctx, &team.ID, opts)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, profs, 0)
|
||||
require.Equal(t, *meta, fleet.PaginationMetadata{})
|
||||
|
||||
// create a mac profile for global and a Windows profile for team
|
||||
profA, err := ds.NewMDMAppleConfigProfile(ctx, *generateCP("A", "A", 0))
|
||||
require.NoError(t, err)
|
||||
|
@ -700,6 +700,18 @@ func (ds *Datastore) DeleteMDMWindowsConfigProfile(ctx context.Context, profileU
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ds *Datastore) DeleteMDMWindowsConfigProfileByTeamAndName(ctx context.Context, teamID *uint, profileName string) error {
|
||||
var globalOrTeamID uint
|
||||
if teamID != nil {
|
||||
globalOrTeamID = *teamID
|
||||
}
|
||||
_, err := ds.writer(ctx).ExecContext(ctx, `DELETE FROM mdm_windows_configuration_profiles WHERE team_id=? AND name=?`, globalOrTeamID, profileName)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func subqueryHostsMDMWindowsOSSettingsStatusFailed() (string, []interface{}) {
|
||||
sql := `
|
||||
SELECT
|
||||
@ -1358,6 +1370,51 @@ INSERT INTO
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (ds *Datastore) SetOrUpdateMDMWindowsConfigProfile(ctx context.Context, cp fleet.MDMWindowsConfigProfile) error {
|
||||
profileUUID := uuid.New().String()
|
||||
stmt := `
|
||||
INSERT INTO
|
||||
mdm_windows_configuration_profiles (profile_uuid, team_id, name, syncml)
|
||||
(SELECT ?, ?, ?, ? FROM DUAL WHERE
|
||||
NOT EXISTS (
|
||||
SELECT 1 FROM mdm_apple_configuration_profiles WHERE name = ? AND team_id = ?
|
||||
)
|
||||
)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
syncml = VALUES(syncml)
|
||||
`
|
||||
|
||||
var teamID uint
|
||||
if cp.TeamID != nil {
|
||||
teamID = *cp.TeamID
|
||||
}
|
||||
|
||||
res, err := ds.writer(ctx).ExecContext(ctx, stmt, profileUUID, teamID, cp.Name, cp.SyncML, cp.Name, teamID)
|
||||
if err != nil {
|
||||
switch {
|
||||
case isDuplicate(err):
|
||||
return &existsError{
|
||||
ResourceType: "MDMWindowsConfigProfile.Name",
|
||||
Identifier: cp.Name,
|
||||
TeamID: cp.TeamID,
|
||||
}
|
||||
default:
|
||||
return ctxerr.Wrap(ctx, err, "creating new windows mdm config profile")
|
||||
}
|
||||
}
|
||||
|
||||
aff, _ := res.RowsAffected()
|
||||
if aff == 0 {
|
||||
return &existsError{
|
||||
ResourceType: "MDMWindowsConfigProfile.Name",
|
||||
Identifier: cp.Name,
|
||||
TeamID: cp.TeamID,
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ds *Datastore) batchSetMDMWindowsProfilesDB(
|
||||
ctx context.Context,
|
||||
tx sqlx.ExtContext,
|
||||
|
@ -33,6 +33,7 @@ func TestMDMWindows(t *testing.T) {
|
||||
{"TestBulkOperationsMDMWindowsHostProfilesBatch3", testBulkOperationsMDMWindowsHostProfilesBatch3},
|
||||
{"TestGetMDMWindowsProfilesContents", testGetMDMWindowsProfilesContents},
|
||||
{"TestMDMWindowsConfigProfiles", testMDMWindowsConfigProfiles},
|
||||
{"TestSetOrReplaceMDMWindowsConfigProfile", testSetOrReplaceMDMWindowsConfigProfile},
|
||||
{"TestMDMWindowsDiskEncryption", testMDMWindowsDiskEncryption},
|
||||
{"TestMDMWindowsProfilesSummary", testMDMWindowsProfilesSummary},
|
||||
{"TestBatchSetMDMWindowsProfiles", testBatchSetMDMWindowsProfiles},
|
||||
@ -1800,10 +1801,74 @@ func testMDMWindowsConfigProfiles(t *testing.T, ds *Datastore) {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func testSetOrReplaceMDMWindowsConfigProfile(t *testing.T, ds *Datastore) {
|
||||
ctx := context.Background()
|
||||
|
||||
// nothing for no-team, nothing for team 1
|
||||
expectWindowsProfiles(t, ds, nil, nil)
|
||||
expectWindowsProfiles(t, ds, ptr.Uint(1), nil)
|
||||
|
||||
// create a profile for no-team
|
||||
cp1 := *windowsConfigProfileForTest(t, "N1", "N1")
|
||||
err := ds.SetOrUpdateMDMWindowsConfigProfile(ctx, cp1)
|
||||
require.NoError(t, err)
|
||||
|
||||
// creating the same profile for Apple / no-team fails
|
||||
_, err = ds.NewMDMAppleConfigProfile(ctx, *generateCP("N1", "I1", 0))
|
||||
require.Error(t, err)
|
||||
|
||||
profs1 := expectWindowsProfiles(t, ds, nil, []*fleet.MDMWindowsConfigProfile{&cp1})
|
||||
|
||||
// update the profile for no-team
|
||||
cp2 := *windowsConfigProfileForTest(t, "N1", "N1.modified")
|
||||
err = ds.SetOrUpdateMDMWindowsConfigProfile(ctx, cp2)
|
||||
require.NoError(t, err)
|
||||
|
||||
profs2 := expectWindowsProfiles(t, ds, nil, []*fleet.MDMWindowsConfigProfile{&cp2})
|
||||
|
||||
// profile UUIDs are the same
|
||||
require.Equal(t, profs1["N1"], profs2["N1"])
|
||||
|
||||
// create a profile for Apple and team 1 with that name works
|
||||
_, err = ds.NewMDMAppleConfigProfile(ctx, *generateCP("N1", "I1", 1))
|
||||
require.NoError(t, err)
|
||||
|
||||
// try to create that profile for Windows and team 1 fails
|
||||
cp3 := *windowsConfigProfileForTest(t, "N1", "N1")
|
||||
cp3.TeamID = ptr.Uint(1)
|
||||
err = ds.SetOrUpdateMDMWindowsConfigProfile(ctx, cp3)
|
||||
require.Error(t, err)
|
||||
|
||||
expectWindowsProfiles(t, ds, ptr.Uint(1), nil)
|
||||
|
||||
// create a profile with the same name for team 2 works
|
||||
cp4 := *windowsConfigProfileForTest(t, "N1", "N1")
|
||||
cp4.TeamID = ptr.Uint(2)
|
||||
err = ds.SetOrUpdateMDMWindowsConfigProfile(ctx, cp4)
|
||||
require.NoError(t, err)
|
||||
|
||||
profs3 := expectWindowsProfiles(t, ds, ptr.Uint(2), []*fleet.MDMWindowsConfigProfile{&cp4})
|
||||
// profile UUIDs are not the same as for no-team
|
||||
require.NotEqual(t, profs3["N1"], profs2["N1"])
|
||||
|
||||
// create a different profile for no-team
|
||||
cp5 := *windowsConfigProfileForTest(t, "N2", "N2")
|
||||
err = ds.SetOrUpdateMDMWindowsConfigProfile(ctx, cp5)
|
||||
require.NoError(t, err)
|
||||
|
||||
expectWindowsProfiles(t, ds, nil, []*fleet.MDMWindowsConfigProfile{&cp2, &cp5})
|
||||
|
||||
// update that profile for no-team
|
||||
cp6 := *windowsConfigProfileForTest(t, "N2", "N2.modified")
|
||||
err = ds.SetOrUpdateMDMWindowsConfigProfile(ctx, cp6)
|
||||
require.NoError(t, err)
|
||||
|
||||
expectWindowsProfiles(t, ds, nil, []*fleet.MDMWindowsConfigProfile{&cp2, &cp6})
|
||||
}
|
||||
|
||||
func expectWindowsProfiles(
|
||||
t *testing.T,
|
||||
ds *Datastore,
|
||||
newSet []*fleet.MDMWindowsConfigProfile,
|
||||
tmID *uint,
|
||||
want []*fleet.MDMWindowsConfigProfile,
|
||||
) map[string]string {
|
||||
@ -1844,7 +1909,7 @@ func testBatchSetMDMWindowsProfiles(t *testing.T, ds *Datastore) {
|
||||
return ds.batchSetMDMWindowsProfilesDB(ctx, tx, tmID, newSet)
|
||||
})
|
||||
require.NoError(t, err)
|
||||
return expectWindowsProfiles(t, ds, newSet, tmID, want)
|
||||
return expectWindowsProfiles(t, ds, tmID, want)
|
||||
}
|
||||
|
||||
withTeamID := func(p *fleet.MDMWindowsConfigProfile, tmID uint) *fleet.MDMWindowsConfigProfile {
|
||||
|
@ -41,7 +41,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\": null, \"minimum_version\": null}, \"macos_settings\": {\"custom_settings\": null}, \"macos_migration\": {\"mode\": \"\", \"enable\": false, \"webhook_url\": \"\"}, \"windows_settings\": {\"custom_settings\": null}, \"apple_bm_default_team\": \"\", \"apple_bm_terms_expired\": false, \"enable_disk_encryption\": false, \"enabled_and_configured\": false, \"end_user_authentication\": {\"idp_name\": \"\", \"metadata\": \"\", \"entity_id\": \"\", \"issuer_uri\": \"\", \"metadata_url\": \"\"}, \"windows_enabled_and_configured\": false, \"apple_bm_enabled_and_configured\": false}, \"scripts\": null, \"features\": {\"enable_host_users\": true, \"enable_software_inventory\": false}, \"org_info\": {\"org_name\": \"\", \"contact_url\": \"\", \"org_logo_url\": \"\", \"org_logo_url_light_background\": \"\"}, \"integrations\": {\"jira\": null, \"zendesk\": null}, \"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, \"query_reports_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\": null, \"minimum_version\": null}, \"macos_settings\": {\"custom_settings\": null}, \"macos_migration\": {\"mode\": \"\", \"enable\": false, \"webhook_url\": \"\"}, \"windows_updates\": {\"deadline_days\": null, \"grace_period_days\": null}, \"windows_settings\": {\"custom_settings\": null}, \"apple_bm_default_team\": \"\", \"apple_bm_terms_expired\": false, \"enable_disk_encryption\": false, \"enabled_and_configured\": false, \"end_user_authentication\": {\"idp_name\": \"\", \"metadata\": \"\", \"entity_id\": \"\", \"issuer_uri\": \"\", \"metadata_url\": \"\"}, \"windows_enabled_and_configured\": false, \"apple_bm_enabled_and_configured\": false}, \"scripts\": null, \"features\": {\"enable_host_users\": true, \"enable_software_inventory\": false}, \"org_info\": {\"org_name\": \"\", \"contact_url\": \"\", \"org_logo_url\": \"\", \"org_logo_url_light_background\": \"\"}, \"integrations\": {\"jira\": null, \"zendesk\": null}, \"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, \"query_reports_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` (
|
||||
|
@ -586,6 +586,10 @@ func testTeamsMDMConfig(t *testing.T, ds *Datastore) {
|
||||
MinimumVersion: optjson.SetString("10.15.0"),
|
||||
Deadline: optjson.SetString("2025-10-01"),
|
||||
},
|
||||
WindowsUpdates: fleet.WindowsUpdates{
|
||||
DeadlineDays: optjson.SetInt(7),
|
||||
GracePeriodDays: optjson.SetInt(3),
|
||||
},
|
||||
MacOSSetup: fleet.MacOSSetup{
|
||||
BootstrapPackage: optjson.SetString("bootstrap"),
|
||||
MacOSSetupAssistant: optjson.SetString("assistant"),
|
||||
@ -605,6 +609,10 @@ func testTeamsMDMConfig(t *testing.T, ds *Datastore) {
|
||||
MinimumVersion: optjson.SetString("10.15.0"),
|
||||
Deadline: optjson.SetString("2025-10-01"),
|
||||
},
|
||||
WindowsUpdates: fleet.WindowsUpdates{
|
||||
DeadlineDays: optjson.SetInt(7),
|
||||
GracePeriodDays: optjson.SetInt(3),
|
||||
},
|
||||
MacOSSetup: fleet.MacOSSetup{
|
||||
BootstrapPackage: optjson.SetString("bootstrap"),
|
||||
MacOSSetupAssistant: optjson.SetString("assistant"),
|
||||
|
@ -49,6 +49,7 @@ var ActivityDetailsList = []ActivityDetails{
|
||||
ActivityTypeMDMUnenrolled{},
|
||||
|
||||
ActivityTypeEditedMacOSMinVersion{},
|
||||
ActivityTypeEditedWindowsUpdates{},
|
||||
|
||||
ActivityTypeReadHostDiskEncryptionKey{},
|
||||
|
||||
@ -769,6 +770,31 @@ func (a ActivityTypeEditedMacOSMinVersion) Documentation() (activity string, det
|
||||
}`
|
||||
}
|
||||
|
||||
type ActivityTypeEditedWindowsUpdates struct {
|
||||
TeamID *uint `json:"team_id"`
|
||||
TeamName *string `json:"team_name"`
|
||||
DeadlineDays *int `json:"deadline_days"`
|
||||
GracePeriodDays *int `json:"grace_period_days"`
|
||||
}
|
||||
|
||||
func (a ActivityTypeEditedWindowsUpdates) ActivityName() string {
|
||||
return "edited_windows_updates"
|
||||
}
|
||||
|
||||
func (a ActivityTypeEditedWindowsUpdates) Documentation() (activity string, details string, detailsExample string) {
|
||||
return `Generated when the Windows OS updates deadline or grace period is modified.`,
|
||||
`This activity contains the following fields:
|
||||
- "team_id": The ID of the team that the Windows OS updates settings applies to, ` + "`null`" + ` if it applies to devices that are not in a team.
|
||||
- "team_name": The name of the team that the Windows OS updates settings applies to, ` + "`null`" + ` if it applies to devices that are not in a team.
|
||||
- "deadline_days": The number of days before updates are installed, ` + "`null`" + ` if the requirement was removed.
|
||||
- "grace_period_days": The number of days after the deadline before the host is forced to restart, ` + "`null`" + ` if the requirement was removed.`, `{
|
||||
"team_id": 3,
|
||||
"team_name": "Workstations",
|
||||
"deadline_days": 5,
|
||||
"grace_period_days": 2
|
||||
}`
|
||||
}
|
||||
|
||||
type ActivityTypeReadHostDiskEncryptionKey struct {
|
||||
HostID uint `json:"host_id"`
|
||||
HostDisplayName string `json:"host_display_name"`
|
||||
|
@ -147,7 +147,9 @@ type MDM struct {
|
||||
// backend, should be done only after careful analysis.
|
||||
EnabledAndConfigured bool `json:"enabled_and_configured"`
|
||||
|
||||
MacOSUpdates MacOSUpdates `json:"macos_updates"`
|
||||
MacOSUpdates MacOSUpdates `json:"macos_updates"`
|
||||
WindowsUpdates WindowsUpdates `json:"windows_updates"`
|
||||
|
||||
MacOSSettings MacOSSettings `json:"macos_settings"`
|
||||
MacOSSetup MacOSSetup `json:"macos_setup"`
|
||||
MacOSMigration MacOSMigration `json:"macos_migration"`
|
||||
@ -231,6 +233,68 @@ func (m MacOSUpdates) Validate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// WindowsUpdates is part of AppConfig and defines the Windows update settings.
|
||||
type WindowsUpdates struct {
|
||||
DeadlineDays optjson.Int `json:"deadline_days"`
|
||||
GracePeriodDays optjson.Int `json:"grace_period_days"`
|
||||
}
|
||||
|
||||
// EnabledForHost returns a boolean indicating if enforced Windows OS updates
|
||||
// are enabled for the host. Note that the provided Host needs to be loaded
|
||||
// with full MDMInfo data for the check to be valid.
|
||||
func (w WindowsUpdates) EnabledForHost(h *Host) bool {
|
||||
return w.DeadlineDays.Valid &&
|
||||
w.GracePeriodDays.Valid &&
|
||||
h.IsOsqueryEnrolled() &&
|
||||
h.MDMInfo.IsFleetEnrolled()
|
||||
}
|
||||
|
||||
// Equal returns true if the values of the fields of w and other are equal. It
|
||||
// returns false otherwise. If e.g. w.DeadlineDays.Value == 0 but its .Valid
|
||||
// field == false (i.e. it is null), it is not equal to
|
||||
// other.DeadlineDays.Value == 0 with its .Valid field == true.
|
||||
func (w WindowsUpdates) Equal(other WindowsUpdates) bool {
|
||||
if w.DeadlineDays.Value != other.DeadlineDays.Value || w.DeadlineDays.Valid != other.DeadlineDays.Valid {
|
||||
return false
|
||||
}
|
||||
if w.GracePeriodDays.Value != other.GracePeriodDays.Value || w.GracePeriodDays.Valid != other.GracePeriodDays.Valid {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (w WindowsUpdates) Validate() error {
|
||||
const (
|
||||
minDeadline = 0
|
||||
maxDeadline = 30
|
||||
minGracePeriod = 0
|
||||
maxGracePeriod = 7
|
||||
)
|
||||
|
||||
// both must be specified or not specified
|
||||
if w.DeadlineDays.Valid != w.GracePeriodDays.Valid {
|
||||
if w.DeadlineDays.Valid && !w.GracePeriodDays.Valid {
|
||||
return errors.New("grace_period_days is required when deadline_days is provided")
|
||||
} else if !w.DeadlineDays.Valid && w.GracePeriodDays.Valid {
|
||||
return errors.New("deadline_days is required when grace_period_days is provided")
|
||||
}
|
||||
}
|
||||
|
||||
// if both are unspecified, nothing more to validate, updates are not enforced.
|
||||
if !w.DeadlineDays.Valid {
|
||||
return nil
|
||||
}
|
||||
|
||||
// at this point, both fields are set
|
||||
if w.DeadlineDays.Value < minDeadline || w.DeadlineDays.Value > maxDeadline {
|
||||
return fmt.Errorf("deadline_days must be an integer between %d and %d", minDeadline, maxDeadline)
|
||||
}
|
||||
if w.GracePeriodDays.Value < minGracePeriod || w.GracePeriodDays.Value > maxGracePeriod {
|
||||
return fmt.Errorf("grace_period_days must be an integer between %d and %d", minGracePeriod, maxGracePeriod)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// MacOSSettings contains settings specific to macOS.
|
||||
type MacOSSettings struct {
|
||||
// CustomSettings is a slice of configuration profile file paths.
|
||||
|
@ -117,6 +117,88 @@ func TestMacOSUpdatesValidate(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestWindowsUpdatesValidate(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
w WindowsUpdates
|
||||
wantErr string
|
||||
}{
|
||||
{"empty", WindowsUpdates{}, ""},
|
||||
{"explicitly unset", WindowsUpdates{DeadlineDays: optjson.Int{Set: false}, GracePeriodDays: optjson.Int{Set: false}}, ""},
|
||||
{"explicitly null", WindowsUpdates{DeadlineDays: optjson.Int{Set: true, Valid: false}, GracePeriodDays: optjson.Int{Set: true, Valid: false}}, ""},
|
||||
{"explicitly set to 0", WindowsUpdates{DeadlineDays: optjson.SetInt(0), GracePeriodDays: optjson.SetInt(0)}, ""},
|
||||
{"set to valid values", WindowsUpdates{DeadlineDays: optjson.SetInt(20), GracePeriodDays: optjson.SetInt(4)}, ""},
|
||||
{"deadline null grace set", WindowsUpdates{DeadlineDays: optjson.Int{Set: true, Valid: false}, GracePeriodDays: optjson.SetInt(2)}, "deadline_days is required when grace_period_days is provided"},
|
||||
{"grace null deadline set", WindowsUpdates{DeadlineDays: optjson.SetInt(10), GracePeriodDays: optjson.Int{Set: true, Valid: false}}, "grace_period_days is required when deadline_days is provided"},
|
||||
{"negative deadline", WindowsUpdates{DeadlineDays: optjson.SetInt(-1), GracePeriodDays: optjson.SetInt(2)}, "deadline_days must be an integer between 0 and 30"},
|
||||
{"negative grace", WindowsUpdates{DeadlineDays: optjson.SetInt(1), GracePeriodDays: optjson.SetInt(-2)}, "grace_period_days must be an integer between 0 and 7"},
|
||||
{"deadline out of range", WindowsUpdates{DeadlineDays: optjson.SetInt(1000), GracePeriodDays: optjson.SetInt(2)}, "deadline_days must be an integer between 0 and 30"},
|
||||
{"grace out of range", WindowsUpdates{DeadlineDays: optjson.SetInt(1), GracePeriodDays: optjson.SetInt(1000)}, "grace_period_days must be an integer between 0 and 7"},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
err := tc.w.Validate()
|
||||
if tc.wantErr != "" {
|
||||
require.ErrorContains(t, err, tc.wantErr)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestWindowsUpdatesEqual(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
w1, w2 WindowsUpdates
|
||||
want bool
|
||||
}{
|
||||
{"both empty", WindowsUpdates{}, WindowsUpdates{}, true},
|
||||
{"both all set", WindowsUpdates{DeadlineDays: optjson.SetInt(1), GracePeriodDays: optjson.SetInt(2)}, WindowsUpdates{DeadlineDays: optjson.SetInt(1), GracePeriodDays: optjson.SetInt(2)}, true},
|
||||
{"both all null", WindowsUpdates{DeadlineDays: optjson.Int{Set: true}, GracePeriodDays: optjson.Int{Set: true}}, WindowsUpdates{DeadlineDays: optjson.Int{Set: true}, GracePeriodDays: optjson.Int{Set: true}}, true},
|
||||
{"both all set to 0", WindowsUpdates{DeadlineDays: optjson.SetInt(0), GracePeriodDays: optjson.SetInt(0)}, WindowsUpdates{DeadlineDays: optjson.SetInt(0), GracePeriodDays: optjson.SetInt(0)}, true},
|
||||
{"different all set", WindowsUpdates{DeadlineDays: optjson.SetInt(1), GracePeriodDays: optjson.SetInt(2)}, WindowsUpdates{DeadlineDays: optjson.SetInt(3), GracePeriodDays: optjson.SetInt(4)}, false},
|
||||
{"different set deadline", WindowsUpdates{DeadlineDays: optjson.SetInt(1), GracePeriodDays: optjson.SetInt(2)}, WindowsUpdates{DeadlineDays: optjson.SetInt(3), GracePeriodDays: optjson.SetInt(2)}, false},
|
||||
{"different set grace", WindowsUpdates{DeadlineDays: optjson.SetInt(1), GracePeriodDays: optjson.SetInt(2)}, WindowsUpdates{DeadlineDays: optjson.SetInt(1), GracePeriodDays: optjson.SetInt(3)}, false},
|
||||
{"different null deadline", WindowsUpdates{DeadlineDays: optjson.SetInt(0), GracePeriodDays: optjson.SetInt(2)}, WindowsUpdates{DeadlineDays: optjson.Int{Set: true, Valid: false}, GracePeriodDays: optjson.SetInt(2)}, false},
|
||||
{"different null grace", WindowsUpdates{DeadlineDays: optjson.SetInt(1), GracePeriodDays: optjson.SetInt(0)}, WindowsUpdates{DeadlineDays: optjson.SetInt(1), GracePeriodDays: optjson.Int{Set: true, Valid: false}}, false},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
got := tc.w1.Equal(tc.w2)
|
||||
require.Equal(t, tc.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestWIndowsUpdatesEnabledForHost(t *testing.T) {
|
||||
hostWithRequirements := &Host{
|
||||
OsqueryHostID: ptr.String("notempty"),
|
||||
Platform: "windows",
|
||||
MDMInfo: &HostMDM{
|
||||
IsServer: false,
|
||||
Enrolled: true,
|
||||
Name: WellKnownMDMFleet,
|
||||
},
|
||||
}
|
||||
cases := []struct {
|
||||
w WindowsUpdates
|
||||
host *Host
|
||||
want bool
|
||||
}{
|
||||
{WindowsUpdates{}, &Host{}, false},
|
||||
{WindowsUpdates{DeadlineDays: optjson.Int{Set: true, Valid: false}, GracePeriodDays: optjson.Int{Set: true, Valid: false}}, hostWithRequirements, false},
|
||||
{WindowsUpdates{DeadlineDays: optjson.Int{Set: true, Valid: true}, GracePeriodDays: optjson.Int{Set: true, Valid: false}}, hostWithRequirements, false},
|
||||
{WindowsUpdates{DeadlineDays: optjson.Int{Set: true, Valid: true}, GracePeriodDays: optjson.Int{Set: true, Valid: true}}, hostWithRequirements, true},
|
||||
{WindowsUpdates{DeadlineDays: optjson.SetInt(1), GracePeriodDays: optjson.SetInt(2)}, &Host{}, false},
|
||||
{WindowsUpdates{DeadlineDays: optjson.SetInt(1), GracePeriodDays: optjson.SetInt(2)}, hostWithRequirements, true},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
require.Equal(t, tc.want, tc.w.EnabledForHost(tc.host))
|
||||
}
|
||||
}
|
||||
|
||||
func TestMacOSUpdatesEnabledForHost(t *testing.T) {
|
||||
hostWithRequirements := &Host{
|
||||
OsqueryHostID: ptr.String("notempty"),
|
||||
|
@ -1077,6 +1077,10 @@ type Datastore interface {
|
||||
// the specified profile uuid.
|
||||
DeleteMDMWindowsConfigProfile(ctx context.Context, profileUUID string) error
|
||||
|
||||
// DeleteMDMWindowsConfigProfileByTeamAndName deletes the Windows MDM profile corresponding to
|
||||
// the specified team ID (or no team if nil) and profile name.
|
||||
DeleteMDMWindowsConfigProfileByTeamAndName(ctx context.Context, teamID *uint, profileName string) error
|
||||
|
||||
// GetHostMDMWindowsProfiles returns the MDM profile information for the specified Windows host UUID.
|
||||
GetHostMDMWindowsProfiles(ctx context.Context, hostUUID string) ([]HostMDMWindowsProfile, error)
|
||||
|
||||
@ -1137,6 +1141,11 @@ type Datastore interface {
|
||||
// NewMDMWindowsConfigProfile creates and returns a new configuration profile.
|
||||
NewMDMWindowsConfigProfile(ctx context.Context, cp MDMWindowsConfigProfile) (*MDMWindowsConfigProfile, error)
|
||||
|
||||
// SetOrUpdateMDMWindowsConfigProfile creates or replaces a Windows profile.
|
||||
// The profile gets replaced if it already exists for the same team and name
|
||||
// combination.
|
||||
SetOrUpdateMDMWindowsConfigProfile(ctx context.Context, cp MDMWindowsConfigProfile) error
|
||||
|
||||
// BatchSetMDMProfiles sets the MDM Apple or Windows profiles for the given team or
|
||||
// no team in a single transaction.
|
||||
BatchSetMDMProfiles(ctx context.Context, tmID *uint, macProfiles []*MDMAppleConfigProfile, winProfiles []*MDMWindowsConfigProfile) error
|
||||
|
@ -32,6 +32,8 @@ type EnterpriseOverrides struct {
|
||||
DeleteMDMAppleSetupAssistant func(ctx context.Context, teamID *uint) error
|
||||
MDMAppleSyncDEPProfiles func(ctx context.Context) error
|
||||
DeleteMDMAppleBootstrapPackage func(ctx context.Context, teamID *uint) error
|
||||
MDMWindowsEnableOSUpdates func(ctx context.Context, teamID *uint, updates WindowsUpdates) error
|
||||
MDMWindowsDisableOSUpdates func(ctx context.Context, teamID *uint) error
|
||||
}
|
||||
|
||||
type OsqueryService interface {
|
||||
|
@ -34,6 +34,7 @@ type TeamPayload struct {
|
||||
type TeamPayloadMDM struct {
|
||||
EnableDiskEncryption optjson.Bool `json:"enable_disk_encryption"`
|
||||
MacOSUpdates *MacOSUpdates `json:"macos_updates"`
|
||||
WindowsUpdates *WindowsUpdates `json:"windows_updates"`
|
||||
MacOSSettings *MacOSSettings `json:"macos_settings"`
|
||||
MacOSSetup *MacOSSetup `json:"macos_setup"`
|
||||
WindowsSettings *WindowsSettings `json:"windows_settings"`
|
||||
@ -149,10 +150,11 @@ type TeamWebhookSettings struct {
|
||||
}
|
||||
|
||||
type TeamMDM struct {
|
||||
EnableDiskEncryption bool `json:"enable_disk_encryption"`
|
||||
MacOSUpdates MacOSUpdates `json:"macos_updates"`
|
||||
MacOSSettings MacOSSettings `json:"macos_settings"`
|
||||
MacOSSetup MacOSSetup `json:"macos_setup"`
|
||||
EnableDiskEncryption bool `json:"enable_disk_encryption"`
|
||||
MacOSUpdates MacOSUpdates `json:"macos_updates"`
|
||||
WindowsUpdates WindowsUpdates `json:"windows_updates"`
|
||||
MacOSSettings MacOSSettings `json:"macos_settings"`
|
||||
MacOSSetup MacOSSetup `json:"macos_setup"`
|
||||
|
||||
WindowsSettings WindowsSettings `json:"windows_settings"`
|
||||
// NOTE: TeamSpecMDM must be kept in sync with TeamMDM.
|
||||
@ -199,7 +201,8 @@ func (t *TeamMDM) Copy() *TeamMDM {
|
||||
type TeamSpecMDM struct {
|
||||
EnableDiskEncryption optjson.Bool `json:"enable_disk_encryption"`
|
||||
|
||||
MacOSUpdates MacOSUpdates `json:"macos_updates"`
|
||||
MacOSUpdates MacOSUpdates `json:"macos_updates"`
|
||||
WindowsUpdates WindowsUpdates `json:"windows_updates"`
|
||||
|
||||
// A map is used for the macos settings so that we can easily detect if its
|
||||
// sub-keys were provided or not in an "apply" call. E.g. if the
|
||||
@ -415,6 +418,7 @@ func TeamSpecFromTeam(t *Team) (*TeamSpec, error) {
|
||||
|
||||
var mdmSpec TeamSpecMDM
|
||||
mdmSpec.MacOSUpdates = t.Config.MDM.MacOSUpdates
|
||||
mdmSpec.WindowsUpdates = t.Config.MDM.WindowsUpdates
|
||||
mdmSpec.MacOSSettings = t.Config.MDM.MacOSSettings.ToMap()
|
||||
delete(mdmSpec.MacOSSettings, "enable_disk_encryption")
|
||||
mdmSpec.MacOSSetup = t.Config.MDM.MacOSSetup
|
||||
|
@ -45,6 +45,10 @@ type MDMWindowsConfigProfile struct {
|
||||
//
|
||||
// Returns an error if these conditions are not met.
|
||||
func (m *MDMWindowsConfigProfile) ValidateUserProvided() error {
|
||||
if _, ok := microsoft_mdm.FleetReservedProfileNames()[m.Name]; ok {
|
||||
return fmt.Errorf("Profile name %q is not allowed.", m.Name)
|
||||
}
|
||||
|
||||
if mdm.GetRawProfilePlatform(m.SyncML) != "windows" {
|
||||
// it doesn't start with <Replace>, check if it is still valid XML.
|
||||
if len(bytes.TrimSpace(m.SyncML)) == 0 {
|
||||
|
@ -3,6 +3,7 @@ package fleet
|
||||
import (
|
||||
"testing"
|
||||
|
||||
microsoft_mdm "github.com/fleetdm/fleet/v4/server/mdm/microsoft"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
@ -140,6 +141,14 @@ func TestValidateUserProvided(t *testing.T) {
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "Valid XML with reserved name",
|
||||
profile: MDMWindowsConfigProfile{
|
||||
Name: microsoft_mdm.FleetWindowsOSUpdatesProfileName,
|
||||
SyncML: []byte(`<Replace><Target><LocURI>Custom/URI</LocURI></Target></Replace>`),
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
|
@ -398,8 +398,16 @@ const (
|
||||
const (
|
||||
FleetBitLockerTargetLocURI = "/Vendor/MSFT/BitLocker"
|
||||
FleetOSUpdateTargetLocURI = "/Vendor/MSFT/Policy/Config/Update"
|
||||
|
||||
FleetWindowsOSUpdatesProfileName = "Windows OS Updates"
|
||||
)
|
||||
|
||||
func FleetReservedProfileNames() map[string]struct{} {
|
||||
return map[string]struct{}{
|
||||
FleetWindowsOSUpdatesProfileName: {},
|
||||
}
|
||||
}
|
||||
|
||||
func ResolveWindowsMDMDiscovery(serverURL string) (string, error) {
|
||||
return commonmdm.ResolveURL(serverURL, MDE2DiscoveryPath, false)
|
||||
}
|
||||
|
@ -704,6 +704,8 @@ type GetMDMWindowsConfigProfileFunc func(ctx context.Context, profileUUID string
|
||||
|
||||
type DeleteMDMWindowsConfigProfileFunc func(ctx context.Context, profileUUID string) error
|
||||
|
||||
type DeleteMDMWindowsConfigProfileByTeamAndNameFunc func(ctx context.Context, teamID *uint, profileName string) error
|
||||
|
||||
type GetHostMDMWindowsProfilesFunc func(ctx context.Context, hostUUID string) ([]fleet.HostMDMWindowsProfile, error)
|
||||
|
||||
type ListMDMConfigProfilesFunc func(ctx context.Context, teamID *uint, opt fleet.ListOptions) ([]*fleet.MDMConfigProfilePayload, *fleet.PaginationMetadata, error)
|
||||
@ -730,6 +732,8 @@ type BulkDeleteMDMWindowsHostsConfigProfilesFunc func(ctx context.Context, paylo
|
||||
|
||||
type NewMDMWindowsConfigProfileFunc func(ctx context.Context, cp fleet.MDMWindowsConfigProfile) (*fleet.MDMWindowsConfigProfile, error)
|
||||
|
||||
type SetOrUpdateMDMWindowsConfigProfileFunc func(ctx context.Context, cp fleet.MDMWindowsConfigProfile) error
|
||||
|
||||
type BatchSetMDMProfilesFunc func(ctx context.Context, tmID *uint, macProfiles []*fleet.MDMAppleConfigProfile, winProfiles []*fleet.MDMWindowsConfigProfile) error
|
||||
|
||||
type NewHostScriptExecutionRequestFunc func(ctx context.Context, request *fleet.HostScriptRequestPayload) (*fleet.HostScriptResult, error)
|
||||
@ -1784,6 +1788,9 @@ type DataStore struct {
|
||||
DeleteMDMWindowsConfigProfileFunc DeleteMDMWindowsConfigProfileFunc
|
||||
DeleteMDMWindowsConfigProfileFuncInvoked bool
|
||||
|
||||
DeleteMDMWindowsConfigProfileByTeamAndNameFunc DeleteMDMWindowsConfigProfileByTeamAndNameFunc
|
||||
DeleteMDMWindowsConfigProfileByTeamAndNameFuncInvoked bool
|
||||
|
||||
GetHostMDMWindowsProfilesFunc GetHostMDMWindowsProfilesFunc
|
||||
GetHostMDMWindowsProfilesFuncInvoked bool
|
||||
|
||||
@ -1823,6 +1830,9 @@ type DataStore struct {
|
||||
NewMDMWindowsConfigProfileFunc NewMDMWindowsConfigProfileFunc
|
||||
NewMDMWindowsConfigProfileFuncInvoked bool
|
||||
|
||||
SetOrUpdateMDMWindowsConfigProfileFunc SetOrUpdateMDMWindowsConfigProfileFunc
|
||||
SetOrUpdateMDMWindowsConfigProfileFuncInvoked bool
|
||||
|
||||
BatchSetMDMProfilesFunc BatchSetMDMProfilesFunc
|
||||
BatchSetMDMProfilesFuncInvoked bool
|
||||
|
||||
@ -4263,6 +4273,13 @@ func (s *DataStore) DeleteMDMWindowsConfigProfile(ctx context.Context, profileUU
|
||||
return s.DeleteMDMWindowsConfigProfileFunc(ctx, profileUUID)
|
||||
}
|
||||
|
||||
func (s *DataStore) DeleteMDMWindowsConfigProfileByTeamAndName(ctx context.Context, teamID *uint, profileName string) error {
|
||||
s.mu.Lock()
|
||||
s.DeleteMDMWindowsConfigProfileByTeamAndNameFuncInvoked = true
|
||||
s.mu.Unlock()
|
||||
return s.DeleteMDMWindowsConfigProfileByTeamAndNameFunc(ctx, teamID, profileName)
|
||||
}
|
||||
|
||||
func (s *DataStore) GetHostMDMWindowsProfiles(ctx context.Context, hostUUID string) ([]fleet.HostMDMWindowsProfile, error) {
|
||||
s.mu.Lock()
|
||||
s.GetHostMDMWindowsProfilesFuncInvoked = true
|
||||
@ -4354,6 +4371,13 @@ func (s *DataStore) NewMDMWindowsConfigProfile(ctx context.Context, cp fleet.MDM
|
||||
return s.NewMDMWindowsConfigProfileFunc(ctx, cp)
|
||||
}
|
||||
|
||||
func (s *DataStore) SetOrUpdateMDMWindowsConfigProfile(ctx context.Context, cp fleet.MDMWindowsConfigProfile) error {
|
||||
s.mu.Lock()
|
||||
s.SetOrUpdateMDMWindowsConfigProfileFuncInvoked = true
|
||||
s.mu.Unlock()
|
||||
return s.SetOrUpdateMDMWindowsConfigProfileFunc(ctx, cp)
|
||||
}
|
||||
|
||||
func (s *DataStore) BatchSetMDMProfiles(ctx context.Context, tmID *uint, macProfiles []*fleet.MDMAppleConfigProfile, winProfiles []*fleet.MDMWindowsConfigProfile) error {
|
||||
s.mu.Lock()
|
||||
s.BatchSetMDMProfilesFuncInvoked = true
|
||||
|
@ -553,6 +553,37 @@ func (svc *Service) ModifyAppConfig(ctx context.Context, p []byte, applyOpts fle
|
||||
}
|
||||
}
|
||||
|
||||
// if the Windows updates requirements changed, create the corresponding
|
||||
// activity.
|
||||
if !oldAppConfig.MDM.WindowsUpdates.Equal(appConfig.MDM.WindowsUpdates) {
|
||||
var deadline, grace *int
|
||||
if appConfig.MDM.WindowsUpdates.DeadlineDays.Valid {
|
||||
deadline = &appConfig.MDM.WindowsUpdates.DeadlineDays.Value
|
||||
}
|
||||
if appConfig.MDM.WindowsUpdates.GracePeriodDays.Valid {
|
||||
grace = &appConfig.MDM.WindowsUpdates.GracePeriodDays.Value
|
||||
}
|
||||
|
||||
if deadline != nil {
|
||||
if err := svc.EnterpriseOverrides.MDMWindowsEnableOSUpdates(ctx, nil, appConfig.MDM.WindowsUpdates); err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "enable no-team windows OS updates")
|
||||
}
|
||||
} else if err := svc.EnterpriseOverrides.MDMWindowsDisableOSUpdates(ctx, nil); err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "disable no-team windows OS updates")
|
||||
}
|
||||
|
||||
if err := svc.ds.NewActivity(
|
||||
ctx,
|
||||
authz.UserFromContext(ctx),
|
||||
fleet.ActivityTypeEditedWindowsUpdates{
|
||||
DeadlineDays: deadline,
|
||||
GracePeriodDays: grace,
|
||||
},
|
||||
); err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "create activity for app config macos min version modification")
|
||||
}
|
||||
}
|
||||
|
||||
if appConfig.MDM.EnableDiskEncryption.Valid && oldAppConfig.MDM.EnableDiskEncryption.Value != appConfig.MDM.EnableDiskEncryption.Value {
|
||||
if oldAppConfig.MDM.EnabledAndConfigured {
|
||||
var act fleet.ActivityDetails
|
||||
@ -691,6 +722,20 @@ func (svc *Service) validateMDM(
|
||||
invalid.Append("macos_updates", err.Error())
|
||||
}
|
||||
|
||||
// WindowsUpdates
|
||||
updatingWindowsUpdates := !mdm.WindowsUpdates.Equal(oldMdm.WindowsUpdates)
|
||||
if updatingWindowsUpdates {
|
||||
// TODO: Should we validate MDM configured on here too?
|
||||
|
||||
if !license.IsPremium() {
|
||||
invalid.Append("windows_updates.deadline_days", ErrMissingLicense.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
if err := mdm.WindowsUpdates.Validate(); err != nil {
|
||||
invalid.Append("windows_updates", err.Error())
|
||||
}
|
||||
|
||||
// EndUserAuthentication
|
||||
// only validate SSO settings if they changed
|
||||
if mdm.EndUserAuthentication.SSOProviderSettings != oldMdm.EndUserAuthentication.SSOProviderSettings {
|
||||
|
@ -812,6 +812,7 @@ func TestMDMAppleConfig(t *testing.T) {
|
||||
expectedMDM: fleet.MDM{
|
||||
MacOSSetup: fleet.MacOSSetup{BootstrapPackage: optjson.String{Set: true}, MacOSSetupAssistant: optjson.String{Set: true}},
|
||||
MacOSUpdates: fleet.MacOSUpdates{MinimumVersion: optjson.String{Set: true}, Deadline: optjson.String{Set: true}},
|
||||
WindowsUpdates: fleet.WindowsUpdates{DeadlineDays: optjson.Int{Set: true}, GracePeriodDays: optjson.Int{Set: true}},
|
||||
EnableDiskEncryption: optjson.Bool{Set: true, Valid: false},
|
||||
WindowsSettings: fleet.WindowsSettings{CustomSettings: optjson.Slice[string]{Set: true, Value: []string{}}},
|
||||
},
|
||||
@ -840,6 +841,7 @@ func TestMDMAppleConfig(t *testing.T) {
|
||||
AppleBMDefaultTeam: "foobar",
|
||||
MacOSSetup: fleet.MacOSSetup{BootstrapPackage: optjson.String{Set: true}, MacOSSetupAssistant: optjson.String{Set: true}},
|
||||
MacOSUpdates: fleet.MacOSUpdates{MinimumVersion: optjson.String{Set: true}, Deadline: optjson.String{Set: true}},
|
||||
WindowsUpdates: fleet.WindowsUpdates{DeadlineDays: optjson.Int{Set: true}, GracePeriodDays: optjson.Int{Set: true}},
|
||||
EnableDiskEncryption: optjson.Bool{Set: true, Valid: false},
|
||||
WindowsSettings: fleet.WindowsSettings{CustomSettings: optjson.Slice[string]{Set: true, Value: []string{}}},
|
||||
},
|
||||
@ -853,6 +855,7 @@ func TestMDMAppleConfig(t *testing.T) {
|
||||
AppleBMDefaultTeam: "foobar",
|
||||
MacOSSetup: fleet.MacOSSetup{BootstrapPackage: optjson.String{Set: true}, MacOSSetupAssistant: optjson.String{Set: true}},
|
||||
MacOSUpdates: fleet.MacOSUpdates{MinimumVersion: optjson.String{Set: true}, Deadline: optjson.String{Set: true}},
|
||||
WindowsUpdates: fleet.WindowsUpdates{DeadlineDays: optjson.Int{Set: true}, GracePeriodDays: optjson.Int{Set: true}},
|
||||
EnableDiskEncryption: optjson.Bool{Set: true, Valid: false},
|
||||
WindowsSettings: fleet.WindowsSettings{CustomSettings: optjson.Slice[string]{Set: true, Value: []string{}}},
|
||||
},
|
||||
@ -872,6 +875,7 @@ func TestMDMAppleConfig(t *testing.T) {
|
||||
EndUserAuthentication: fleet.MDMEndUserAuthentication{SSOProviderSettings: fleet.SSOProviderSettings{EntityID: "foo"}},
|
||||
MacOSSetup: fleet.MacOSSetup{BootstrapPackage: optjson.String{Set: true}, MacOSSetupAssistant: optjson.String{Set: true}},
|
||||
MacOSUpdates: fleet.MacOSUpdates{MinimumVersion: optjson.String{Set: true}, Deadline: optjson.String{Set: true}},
|
||||
WindowsUpdates: fleet.WindowsUpdates{DeadlineDays: optjson.Int{Set: true}, GracePeriodDays: optjson.Int{Set: true}},
|
||||
EnableDiskEncryption: optjson.Bool{Set: true, Valid: false},
|
||||
WindowsSettings: fleet.WindowsSettings{CustomSettings: optjson.Slice[string]{Set: true, Value: []string{}}},
|
||||
},
|
||||
@ -894,6 +898,7 @@ func TestMDMAppleConfig(t *testing.T) {
|
||||
}},
|
||||
MacOSSetup: fleet.MacOSSetup{BootstrapPackage: optjson.String{Set: true}, MacOSSetupAssistant: optjson.String{Set: true}},
|
||||
MacOSUpdates: fleet.MacOSUpdates{MinimumVersion: optjson.String{Set: true}, Deadline: optjson.String{Set: true}},
|
||||
WindowsUpdates: fleet.WindowsUpdates{DeadlineDays: optjson.Int{Set: true}, GracePeriodDays: optjson.Int{Set: true}},
|
||||
EnableDiskEncryption: optjson.Bool{Set: true, Valid: false},
|
||||
WindowsSettings: fleet.WindowsSettings{CustomSettings: optjson.Slice[string]{Set: true, Value: []string{}}},
|
||||
},
|
||||
|
@ -5270,6 +5270,13 @@ func (s *integrationTestSuite) TestAppConfig() {
|
||||
"mdm": { "apple_bm_default_team": "xyz" }
|
||||
}`), http.StatusUnprocessableEntity, &acResp)
|
||||
|
||||
// try to set the windows updates, which is premium only
|
||||
res = s.Do("PATCH", "/api/latest/fleet/config", json.RawMessage(`{
|
||||
"mdm": { "windows_updates": {"deadline_days": 1, "grace_period_days": 0} }
|
||||
}`), http.StatusUnprocessableEntity)
|
||||
errMsg = extractServerErrorText(res.Body)
|
||||
assert.Contains(t, errMsg, "missing or invalid license")
|
||||
|
||||
// try to enable Windows MDM, impossible without the feature flag
|
||||
// (only set in mdm integrations tests)
|
||||
res = s.Do("PATCH", "/api/latest/fleet/config", json.RawMessage(`{
|
||||
|
@ -3,8 +3,10 @@ package service
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
@ -22,6 +24,7 @@ import (
|
||||
"github.com/fleetdm/fleet/v4/server/datastore/redis/redistest"
|
||||
"github.com/fleetdm/fleet/v4/server/fleet"
|
||||
"github.com/fleetdm/fleet/v4/server/live_query/live_query_mock"
|
||||
microsoft_mdm "github.com/fleetdm/fleet/v4/server/mdm/microsoft"
|
||||
"github.com/fleetdm/fleet/v4/server/ptr"
|
||||
"github.com/fleetdm/fleet/v4/server/test"
|
||||
"github.com/go-kit/log"
|
||||
@ -130,6 +133,10 @@ func (s *integrationEnterpriseTestSuite) TestTeamSpecs() {
|
||||
MinimumVersion: optjson.SetString("10.15.0"),
|
||||
Deadline: optjson.SetString("2021-01-01"),
|
||||
},
|
||||
WindowsUpdates: fleet.WindowsUpdates{
|
||||
DeadlineDays: optjson.Int{Set: true},
|
||||
GracePeriodDays: optjson.Int{Set: true},
|
||||
},
|
||||
MacOSSetup: fleet.MacOSSetup{
|
||||
// because the MacOSSetup was marshalled to JSON to be saved in the DB,
|
||||
// it did get marshalled, and then when unmarshalled it was set (but
|
||||
@ -148,6 +155,105 @@ func (s *integrationEnterpriseTestSuite) TestTeamSpecs() {
|
||||
// an activity was created for team spec applied
|
||||
s.lastActivityMatches(fleet.ActivityTypeAppliedSpecTeam{}.ActivityName(), fmt.Sprintf(`{"teams": [{"id": %d, "name": %q}]}`, team.ID, team.Name), 0)
|
||||
|
||||
// dry-run with invalid windows updates
|
||||
teamSpecs = map[string]any{
|
||||
"specs": []any{
|
||||
map[string]any{
|
||||
"name": teamName,
|
||||
"mdm": map[string]any{
|
||||
"windows_updates": map[string]any{
|
||||
"deadline_days": -1,
|
||||
"grace_period_days": 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
res := s.Do("POST", "/api/latest/fleet/spec/teams", teamSpecs, http.StatusUnprocessableEntity, "dry_run", "true")
|
||||
errMsg := extractServerErrorText(res.Body)
|
||||
require.Contains(t, errMsg, "deadline_days must be an integer between 0 and 30")
|
||||
|
||||
// apply valid windows updates settings
|
||||
teamSpecs = map[string]any{
|
||||
"specs": []any{
|
||||
map[string]any{
|
||||
"name": teamName,
|
||||
"mdm": map[string]any{
|
||||
"windows_updates": map[string]any{
|
||||
"deadline_days": 1,
|
||||
"grace_period_days": 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
s.DoJSON("POST", "/api/latest/fleet/spec/teams", teamSpecs, http.StatusOK, &applyResp)
|
||||
require.Len(t, applyResp.TeamIDsByName, 1)
|
||||
team, err = s.ds.TeamByName(context.Background(), teamName)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, applyResp.TeamIDsByName[teamName], team.ID)
|
||||
require.Equal(t, fleet.TeamMDM{
|
||||
MacOSUpdates: fleet.MacOSUpdates{
|
||||
MinimumVersion: optjson.SetString("10.15.0"),
|
||||
Deadline: optjson.SetString("2021-01-01"),
|
||||
},
|
||||
WindowsUpdates: fleet.WindowsUpdates{
|
||||
DeadlineDays: optjson.SetInt(1),
|
||||
GracePeriodDays: optjson.SetInt(1),
|
||||
},
|
||||
MacOSSetup: fleet.MacOSSetup{
|
||||
MacOSSetupAssistant: optjson.String{Set: true},
|
||||
BootstrapPackage: optjson.String{Set: true},
|
||||
},
|
||||
WindowsSettings: fleet.WindowsSettings{
|
||||
CustomSettings: optjson.Slice[string]{Set: true, Value: []string{}},
|
||||
},
|
||||
}, team.Config.MDM)
|
||||
|
||||
// get the team via the GET endpoint, check that it properly returns the mdm settings
|
||||
var getTmResp getTeamResponse
|
||||
s.DoJSON("GET", "/api/latest/fleet/teams/"+fmt.Sprint(team.ID), nil, http.StatusOK, &getTmResp)
|
||||
require.Equal(t, fleet.TeamMDM{
|
||||
MacOSUpdates: fleet.MacOSUpdates{
|
||||
MinimumVersion: optjson.SetString("10.15.0"),
|
||||
Deadline: optjson.SetString("2021-01-01"),
|
||||
},
|
||||
WindowsUpdates: fleet.WindowsUpdates{
|
||||
DeadlineDays: optjson.SetInt(1),
|
||||
GracePeriodDays: optjson.SetInt(1),
|
||||
},
|
||||
MacOSSetup: fleet.MacOSSetup{
|
||||
MacOSSetupAssistant: optjson.String{Set: true},
|
||||
BootstrapPackage: optjson.String{Set: true},
|
||||
},
|
||||
WindowsSettings: fleet.WindowsSettings{
|
||||
CustomSettings: optjson.Slice[string]{Set: true, Value: []string{}},
|
||||
},
|
||||
}, getTmResp.Team.Config.MDM)
|
||||
|
||||
// get the team via the list teams endpoint, check that it properly returns the mdm settings
|
||||
var listTmResp listTeamsResponse
|
||||
s.DoJSON("GET", "/api/latest/fleet/teams", nil, http.StatusOK, &listTmResp, "query", teamName)
|
||||
require.True(t, len(listTmResp.Teams) > 0)
|
||||
require.Equal(t, team.ID, listTmResp.Teams[0].ID)
|
||||
require.Equal(t, fleet.TeamMDM{
|
||||
MacOSUpdates: fleet.MacOSUpdates{
|
||||
MinimumVersion: optjson.SetString("10.15.0"),
|
||||
Deadline: optjson.SetString("2021-01-01"),
|
||||
},
|
||||
WindowsUpdates: fleet.WindowsUpdates{
|
||||
DeadlineDays: optjson.SetInt(1),
|
||||
GracePeriodDays: optjson.SetInt(1),
|
||||
},
|
||||
MacOSSetup: fleet.MacOSSetup{
|
||||
MacOSSetupAssistant: optjson.String{Set: true},
|
||||
BootstrapPackage: optjson.String{Set: true},
|
||||
},
|
||||
WindowsSettings: fleet.WindowsSettings{
|
||||
CustomSettings: optjson.Slice[string]{Set: true, Value: []string{}},
|
||||
},
|
||||
}, listTmResp.Teams[0].Config.MDM)
|
||||
|
||||
// dry-run with invalid agent options
|
||||
agentOpts = json.RawMessage(`{"config": {"nope": 1}}`)
|
||||
teamSpecs = map[string]any{
|
||||
@ -161,7 +267,7 @@ func (s *integrationEnterpriseTestSuite) TestTeamSpecs() {
|
||||
s.Do("POST", "/api/latest/fleet/spec/teams", teamSpecs, http.StatusBadRequest, "dry_run", "true")
|
||||
|
||||
// dry-run with empty body
|
||||
res := s.DoRaw("POST", "/api/latest/fleet/spec/teams", nil, http.StatusBadRequest, "force", "true")
|
||||
res = s.DoRaw("POST", "/api/latest/fleet/spec/teams", nil, http.StatusBadRequest, "force", "true")
|
||||
errBody, err := io.ReadAll(res.Body)
|
||||
require.NoError(t, err)
|
||||
require.Contains(t, string(errBody), `"Expected JSON Body"`)
|
||||
@ -193,7 +299,7 @@ func (s *integrationEnterpriseTestSuite) TestTeamSpecs() {
|
||||
},
|
||||
}
|
||||
res = s.Do("POST", "/api/latest/fleet/spec/teams", teamSpecs, http.StatusUnprocessableEntity, "dry_run", "true")
|
||||
errMsg := extractServerErrorText(res.Body)
|
||||
errMsg = extractServerErrorText(res.Body)
|
||||
require.Contains(t, errMsg, "Couldn't update macos_settings because MDM features aren't turned on in Fleet.")
|
||||
|
||||
// dry-run with macos disk encryption set to false, no error
|
||||
@ -1694,7 +1800,191 @@ func (s *integrationEnterpriseTestSuite) TestExternalIntegrationsTeamConfig() {
|
||||
require.Len(t, appCfgResp.Integrations.Zendesk, 0)
|
||||
}
|
||||
|
||||
func (s *integrationEnterpriseTestSuite) TestMacOSUpdatesConfig() {
|
||||
func (s *integrationEnterpriseTestSuite) TestWindowsUpdatesTeamConfig() {
|
||||
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
|
||||
|
||||
checkWindowsOSUpdatesProfile(t, s.ds, &team.ID, nil)
|
||||
|
||||
// modify the team's config
|
||||
s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/teams/%d", team.ID), map[string]any{
|
||||
"mdm": map[string]any{
|
||||
"windows_updates": &fleet.WindowsUpdates{
|
||||
DeadlineDays: optjson.SetInt(5),
|
||||
GracePeriodDays: optjson.SetInt(2),
|
||||
},
|
||||
},
|
||||
}, http.StatusOK, &tmResp)
|
||||
require.Equal(t, 5, tmResp.Team.Config.MDM.WindowsUpdates.DeadlineDays.Value)
|
||||
require.Equal(t, 2, tmResp.Team.Config.MDM.WindowsUpdates.GracePeriodDays.Value)
|
||||
s.lastActivityMatches(fleet.ActivityTypeEditedWindowsUpdates{}.ActivityName(), fmt.Sprintf(`{"team_id": %d, "team_name": %q, "deadline_days": 5, "grace_period_days": 2}`, team.ID, team.Name), 0)
|
||||
|
||||
checkWindowsOSUpdatesProfile(t, s.ds, &team.ID, &fleet.WindowsUpdates{
|
||||
DeadlineDays: optjson.SetInt(5),
|
||||
GracePeriodDays: optjson.SetInt(2),
|
||||
})
|
||||
|
||||
// get the team via the GET endpoint, check that it properly returns the mdm
|
||||
// settings.
|
||||
var getTmResp getTeamResponse
|
||||
s.DoJSON("GET", "/api/latest/fleet/teams/"+fmt.Sprint(team.ID), nil, http.StatusOK, &getTmResp)
|
||||
require.Equal(t, fleet.TeamMDM{
|
||||
MacOSUpdates: fleet.MacOSUpdates{
|
||||
MinimumVersion: optjson.String{Set: true},
|
||||
Deadline: optjson.String{Set: true},
|
||||
},
|
||||
WindowsUpdates: fleet.WindowsUpdates{
|
||||
DeadlineDays: optjson.SetInt(5),
|
||||
GracePeriodDays: optjson.SetInt(2),
|
||||
},
|
||||
MacOSSetup: fleet.MacOSSetup{
|
||||
MacOSSetupAssistant: optjson.String{Set: true},
|
||||
BootstrapPackage: optjson.String{Set: true},
|
||||
},
|
||||
WindowsSettings: fleet.WindowsSettings{
|
||||
CustomSettings: optjson.Slice[string]{Set: true, Value: []string{}},
|
||||
},
|
||||
}, getTmResp.Team.Config.MDM)
|
||||
|
||||
// only update the deadline
|
||||
s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/teams/%d", team.ID), map[string]any{
|
||||
"mdm": map[string]any{
|
||||
"windows_updates": &fleet.WindowsUpdates{
|
||||
DeadlineDays: optjson.SetInt(6),
|
||||
GracePeriodDays: optjson.SetInt(2),
|
||||
},
|
||||
},
|
||||
}, http.StatusOK, &tmResp)
|
||||
require.Equal(t, 6, tmResp.Team.Config.MDM.WindowsUpdates.DeadlineDays.Value)
|
||||
require.Equal(t, 2, tmResp.Team.Config.MDM.WindowsUpdates.GracePeriodDays.Value)
|
||||
lastActivity := s.lastActivityMatches(fleet.ActivityTypeEditedWindowsUpdates{}.ActivityName(), fmt.Sprintf(`{"team_id": %d, "team_name": %q, "deadline_days": 6, "grace_period_days": 2}`, team.ID, team.Name), 0)
|
||||
|
||||
checkWindowsOSUpdatesProfile(t, s.ds, &team.ID, &fleet.WindowsUpdates{
|
||||
DeadlineDays: optjson.SetInt(6),
|
||||
GracePeriodDays: optjson.SetInt(2),
|
||||
})
|
||||
|
||||
// setting the macos updates doesn't alter the windows updates
|
||||
tmResp = teamResponse{}
|
||||
s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/teams/%d", team.ID), map[string]any{
|
||||
"mdm": map[string]any{
|
||||
"macos_updates": &fleet.MacOSUpdates{
|
||||
MinimumVersion: optjson.SetString("10.15.0"),
|
||||
Deadline: optjson.SetString("2021-01-01"),
|
||||
},
|
||||
},
|
||||
}, http.StatusOK, &tmResp)
|
||||
require.Equal(t, "10.15.0", tmResp.Team.Config.MDM.MacOSUpdates.MinimumVersion.Value)
|
||||
require.Equal(t, "2021-01-01", tmResp.Team.Config.MDM.MacOSUpdates.Deadline.Value)
|
||||
require.Equal(t, 6, tmResp.Team.Config.MDM.WindowsUpdates.DeadlineDays.Value)
|
||||
require.Equal(t, 2, tmResp.Team.Config.MDM.WindowsUpdates.GracePeriodDays.Value)
|
||||
// did not create a new activity for windows updates
|
||||
s.lastActivityOfTypeMatches(fleet.ActivityTypeEditedWindowsUpdates{}.ActivityName(), "", lastActivity)
|
||||
lastActivity = s.lastActivityMatches(fleet.ActivityTypeEditedMacOSMinVersion{}.ActivityName(), ``, 0)
|
||||
|
||||
checkWindowsOSUpdatesProfile(t, s.ds, &team.ID, &fleet.WindowsUpdates{
|
||||
DeadlineDays: optjson.SetInt(6),
|
||||
GracePeriodDays: optjson.SetInt(2),
|
||||
})
|
||||
|
||||
// sending a nil MDM or WindowsUpdates config doesn't modify anything
|
||||
s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/teams/%d", team.ID), map[string]any{
|
||||
"mdm": nil,
|
||||
}, http.StatusOK, &tmResp)
|
||||
s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/teams/%d", team.ID), map[string]any{
|
||||
"mdm": map[string]any{
|
||||
"windows_updates": nil,
|
||||
},
|
||||
}, http.StatusOK, &tmResp)
|
||||
require.Equal(t, "10.15.0", tmResp.Team.Config.MDM.MacOSUpdates.MinimumVersion.Value)
|
||||
require.Equal(t, "2021-01-01", tmResp.Team.Config.MDM.MacOSUpdates.Deadline.Value)
|
||||
require.Equal(t, 6, tmResp.Team.Config.MDM.WindowsUpdates.DeadlineDays.Value)
|
||||
require.Equal(t, 2, tmResp.Team.Config.MDM.WindowsUpdates.GracePeriodDays.Value)
|
||||
// no new activity is created
|
||||
s.lastActivityMatches("", "", lastActivity)
|
||||
|
||||
checkWindowsOSUpdatesProfile(t, s.ds, &team.ID, &fleet.WindowsUpdates{
|
||||
DeadlineDays: optjson.SetInt(6),
|
||||
GracePeriodDays: optjson.SetInt(2),
|
||||
})
|
||||
|
||||
// sending empty WindowsUpdates fields empties both fields
|
||||
s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/teams/%d", team.ID), map[string]any{
|
||||
"mdm": map[string]any{
|
||||
"windows_updates": map[string]any{
|
||||
"deadline_days": nil,
|
||||
"grace_period_days": nil,
|
||||
},
|
||||
},
|
||||
}, http.StatusOK, &tmResp)
|
||||
require.False(t, tmResp.Team.Config.MDM.WindowsUpdates.DeadlineDays.Valid)
|
||||
require.False(t, tmResp.Team.Config.MDM.WindowsUpdates.GracePeriodDays.Valid)
|
||||
s.lastActivityMatches(fleet.ActivityTypeEditedWindowsUpdates{}.ActivityName(), fmt.Sprintf(`{"team_id": %d, "team_name": %q, "deadline_days": null, "grace_period_days": null}`, team.ID, team.Name), 0)
|
||||
|
||||
checkWindowsOSUpdatesProfile(t, s.ds, &team.ID, nil)
|
||||
|
||||
// error checks:
|
||||
|
||||
// try to set an invalid deadline
|
||||
s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/teams/%d", team.ID), map[string]any{
|
||||
"mdm": map[string]any{
|
||||
"windows_updates": map[string]any{
|
||||
"deadline_days": 1000,
|
||||
"grace_period_days": 1,
|
||||
},
|
||||
},
|
||||
}, http.StatusUnprocessableEntity, &tmResp)
|
||||
|
||||
// try to set an invalid grace period
|
||||
s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/teams/%d", team.ID), map[string]any{
|
||||
"mdm": map[string]any{
|
||||
"windows_updates": map[string]any{
|
||||
"deadline_days": 1,
|
||||
"grace_period_days": 1000,
|
||||
},
|
||||
},
|
||||
}, http.StatusUnprocessableEntity, &tmResp)
|
||||
|
||||
// try to set a deadline but not a grace period
|
||||
s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/teams/%d", team.ID), map[string]any{
|
||||
"mdm": map[string]any{
|
||||
"windows_updates": map[string]any{
|
||||
"deadline_days": 1,
|
||||
},
|
||||
},
|
||||
}, http.StatusUnprocessableEntity, &tmResp)
|
||||
|
||||
// try to set a grace period but no deadline
|
||||
s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/teams/%d", team.ID), map[string]any{
|
||||
"mdm": map[string]any{
|
||||
"windows_updates": map[string]any{
|
||||
"grace_period_days": 1,
|
||||
},
|
||||
},
|
||||
}, http.StatusUnprocessableEntity, &tmResp)
|
||||
|
||||
// try to set an empty grace period but a non-empty deadline
|
||||
s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/teams/%d", team.ID), map[string]any{
|
||||
"mdm": map[string]any{
|
||||
"windows_updates": map[string]any{
|
||||
"deadline_days": 1,
|
||||
"grace_period_days": nil,
|
||||
},
|
||||
},
|
||||
}, http.StatusUnprocessableEntity, &tmResp)
|
||||
}
|
||||
|
||||
func (s *integrationEnterpriseTestSuite) TestMacOSUpdatesTeamConfig() {
|
||||
t := s.T()
|
||||
|
||||
// Create a team
|
||||
@ -1734,6 +2024,24 @@ func (s *integrationEnterpriseTestSuite) TestMacOSUpdatesConfig() {
|
||||
require.Equal(t, "2025-10-01", tmResp.Team.Config.MDM.MacOSUpdates.Deadline.Value)
|
||||
lastActivity := s.lastActivityMatches(fleet.ActivityTypeEditedMacOSMinVersion{}.ActivityName(), fmt.Sprintf(`{"team_id": %d, "team_name": %q, "minimum_version": "10.15.0", "deadline": "2025-10-01"}`, team.ID, team.Name), 0)
|
||||
|
||||
// setting the windows updates doesn't alter the macos updates
|
||||
tmResp = teamResponse{}
|
||||
s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/teams/%d", team.ID), map[string]any{
|
||||
"mdm": map[string]any{
|
||||
"windows_updates": &fleet.WindowsUpdates{
|
||||
DeadlineDays: optjson.SetInt(10),
|
||||
GracePeriodDays: optjson.SetInt(2),
|
||||
},
|
||||
},
|
||||
}, http.StatusOK, &tmResp)
|
||||
require.Equal(t, "10.15.0", tmResp.Team.Config.MDM.MacOSUpdates.MinimumVersion.Value)
|
||||
require.Equal(t, "2025-10-01", tmResp.Team.Config.MDM.MacOSUpdates.Deadline.Value)
|
||||
require.Equal(t, 10, tmResp.Team.Config.MDM.WindowsUpdates.DeadlineDays.Value)
|
||||
require.Equal(t, 2, tmResp.Team.Config.MDM.WindowsUpdates.GracePeriodDays.Value)
|
||||
// did not create a new activity for macos updates
|
||||
s.lastActivityOfTypeMatches(fleet.ActivityTypeEditedMacOSMinVersion{}.ActivityName(), "", lastActivity)
|
||||
lastActivity = s.lastActivityMatches(fleet.ActivityTypeEditedWindowsUpdates{}.ActivityName(), ``, 0)
|
||||
|
||||
// sending a nil MDM or MacOSUpdate config doesn't modify anything
|
||||
s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/teams/%d", team.ID), map[string]any{
|
||||
"mdm": nil,
|
||||
@ -2040,6 +2348,165 @@ func (s *integrationEnterpriseTestSuite) TestDefaultAppleBMTeam() {
|
||||
require.Equal(t, tm.Name, acResp.MDM.AppleBMDefaultTeam)
|
||||
}
|
||||
|
||||
func (s *integrationEnterpriseTestSuite) TestMDMWindowsUpdates() {
|
||||
t := s.T()
|
||||
|
||||
// keep the last activity, to detect newly created ones
|
||||
var activitiesResp listActivitiesResponse
|
||||
s.DoJSON("GET", "/api/latest/fleet/activities", nil, http.StatusOK, &activitiesResp, "order_key", "a.id", "order_direction", "desc")
|
||||
var lastActivity uint
|
||||
if len(activitiesResp.Activities) > 0 {
|
||||
lastActivity = activitiesResp.Activities[0].ID
|
||||
}
|
||||
|
||||
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.WindowsUpdates{DeadlineDays: optjson.Int{Set: true}, GracePeriodDays: optjson.Int{Set: true}}, acResp.MDM.WindowsUpdates)
|
||||
|
||||
// no activity got created
|
||||
activitiesResp = listActivitiesResponse{}
|
||||
s.DoJSON("GET", "/api/latest/fleet/activities", nil, http.StatusOK, &activitiesResp, "order_key", "a.id", "order_direction", "desc")
|
||||
require.Condition(t, func() bool {
|
||||
return (lastActivity == 0 && len(activitiesResp.Activities) == 0) ||
|
||||
(len(activitiesResp.Activities) > 0 && activitiesResp.Activities[0].ID == lastActivity)
|
||||
})
|
||||
}
|
||||
|
||||
// missing grace period
|
||||
checkInvalidConfig(`{"mdm": {
|
||||
"windows_updates": {
|
||||
"deadline_days": 1
|
||||
}
|
||||
}}`)
|
||||
|
||||
// missing deadline
|
||||
checkInvalidConfig(`{"mdm": {
|
||||
"windows_updates": {
|
||||
"grace_period_days": 1
|
||||
}
|
||||
}}`)
|
||||
|
||||
// invalid deadline
|
||||
checkInvalidConfig(`{"mdm": {
|
||||
"windows_updates": {
|
||||
"grace_period_days": 1,
|
||||
"deadline_days": -1
|
||||
}
|
||||
}}`)
|
||||
|
||||
// invalid grace period
|
||||
checkInvalidConfig(`{"mdm": {
|
||||
"windows_updates": {
|
||||
"grace_period_days": -1,
|
||||
"deadline_days": 1
|
||||
}
|
||||
}}`)
|
||||
|
||||
checkWindowsOSUpdatesProfile(t, s.ds, nil, nil)
|
||||
|
||||
// valid config
|
||||
acResp := appConfigResponse{}
|
||||
s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(`{
|
||||
"mdm": {
|
||||
"windows_updates": {
|
||||
"deadline_days": 5,
|
||||
"grace_period_days": 1
|
||||
}
|
||||
}
|
||||
}`), http.StatusOK, &acResp)
|
||||
require.Equal(t, 5, acResp.MDM.WindowsUpdates.DeadlineDays.Value)
|
||||
require.Equal(t, 1, acResp.MDM.WindowsUpdates.GracePeriodDays.Value)
|
||||
|
||||
checkWindowsOSUpdatesProfile(t, s.ds, nil, &fleet.WindowsUpdates{
|
||||
DeadlineDays: optjson.SetInt(5),
|
||||
GracePeriodDays: optjson.SetInt(1),
|
||||
})
|
||||
|
||||
// edited windows updates activity got created
|
||||
s.lastActivityMatches(fleet.ActivityTypeEditedWindowsUpdates{}.ActivityName(), `{"deadline_days":5, "grace_period_days":1, "team_id": null, "team_name": null}`, 0)
|
||||
|
||||
// get the appconfig
|
||||
acResp = appConfigResponse{}
|
||||
s.DoJSON("GET", "/api/latest/fleet/config", nil, http.StatusOK, &acResp)
|
||||
require.Equal(t, 5, acResp.MDM.WindowsUpdates.DeadlineDays.Value)
|
||||
require.Equal(t, 1, acResp.MDM.WindowsUpdates.GracePeriodDays.Value)
|
||||
|
||||
// update the deadline
|
||||
acResp = appConfigResponse{}
|
||||
s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(`{
|
||||
"mdm": {
|
||||
"windows_updates": {
|
||||
"deadline_days": 6,
|
||||
"grace_period_days": 1
|
||||
}
|
||||
}
|
||||
}`), http.StatusOK, &acResp)
|
||||
require.Equal(t, 6, acResp.MDM.WindowsUpdates.DeadlineDays.Value)
|
||||
require.Equal(t, 1, acResp.MDM.WindowsUpdates.GracePeriodDays.Value)
|
||||
|
||||
checkWindowsOSUpdatesProfile(t, s.ds, nil, &fleet.WindowsUpdates{
|
||||
DeadlineDays: optjson.SetInt(6),
|
||||
GracePeriodDays: optjson.SetInt(1),
|
||||
})
|
||||
|
||||
// another edited windows updates activity got created
|
||||
lastActivity = s.lastActivityMatches(fleet.ActivityTypeEditedWindowsUpdates{}.ActivityName(), `{"deadline_days":6, "grace_period_days":1, "team_id": null, "team_name": null}`, 0)
|
||||
|
||||
// update something unrelated - the transparency url
|
||||
acResp = appConfigResponse{}
|
||||
s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(`{"fleet_desktop":{"transparency_url": "customURL"}}`), http.StatusOK, &acResp)
|
||||
require.Equal(t, 6, acResp.MDM.WindowsUpdates.DeadlineDays.Value)
|
||||
require.Equal(t, 1, acResp.MDM.WindowsUpdates.GracePeriodDays.Value)
|
||||
|
||||
// no activity got created
|
||||
s.lastActivityMatches("", ``, lastActivity)
|
||||
|
||||
checkWindowsOSUpdatesProfile(t, s.ds, nil, &fleet.WindowsUpdates{
|
||||
DeadlineDays: optjson.SetInt(6),
|
||||
GracePeriodDays: optjson.SetInt(1),
|
||||
})
|
||||
|
||||
// clear the Windows requirement
|
||||
acResp = appConfigResponse{}
|
||||
s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(`{
|
||||
"mdm": {
|
||||
"windows_updates": {
|
||||
"deadline_days": null,
|
||||
"grace_period_days": null
|
||||
}
|
||||
}
|
||||
}`), http.StatusOK, &acResp)
|
||||
require.False(t, acResp.MDM.WindowsUpdates.DeadlineDays.Valid)
|
||||
require.False(t, acResp.MDM.WindowsUpdates.GracePeriodDays.Valid)
|
||||
|
||||
// edited windows updates activity got created with empty requirement
|
||||
lastActivity = s.lastActivityMatches(fleet.ActivityTypeEditedWindowsUpdates{}.ActivityName(), `{"deadline_days":null, "grace_period_days":null, "team_id": null, "team_name": null}`, 0)
|
||||
|
||||
checkWindowsOSUpdatesProfile(t, s.ds, nil, nil)
|
||||
|
||||
// update again with empty windows requirement
|
||||
acResp = appConfigResponse{}
|
||||
s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(`{
|
||||
"mdm": {
|
||||
"windows_updates": {
|
||||
"deadline_days": null,
|
||||
"grace_period_days": null
|
||||
}
|
||||
}
|
||||
}`), http.StatusOK, &acResp)
|
||||
require.False(t, acResp.MDM.WindowsUpdates.DeadlineDays.Valid)
|
||||
require.False(t, acResp.MDM.WindowsUpdates.GracePeriodDays.Valid)
|
||||
|
||||
// no activity got created
|
||||
s.lastActivityMatches("", ``, lastActivity)
|
||||
}
|
||||
|
||||
func (s *integrationEnterpriseTestSuite) TestMDMMacOSUpdates() {
|
||||
t := s.T()
|
||||
|
||||
@ -5193,3 +5660,33 @@ func (s *integrationEnterpriseTestSuite) TestTeamConfigDetailQueriesOverrides()
|
||||
require.Contains(t, dqResp.Queries, "fleet_detail_query_software_linux")
|
||||
require.Contains(t, dqResp.Queries, fmt.Sprintf("fleet_distributed_query_%s", t.Name()))
|
||||
}
|
||||
|
||||
// checks that the specified team/no-team has the Windows OS Updates profile with
|
||||
// the specified deadline/grace settings (or checks that it doesn't have the
|
||||
// profile if wantSettings is nil). It returns the profile_uuid if it exists,
|
||||
// empty string otherwise.
|
||||
func checkWindowsOSUpdatesProfile(t *testing.T, ds *mysql.Datastore, teamID *uint, wantSettings *fleet.WindowsUpdates) string {
|
||||
ctx := context.Background()
|
||||
|
||||
var prof fleet.MDMWindowsConfigProfile
|
||||
mysql.ExecAdhocSQL(t, ds, func(tx sqlx.ExtContext) error {
|
||||
var globalOrTeamID uint
|
||||
if teamID != nil {
|
||||
globalOrTeamID = *teamID
|
||||
}
|
||||
err := sqlx.GetContext(ctx, tx, &prof, `SELECT profile_uuid, syncml FROM mdm_windows_configuration_profiles WHERE team_id = ? AND name = ?`, globalOrTeamID, microsoft_mdm.FleetWindowsOSUpdatesProfileName)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
})
|
||||
if wantSettings == nil {
|
||||
require.Empty(t, prof.ProfileUUID)
|
||||
} else {
|
||||
require.NotEmpty(t, prof.ProfileUUID)
|
||||
require.Contains(t, string(prof.SyncML), fmt.Sprintf(`<Data>%d</Data>`, wantSettings.DeadlineDays.Value))
|
||||
require.Contains(t, string(prof.SyncML), fmt.Sprintf(`<Data>%d</Data>`, wantSettings.GracePeriodDays.Value))
|
||||
}
|
||||
|
||||
return prof.ProfileUUID
|
||||
}
|
||||
|
@ -8043,7 +8043,7 @@ func (s *integrationMDMTestSuite) TestMDMConfigProfileCRUD() {
|
||||
return id
|
||||
}
|
||||
|
||||
assertWindowsProfile := func(filename, name, locURI string, teamID uint, wantStatus int, wantErrMsg string) string {
|
||||
assertWindowsProfile := func(filename, locURI string, teamID uint, wantStatus int, wantErrMsg string) string {
|
||||
var tmPtr *uint
|
||||
if teamID > 0 {
|
||||
tmPtr = &teamID
|
||||
@ -8065,7 +8065,7 @@ func (s *integrationMDMTestSuite) TestMDMConfigProfileCRUD() {
|
||||
return resp.ProfileID
|
||||
}
|
||||
createWindowsProfile := func(name string, teamID uint) string {
|
||||
id := assertWindowsProfile(name+".xml", name, "./Test", teamID, http.StatusOK, "")
|
||||
id := assertWindowsProfile(name+".xml", "./Test", teamID, http.StatusOK, "")
|
||||
|
||||
var wantJSON string
|
||||
if teamID == 0 {
|
||||
@ -8086,29 +8086,31 @@ func (s *integrationMDMTestSuite) TestMDMConfigProfileCRUD() {
|
||||
teamWinProfID := createWindowsProfile("win-team-profile", testTeam.ID)
|
||||
|
||||
// Windows profile name conflicts with Apple's for no team
|
||||
assertWindowsProfile("apple-global-profile.xml", "apple-global-profile", "./Test", 0, http.StatusConflict, "Couldn't upload. A configuration profile with this name already exists.")
|
||||
assertWindowsProfile("apple-global-profile.xml", "./Test", 0, http.StatusConflict, "Couldn't upload. A configuration profile with this name already exists.")
|
||||
// but no conflict for team 1
|
||||
assertWindowsProfile("apple-global-profile.xml", "apple-global-profile", "./Test", testTeam.ID, http.StatusOK, "")
|
||||
assertWindowsProfile("apple-global-profile.xml", "./Test", testTeam.ID, http.StatusOK, "")
|
||||
// Apple profile name conflicts with Windows' for no team
|
||||
assertAppleProfile("win-global-profile.mobileconfig", "win-global-profile", "test-global-ident-2", 0, http.StatusConflict, "Couldn't upload. A configuration profile with this name already exists.")
|
||||
// but no conflict for team 1
|
||||
assertAppleProfile("win-global-profile.mobileconfig", "win-global-profile", "test-global-ident-2", testTeam.ID, http.StatusOK, "")
|
||||
// Windows profile name conflicts with Apple's for team 1
|
||||
assertWindowsProfile("apple-team-profile.xml", "apple-team-profile", "./Test", testTeam.ID, http.StatusConflict, "Couldn't upload. A configuration profile with this name already exists.")
|
||||
assertWindowsProfile("apple-team-profile.xml", "./Test", testTeam.ID, http.StatusConflict, "Couldn't upload. A configuration profile with this name already exists.")
|
||||
// but no conflict for no-team
|
||||
assertWindowsProfile("apple-team-profile.xml", "apple-team-profile", "./Test", 0, http.StatusOK, "")
|
||||
assertWindowsProfile("apple-team-profile.xml", "./Test", 0, http.StatusOK, "")
|
||||
// Apple profile name conflicts with Windows' for team 1
|
||||
assertAppleProfile("win-team-profile.mobileconfig", "win-team-profile", "test-team-ident-2", testTeam.ID, http.StatusConflict, "Couldn't upload. A configuration profile with this name already exists.")
|
||||
// but no conflict for no-team
|
||||
assertAppleProfile("win-team-profile.mobileconfig", "win-team-profile", "test-team-ident-2", 0, http.StatusOK, "")
|
||||
|
||||
// not an xml nor mobileconfig file
|
||||
assertWindowsProfile("foo.txt", "foo", "./Test", 0, http.StatusBadRequest, "Couldn't upload. The file should be a .mobileconfig or .xml file.")
|
||||
assertWindowsProfile("foo.txt", "./Test", 0, http.StatusBadRequest, "Couldn't upload. The file should be a .mobileconfig or .xml file.")
|
||||
assertAppleProfile("foo.txt", "foo", "foo-ident", 0, http.StatusBadRequest, "Couldn't upload. The file should be a .mobileconfig or .xml file.")
|
||||
|
||||
// Windows-reserved LocURI
|
||||
assertWindowsProfile("bitlocker.xml", "bitlocker", microsoft_mdm.FleetBitLockerTargetLocURI, 0, http.StatusBadRequest, "Couldn't upload. Custom configuration profiles can't include BitLocker settings.")
|
||||
assertWindowsProfile("updates.xml", "updates", microsoft_mdm.FleetOSUpdateTargetLocURI, testTeam.ID, http.StatusBadRequest, "Couldn't upload. Custom configuration profiles can't include Windows updates settings.")
|
||||
assertWindowsProfile("bitlocker.xml", microsoft_mdm.FleetBitLockerTargetLocURI, 0, http.StatusBadRequest, "Couldn't upload. Custom configuration profiles can't include BitLocker settings.")
|
||||
assertWindowsProfile("updates.xml", microsoft_mdm.FleetOSUpdateTargetLocURI, testTeam.ID, http.StatusBadRequest, "Couldn't upload. Custom configuration profiles can't include Windows updates settings.")
|
||||
// Windows-reserved profile name
|
||||
assertWindowsProfile(microsoft_mdm.FleetWindowsOSUpdatesProfileName+".xml", "./Test", 0, http.StatusBadRequest, `Couldn't upload. Profile name "Windows OS Updates" is not allowed.`)
|
||||
|
||||
// Windows invalid content
|
||||
body, headers := generateNewProfileMultipartRequest(t, nil, "win.xml", []byte("\x00\x01\x02"), s.token)
|
||||
@ -8218,6 +8220,16 @@ func (s *integrationMDMTestSuite) TestMDMConfigProfileCRUD() {
|
||||
|
||||
// try to delete the profile
|
||||
s.DoJSON("DELETE", fmt.Sprintf("/api/latest/fleet/mdm/profiles/%d", profile.ProfileID), nil, http.StatusBadRequest, &deleteResp)
|
||||
|
||||
// make fleet add a Windows OS Updates profile
|
||||
acResp = appConfigResponse{}
|
||||
s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(`{
|
||||
"mdm": { "windows_updates": {"deadline_days": 1, "grace_period_days": 1} }
|
||||
}`), http.StatusOK, &acResp)
|
||||
profUUID := checkWindowsOSUpdatesProfile(t, s.ds, nil, &fleet.WindowsUpdates{DeadlineDays: optjson.SetInt(1), GracePeriodDays: optjson.SetInt(1)})
|
||||
|
||||
// try to delete the profile
|
||||
s.DoJSON("DELETE", fmt.Sprintf("/api/latest/fleet/mdm/profiles/%s", profUUID), nil, http.StatusBadRequest, &deleteResp)
|
||||
}
|
||||
|
||||
func (s *integrationMDMTestSuite) TestListMDMConfigProfiles() {
|
||||
|
@ -24,6 +24,7 @@ import (
|
||||
"github.com/fleetdm/fleet/v4/server/fleet"
|
||||
"github.com/fleetdm/fleet/v4/server/mdm"
|
||||
apple_mdm "github.com/fleetdm/fleet/v4/server/mdm/apple"
|
||||
microsoft_mdm "github.com/fleetdm/fleet/v4/server/mdm/microsoft"
|
||||
"github.com/fleetdm/fleet/v4/server/ptr"
|
||||
"github.com/go-kit/kit/log/level"
|
||||
"github.com/go-sql-driver/mysql"
|
||||
@ -1116,6 +1117,12 @@ func (svc *Service) DeleteMDMWindowsConfigProfile(ctx context.Context, profileUU
|
||||
return ctxerr.Wrap(ctx, err)
|
||||
}
|
||||
|
||||
// prevent deleting Windows OS Updates profile (controlled by the OS Updates settings)
|
||||
if _, ok := microsoft_mdm.FleetReservedProfileNames()[prof.Name]; ok {
|
||||
err := &fleet.BadRequestError{Message: "Profiles managed by Fleet can't be deleted using this endpoint."}
|
||||
return ctxerr.Wrap(ctx, err, "validate profile")
|
||||
}
|
||||
|
||||
if err := svc.ds.DeleteMDMWindowsConfigProfile(ctx, profileUUID); err != nil {
|
||||
return ctxerr.Wrap(ctx, err)
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user