mirror of
https://github.com/empayre/fleet.git
synced 2024-11-06 08:55:24 +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)
|
assert.True(t, ds.ApplyEnrollSecretsFuncInvoked)
|
||||||
ds.ApplyEnrollSecretsFuncInvoked = false
|
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")
|
mobileCfgPath := writeTmpMobileconfig(t, "N1")
|
||||||
filename = writeTmpYml(t, fmt.Sprintf(`
|
filename = writeTmpYml(t, fmt.Sprintf(`
|
||||||
apiVersion: v1
|
apiVersion: v1
|
||||||
@ -231,6 +257,10 @@ spec:
|
|||||||
MinimumVersion: optjson.SetString("12.3.1"),
|
MinimumVersion: optjson.SetString("12.3.1"),
|
||||||
Deadline: optjson.SetString("2011-03-01"),
|
Deadline: optjson.SetString("2011-03-01"),
|
||||||
},
|
},
|
||||||
|
WindowsUpdates: fleet.WindowsUpdates{
|
||||||
|
DeadlineDays: optjson.SetInt(5),
|
||||||
|
GracePeriodDays: optjson.SetInt(1),
|
||||||
|
},
|
||||||
MacOSSettings: fleet.MacOSSettings{
|
MacOSSettings: fleet.MacOSSettings{
|
||||||
CustomSettings: []string{mobileCfgPath},
|
CustomSettings: []string{mobileCfgPath},
|
||||||
},
|
},
|
||||||
@ -268,6 +298,10 @@ spec:
|
|||||||
MinimumVersion: optjson.SetString("10.10.10"),
|
MinimumVersion: optjson.SetString("10.10.10"),
|
||||||
Deadline: optjson.SetString("1992-03-01"),
|
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
|
MacOSSettings: fleet.MacOSSettings{ // macos settings not provided, so not cleared
|
||||||
CustomSettings: []string{mobileCfgPath},
|
CustomSettings: []string{mobileCfgPath},
|
||||||
},
|
},
|
||||||
@ -325,6 +359,9 @@ spec:
|
|||||||
macos_updates:
|
macos_updates:
|
||||||
minimum_version:
|
minimum_version:
|
||||||
deadline:
|
deadline:
|
||||||
|
windows_updates:
|
||||||
|
deadline_days:
|
||||||
|
grace_period_days:
|
||||||
macos_settings:
|
macos_settings:
|
||||||
custom_settings:
|
custom_settings:
|
||||||
`)
|
`)
|
||||||
@ -334,6 +371,10 @@ spec:
|
|||||||
MinimumVersion: optjson.String{Set: true},
|
MinimumVersion: optjson.String{Set: true},
|
||||||
Deadline: 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{
|
MacOSSettings: fleet.MacOSSettings{
|
||||||
CustomSettings: []string{},
|
CustomSettings: []string{},
|
||||||
},
|
},
|
||||||
@ -385,6 +426,12 @@ func TestApplyAppConfig(t *testing.T) {
|
|||||||
ds.TeamByNameFunc = func(ctx context.Context, name string) (*fleet.Team, error) {
|
ds.TeamByNameFunc = func(ctx context.Context, name string) (*fleet.Team, error) {
|
||||||
return &fleet.Team{ID: 123}, nil
|
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"}}`)
|
defaultAgentOpts := json.RawMessage(`{"config":{"foo":"bar"}}`)
|
||||||
savedAppConfig := &fleet.AppConfig{
|
savedAppConfig := &fleet.AppConfig{
|
||||||
@ -413,6 +460,9 @@ spec:
|
|||||||
macos_updates:
|
macos_updates:
|
||||||
minimum_version: 12.1.1
|
minimum_version: 12.1.1
|
||||||
deadline: 2011-02-01
|
deadline: 2011-02-01
|
||||||
|
windows_updates:
|
||||||
|
deadline_days: 5
|
||||||
|
grace_period_days: 1
|
||||||
`)
|
`)
|
||||||
|
|
||||||
newMDMSettings := fleet.MDM{
|
newMDMSettings := fleet.MDM{
|
||||||
@ -422,6 +472,10 @@ spec:
|
|||||||
MinimumVersion: optjson.SetString("12.1.1"),
|
MinimumVersion: optjson.SetString("12.1.1"),
|
||||||
Deadline: optjson.SetString("2011-02-01"),
|
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}))
|
assert.Equal(t, "[+] applied fleet config\n", runAppForTest(t, []string{"apply", "-f", name}))
|
||||||
require.NotNil(t, savedAppConfig)
|
require.NotNil(t, savedAppConfig)
|
||||||
@ -441,6 +495,7 @@ spec:
|
|||||||
agent_options:
|
agent_options:
|
||||||
mdm:
|
mdm:
|
||||||
macos_updates:
|
macos_updates:
|
||||||
|
windows_updates:
|
||||||
`)
|
`)
|
||||||
|
|
||||||
assert.Equal(t, "[+] applied fleet config\n", runAppForTest(t, []string{"apply", "-f", name}))
|
assert.Equal(t, "[+] applied fleet config\n", runAppForTest(t, []string{"apply", "-f", name}))
|
||||||
@ -449,6 +504,33 @@ spec:
|
|||||||
assert.True(t, savedAppConfig.Features.EnableSoftwareInventory)
|
assert.True(t, savedAppConfig.Features.EnableSoftwareInventory)
|
||||||
// agent options were cleared, provided but empty
|
// agent options were cleared, provided but empty
|
||||||
assert.Nil(t, savedAppConfig.AgentOptions)
|
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)
|
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 {
|
ds.InsertMDMAppleBootstrapPackageFunc = func(ctx context.Context, bp *fleet.MDMAppleBootstrapPackage) error {
|
||||||
return nil
|
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.
|
// Apply global config.
|
||||||
name := writeTmpYml(t, `---
|
name := writeTmpYml(t, `---
|
||||||
@ -977,6 +1065,9 @@ spec:
|
|||||||
macos_updates:
|
macos_updates:
|
||||||
minimum_version: 10.10.10
|
minimum_version: 10.10.10
|
||||||
deadline: 2020-02-02
|
deadline: 2020-02-02
|
||||||
|
windows_updates:
|
||||||
|
deadline_days: 1
|
||||||
|
grace_period_days: 0
|
||||||
macos_settings:
|
macos_settings:
|
||||||
custom_settings:
|
custom_settings:
|
||||||
- %s
|
- %s
|
||||||
@ -1001,6 +1092,10 @@ spec:
|
|||||||
MinimumVersion: optjson.SetString("10.10.10"),
|
MinimumVersion: optjson.SetString("10.10.10"),
|
||||||
Deadline: optjson.SetString("2020-02-02"),
|
Deadline: optjson.SetString("2020-02-02"),
|
||||||
},
|
},
|
||||||
|
WindowsUpdates: fleet.WindowsUpdates{
|
||||||
|
DeadlineDays: optjson.SetInt(1),
|
||||||
|
GracePeriodDays: optjson.SetInt(0),
|
||||||
|
},
|
||||||
MacOSSettings: fleet.MacOSSettings{
|
MacOSSettings: fleet.MacOSSettings{
|
||||||
CustomSettings: []string{mobileConfigPath},
|
CustomSettings: []string{mobileConfigPath},
|
||||||
},
|
},
|
||||||
@ -1039,6 +1134,10 @@ spec:
|
|||||||
MinimumVersion: optjson.SetString("10.10.10"),
|
MinimumVersion: optjson.SetString("10.10.10"),
|
||||||
Deadline: optjson.SetString("2020-02-02"),
|
Deadline: optjson.SetString("2020-02-02"),
|
||||||
},
|
},
|
||||||
|
WindowsUpdates: fleet.WindowsUpdates{
|
||||||
|
DeadlineDays: optjson.SetInt(1),
|
||||||
|
GracePeriodDays: optjson.SetInt(0),
|
||||||
|
},
|
||||||
MacOSSettings: fleet.MacOSSettings{
|
MacOSSettings: fleet.MacOSSettings{
|
||||||
CustomSettings: []string{mobileConfigPath},
|
CustomSettings: []string{mobileConfigPath},
|
||||||
},
|
},
|
||||||
@ -1061,6 +1160,9 @@ spec:
|
|||||||
macos_updates:
|
macos_updates:
|
||||||
minimum_version: 10.10.10
|
minimum_version: 10.10.10
|
||||||
deadline: 1992-03-01
|
deadline: 1992-03-01
|
||||||
|
windows_updates:
|
||||||
|
deadline_days: 0
|
||||||
|
grace_period_days: 1
|
||||||
macos_settings:
|
macos_settings:
|
||||||
custom_settings:
|
custom_settings:
|
||||||
- %s
|
- %s
|
||||||
@ -1083,6 +1185,10 @@ spec:
|
|||||||
MinimumVersion: optjson.SetString("10.10.10"),
|
MinimumVersion: optjson.SetString("10.10.10"),
|
||||||
Deadline: optjson.SetString("1992-03-01"),
|
Deadline: optjson.SetString("1992-03-01"),
|
||||||
},
|
},
|
||||||
|
WindowsUpdates: fleet.WindowsUpdates{
|
||||||
|
DeadlineDays: optjson.SetInt(0),
|
||||||
|
GracePeriodDays: optjson.SetInt(1),
|
||||||
|
},
|
||||||
}, savedTeam.Config.MDM)
|
}, savedTeam.Config.MDM)
|
||||||
assert.Equal(t, []*fleet.EnrollSecret{{Secret: "BBB"}}, teamEnrollSecrets)
|
assert.Equal(t, []*fleet.EnrollSecret{{Secret: "BBB"}}, teamEnrollSecrets)
|
||||||
assert.True(t, ds.ApplyEnrollSecretsFuncInvoked)
|
assert.True(t, ds.ApplyEnrollSecretsFuncInvoked)
|
||||||
@ -1118,6 +1224,10 @@ spec:
|
|||||||
MinimumVersion: optjson.SetString("10.10.10"),
|
MinimumVersion: optjson.SetString("10.10.10"),
|
||||||
Deadline: optjson.SetString("1992-03-01"),
|
Deadline: optjson.SetString("1992-03-01"),
|
||||||
},
|
},
|
||||||
|
WindowsUpdates: fleet.WindowsUpdates{
|
||||||
|
DeadlineDays: optjson.SetInt(0),
|
||||||
|
GracePeriodDays: optjson.SetInt(1),
|
||||||
|
},
|
||||||
MacOSSetup: fleet.MacOSSetup{
|
MacOSSetup: fleet.MacOSSetup{
|
||||||
MacOSSetupAssistant: optjson.SetString(emptySetupAsst),
|
MacOSSetupAssistant: optjson.SetString(emptySetupAsst),
|
||||||
},
|
},
|
||||||
@ -1150,6 +1260,10 @@ spec:
|
|||||||
MinimumVersion: optjson.SetString("10.10.10"),
|
MinimumVersion: optjson.SetString("10.10.10"),
|
||||||
Deadline: optjson.SetString("1992-03-01"),
|
Deadline: optjson.SetString("1992-03-01"),
|
||||||
},
|
},
|
||||||
|
WindowsUpdates: fleet.WindowsUpdates{
|
||||||
|
DeadlineDays: optjson.SetInt(0),
|
||||||
|
GracePeriodDays: optjson.SetInt(1),
|
||||||
|
},
|
||||||
MacOSSetup: fleet.MacOSSetup{
|
MacOSSetup: fleet.MacOSSetup{
|
||||||
MacOSSetupAssistant: optjson.SetString(emptySetupAsst),
|
MacOSSetupAssistant: optjson.SetString(emptySetupAsst),
|
||||||
BootstrapPackage: optjson.SetString(bootstrapURL),
|
BootstrapPackage: optjson.SetString(bootstrapURL),
|
||||||
@ -2196,6 +2310,12 @@ func TestApplySpecs(t *testing.T) {
|
|||||||
ds.SaveAppConfigFunc = func(ctx context.Context, config *fleet.AppConfig) error {
|
ds.SaveAppConfigFunc = func(ctx context.Context, config *fleet.AppConfig) error {
|
||||||
return nil
|
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 {
|
cases := []struct {
|
||||||
@ -2665,6 +2785,124 @@ spec:
|
|||||||
`,
|
`,
|
||||||
wantErr: `422 Validation Failed: deadline accepts YYYY-MM-DD format only (E.g., "2023-06-01.")`,
|
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",
|
desc: "missing required sso entity_id",
|
||||||
spec: `
|
spec: `
|
||||||
@ -2868,6 +3106,108 @@ spec:
|
|||||||
`,
|
`,
|
||||||
wantErr: `422 Validation Failed: deadline accepts YYYY-MM-DD format only (E.g., "2023-06-01.")`,
|
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",
|
desc: "app config macos_settings.enable_disk_encryption without a value",
|
||||||
spec: `
|
spec: `
|
||||||
|
@ -161,6 +161,10 @@ func TestGetTeams(t *testing.T) {
|
|||||||
MinimumVersion: optjson.SetString("12.3.1"),
|
MinimumVersion: optjson.SetString("12.3.1"),
|
||||||
Deadline: optjson.SetString("2021-12-14"),
|
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"},
|
VulnerabilitySettings: fleet.VulnerabilitySettings{DatabasesPath: "/some/path"},
|
||||||
SMTPSettings: &fleet.SMTPSettings{},
|
SMTPSettings: &fleet.SMTPSettings{},
|
||||||
SSOSettings: &fleet.SSOSettings{},
|
SSOSettings: &fleet.SSOSettings{},
|
||||||
|
MDM: fleet.MDM{
|
||||||
|
WindowsUpdates: fleet.WindowsUpdates{
|
||||||
|
DeadlineDays: optjson.SetInt(7),
|
||||||
|
GracePeriodDays: optjson.SetInt(3),
|
||||||
|
},
|
||||||
|
},
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1989,6 +1999,10 @@ func TestGetTeamsYAMLAndApply(t *testing.T) {
|
|||||||
MinimumVersion: optjson.SetString("12.3.1"),
|
MinimumVersion: optjson.SetString("12.3.1"),
|
||||||
Deadline: optjson.SetString("2021-12-14"),
|
Deadline: optjson.SetString("2021-12-14"),
|
||||||
},
|
},
|
||||||
|
WindowsUpdates: fleet.WindowsUpdates{
|
||||||
|
DeadlineDays: optjson.SetInt(7),
|
||||||
|
GracePeriodDays: optjson.SetInt(3),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -91,6 +91,10 @@
|
|||||||
"minimum_version": null,
|
"minimum_version": null,
|
||||||
"deadline": null
|
"deadline": null
|
||||||
},
|
},
|
||||||
|
"windows_updates": {
|
||||||
|
"deadline_days": 7,
|
||||||
|
"grace_period_days": 3
|
||||||
|
},
|
||||||
"macos_migration": {
|
"macos_migration": {
|
||||||
"enable": false,
|
"enable": false,
|
||||||
"mode": "",
|
"mode": "",
|
||||||
|
@ -27,6 +27,9 @@ spec:
|
|||||||
macos_updates:
|
macos_updates:
|
||||||
minimum_version: null
|
minimum_version: null
|
||||||
deadline: null
|
deadline: null
|
||||||
|
windows_updates:
|
||||||
|
deadline_days: 7
|
||||||
|
grace_period_days: 3
|
||||||
macos_settings:
|
macos_settings:
|
||||||
custom_settings:
|
custom_settings:
|
||||||
macos_setup:
|
macos_setup:
|
||||||
|
@ -49,6 +49,10 @@
|
|||||||
"minimum_version": null,
|
"minimum_version": null,
|
||||||
"deadline": null
|
"deadline": null
|
||||||
},
|
},
|
||||||
|
"windows_updates": {
|
||||||
|
"deadline_days": 7,
|
||||||
|
"grace_period_days": 3
|
||||||
|
},
|
||||||
"macos_migration": {
|
"macos_migration": {
|
||||||
"enable": false,
|
"enable": false,
|
||||||
"mode": "",
|
"mode": "",
|
||||||
|
@ -27,6 +27,9 @@ spec:
|
|||||||
macos_updates:
|
macos_updates:
|
||||||
minimum_version: null
|
minimum_version: null
|
||||||
deadline: null
|
deadline: null
|
||||||
|
windows_updates:
|
||||||
|
deadline_days: 7
|
||||||
|
grace_period_days: 3
|
||||||
macos_settings:
|
macos_settings:
|
||||||
custom_settings:
|
custom_settings:
|
||||||
macos_setup:
|
macos_setup:
|
||||||
|
@ -29,6 +29,10 @@
|
|||||||
"minimum_version": null,
|
"minimum_version": null,
|
||||||
"deadline": null
|
"deadline": null
|
||||||
},
|
},
|
||||||
|
"windows_updates": {
|
||||||
|
"deadline_days": null,
|
||||||
|
"grace_period_days": null
|
||||||
|
},
|
||||||
"macos_settings": {
|
"macos_settings": {
|
||||||
"custom_settings": null
|
"custom_settings": null
|
||||||
},
|
},
|
||||||
@ -93,6 +97,10 @@
|
|||||||
"minimum_version": "12.3.1",
|
"minimum_version": "12.3.1",
|
||||||
"deadline": "2021-12-14"
|
"deadline": "2021-12-14"
|
||||||
},
|
},
|
||||||
|
"windows_updates": {
|
||||||
|
"deadline_days": 7,
|
||||||
|
"grace_period_days": 3
|
||||||
|
},
|
||||||
"macos_settings": {
|
"macos_settings": {
|
||||||
"custom_settings": null
|
"custom_settings": null
|
||||||
},
|
},
|
||||||
|
@ -11,6 +11,9 @@ spec:
|
|||||||
macos_updates:
|
macos_updates:
|
||||||
minimum_version: null
|
minimum_version: null
|
||||||
deadline: null
|
deadline: null
|
||||||
|
windows_updates:
|
||||||
|
deadline_days: null
|
||||||
|
grace_period_days: null
|
||||||
macos_settings:
|
macos_settings:
|
||||||
custom_settings:
|
custom_settings:
|
||||||
windows_settings:
|
windows_settings:
|
||||||
@ -43,6 +46,9 @@ spec:
|
|||||||
macos_updates:
|
macos_updates:
|
||||||
minimum_version: "12.3.1"
|
minimum_version: "12.3.1"
|
||||||
deadline: "2021-12-14"
|
deadline: "2021-12-14"
|
||||||
|
windows_updates:
|
||||||
|
deadline_days: 7
|
||||||
|
grace_period_days: 3
|
||||||
macos_settings:
|
macos_settings:
|
||||||
custom_settings:
|
custom_settings:
|
||||||
windows_settings:
|
windows_settings:
|
||||||
|
@ -35,6 +35,9 @@ spec:
|
|||||||
macos_updates:
|
macos_updates:
|
||||||
deadline: null
|
deadline: null
|
||||||
minimum_version: null
|
minimum_version: null
|
||||||
|
windows_updates:
|
||||||
|
deadline_days: null
|
||||||
|
grace_period_days: null
|
||||||
end_user_authentication:
|
end_user_authentication:
|
||||||
idp_name: ""
|
idp_name: ""
|
||||||
issuer_uri: ""
|
issuer_uri: ""
|
||||||
|
@ -35,6 +35,9 @@ spec:
|
|||||||
macos_updates:
|
macos_updates:
|
||||||
deadline: null
|
deadline: null
|
||||||
minimum_version: null
|
minimum_version: null
|
||||||
|
windows_updates:
|
||||||
|
deadline_days: null
|
||||||
|
grace_period_days: null
|
||||||
end_user_authentication:
|
end_user_authentication:
|
||||||
idp_name: ""
|
idp_name: ""
|
||||||
issuer_uri: ""
|
issuer_uri: ""
|
||||||
|
@ -19,6 +19,9 @@ spec:
|
|||||||
macos_updates:
|
macos_updates:
|
||||||
deadline: null
|
deadline: null
|
||||||
minimum_version: null
|
minimum_version: null
|
||||||
|
windows_updates:
|
||||||
|
deadline_days: null
|
||||||
|
grace_period_days: null
|
||||||
scripts: null
|
scripts: null
|
||||||
name: tm1
|
name: tm1
|
||||||
---
|
---
|
||||||
@ -41,5 +44,8 @@ spec:
|
|||||||
macos_updates:
|
macos_updates:
|
||||||
deadline: null
|
deadline: null
|
||||||
minimum_version: null
|
minimum_version: null
|
||||||
|
windows_updates:
|
||||||
|
deadline_days: null
|
||||||
|
grace_period_days: null
|
||||||
scripts: null
|
scripts: null
|
||||||
name: tm2
|
name: tm2
|
||||||
|
@ -19,6 +19,9 @@ spec:
|
|||||||
macos_updates:
|
macos_updates:
|
||||||
deadline: null
|
deadline: null
|
||||||
minimum_version: null
|
minimum_version: null
|
||||||
|
windows_updates:
|
||||||
|
deadline_days: null
|
||||||
|
grace_period_days: null
|
||||||
scripts: null
|
scripts: null
|
||||||
name: tm1
|
name: tm1
|
||||||
---
|
---
|
||||||
@ -41,5 +44,8 @@ spec:
|
|||||||
macos_updates:
|
macos_updates:
|
||||||
deadline: null
|
deadline: null
|
||||||
minimum_version: null
|
minimum_version: null
|
||||||
|
windows_updates:
|
||||||
|
deadline_days: null
|
||||||
|
grace_period_days: null
|
||||||
scripts: null
|
scripts: null
|
||||||
name: tm2
|
name: tm2
|
||||||
|
@ -17,6 +17,9 @@ spec:
|
|||||||
macos_updates:
|
macos_updates:
|
||||||
deadline: null
|
deadline: null
|
||||||
minimum_version: null
|
minimum_version: null
|
||||||
|
windows_updates:
|
||||||
|
deadline_days: null
|
||||||
|
grace_period_days: null
|
||||||
windows_settings:
|
windows_settings:
|
||||||
custom_settings: null
|
custom_settings: null
|
||||||
scripts: null
|
scripts: null
|
||||||
|
@ -23,6 +23,7 @@ import (
|
|||||||
"github.com/fleetdm/fleet/v4/server/fleet"
|
"github.com/fleetdm/fleet/v4/server/fleet"
|
||||||
apple_mdm "github.com/fleetdm/fleet/v4/server/mdm/apple"
|
apple_mdm "github.com/fleetdm/fleet/v4/server/mdm/apple"
|
||||||
"github.com/fleetdm/fleet/v4/server/mdm/apple/mobileconfig"
|
"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/sso"
|
||||||
"github.com/fleetdm/fleet/v4/server/worker"
|
"github.com/fleetdm/fleet/v4/server/worker"
|
||||||
kitlog "github.com/go-kit/kit/log"
|
kitlog "github.com/go-kit/kit/log"
|
||||||
@ -1020,3 +1021,30 @@ func (svc *Service) GetMDMDiskEncryptionSummary(ctx context.Context, teamID *uin
|
|||||||
},
|
},
|
||||||
}, nil
|
}, 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>
|
</dict>
|
||||||
</plist>`))
|
</plist>`))
|
||||||
|
|
||||||
// TODO(mna): we have a potential issue here with profile names - we need to
|
type windowsOSUpdatesProfileOptions struct {
|
||||||
// make sure they are unique for a given team, but there is no validation of
|
Deadline int
|
||||||
// Fleet-reserved profile names, only of identifiers. A user could create a
|
GracePeriod int
|
||||||
// "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.
|
|
||||||
|
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,
|
DeleteMDMAppleSetupAssistant: eeservice.DeleteMDMAppleSetupAssistant,
|
||||||
MDMAppleSyncDEPProfiles: eeservice.mdmAppleSyncDEPProfiles,
|
MDMAppleSyncDEPProfiles: eeservice.mdmAppleSyncDEPProfiles,
|
||||||
DeleteMDMAppleBootstrapPackage: eeservice.DeleteMDMAppleBootstrapPackage,
|
DeleteMDMAppleBootstrapPackage: eeservice.DeleteMDMAppleBootstrapPackage,
|
||||||
|
MDMWindowsEnableOSUpdates: eeservice.mdmWindowsEnableOSUpdates,
|
||||||
|
MDMWindowsDisableOSUpdates: eeservice.mdmWindowsDisableOSUpdates,
|
||||||
})
|
})
|
||||||
|
|
||||||
return eeservice, nil
|
return eeservice, nil
|
||||||
|
@ -137,7 +137,7 @@ func (svc *Service) ModifyTeam(ctx context.Context, teamID uint, payload fleet.T
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var macOSMinVersionUpdated, macOSDiskEncryptionUpdated, macOSEnableEndUserAuthUpdated bool
|
var macOSMinVersionUpdated, windowsUpdatesUpdated, macOSDiskEncryptionUpdated, macOSEnableEndUserAuthUpdated bool
|
||||||
if payload.MDM != nil {
|
if payload.MDM != nil {
|
||||||
if payload.MDM.MacOSUpdates != nil {
|
if payload.MDM.MacOSUpdates != nil {
|
||||||
if err := payload.MDM.MacOSUpdates.Validate(); err != 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 {
|
if payload.MDM.EnableDiskEncryption.Valid {
|
||||||
macOSDiskEncryptionUpdated = team.Config.MDM.EnableDiskEncryption != payload.MDM.EnableDiskEncryption.Value
|
macOSDiskEncryptionUpdated = team.Config.MDM.EnableDiskEncryption != payload.MDM.EnableDiskEncryption.Value
|
||||||
if macOSDiskEncryptionUpdated && !appCfg.MDM.EnabledAndConfigured {
|
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")
|
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 {
|
if macOSDiskEncryptionUpdated {
|
||||||
var act fleet.ActivityDetails
|
var act fleet.ActivityDetails
|
||||||
if team.Config.MDM.EnableDiskEncryption {
|
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 {
|
if err := spec.MDM.MacOSUpdates.Validate(); err != nil {
|
||||||
return nil, ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("macos_updates", err.Error()))
|
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 {
|
if create {
|
||||||
|
|
||||||
@ -826,6 +869,7 @@ func (svc *Service) createTeamFromSpec(
|
|||||||
MDM: fleet.TeamMDM{
|
MDM: fleet.TeamMDM{
|
||||||
EnableDiskEncryption: enableDiskEncryption,
|
EnableDiskEncryption: enableDiskEncryption,
|
||||||
MacOSUpdates: spec.MDM.MacOSUpdates,
|
MacOSUpdates: spec.MDM.MacOSUpdates,
|
||||||
|
WindowsUpdates: spec.MDM.WindowsUpdates,
|
||||||
MacOSSettings: macOSSettings,
|
MacOSSettings: macOSSettings,
|
||||||
MacOSSetup: macOSSetup,
|
MacOSSetup: macOSSetup,
|
||||||
},
|
},
|
||||||
@ -883,6 +927,9 @@ func (svc *Service) editTeamFromSpec(
|
|||||||
if spec.MDM.MacOSUpdates.Deadline.Set || spec.MDM.MacOSUpdates.MinimumVersion.Set {
|
if spec.MDM.MacOSUpdates.Deadline.Set || spec.MDM.MacOSUpdates.MinimumVersion.Set {
|
||||||
team.Config.MDM.MacOSUpdates = spec.MDM.MacOSUpdates
|
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
|
oldEnableDiskEncryption := team.Config.MDM.EnableDiskEncryption
|
||||||
if err := svc.applyTeamMacOSSettings(ctx, spec, &team.Config.MDM.MacOSSettings); err != nil {
|
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
|
didUpdateBootstrapPackage = oldMacOSSetup.BootstrapPackage.Value != spec.MDM.MacOSSetup.BootstrapPackage.Value
|
||||||
team.Config.MDM.MacOSSetup.BootstrapPackage = spec.MDM.MacOSSetup.BootstrapPackage
|
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 &&
|
if !appCfg.MDM.EnabledAndConfigured &&
|
||||||
((didUpdateSetupAssistant && team.Config.MDM.MacOSSetup.MacOSSetupAssistant.Value != "") ||
|
((didUpdateSetupAssistant && team.Config.MDM.MacOSSetup.MacOSSetupAssistant.Value != "") ||
|
||||||
(didUpdateBootstrapPackage && team.Config.MDM.MacOSSetup.BootstrapPackage.Value != "")) {
|
(didUpdateBootstrapPackage && team.Config.MDM.MacOSSetup.BootstrapPackage.Value != "")) {
|
||||||
|
@ -150,6 +150,10 @@ const DEFAULT_CONFIG_MOCK: IConfig = {
|
|||||||
mode: "",
|
mode: "",
|
||||||
webhook_url: "",
|
webhook_url: "",
|
||||||
},
|
},
|
||||||
|
windows_updates: {
|
||||||
|
deadline_days: null,
|
||||||
|
grace_period_days: null,
|
||||||
|
},
|
||||||
end_user_authentication: {
|
end_user_authentication: {
|
||||||
entity_id: "",
|
entity_id: "",
|
||||||
issuer_uri: "",
|
issuer_uri: "",
|
||||||
|
@ -9,6 +9,8 @@ const baseClass = "data-error";
|
|||||||
interface IDataErrorProps {
|
interface IDataErrorProps {
|
||||||
/** the description text displayed under the header */
|
/** the description text displayed under the header */
|
||||||
description?: string;
|
description?: string;
|
||||||
|
/** Excludes the link that asks user to create an issue. Defaults to `false` */
|
||||||
|
excludeIssueLink?: boolean;
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
card?: boolean;
|
card?: boolean;
|
||||||
className?: string;
|
className?: string;
|
||||||
@ -18,6 +20,7 @@ const DEFAULT_DESCRIPTION = "Refresh the page or log in again.";
|
|||||||
|
|
||||||
const DataError = ({
|
const DataError = ({
|
||||||
description = DEFAULT_DESCRIPTION,
|
description = DEFAULT_DESCRIPTION,
|
||||||
|
excludeIssueLink = false,
|
||||||
children,
|
children,
|
||||||
card,
|
card,
|
||||||
className,
|
className,
|
||||||
@ -37,6 +40,7 @@ const DataError = ({
|
|||||||
{children || (
|
{children || (
|
||||||
<>
|
<>
|
||||||
<span className="info__data">{description}</span>
|
<span className="info__data">{description}</span>
|
||||||
|
{!excludeIssueLink && (
|
||||||
<span className="info__data">
|
<span className="info__data">
|
||||||
If this keeps happening, please
|
If this keeps happening, please
|
||||||
<CustomLink
|
<CustomLink
|
||||||
@ -45,6 +49,7 @@ const DataError = ({
|
|||||||
newTab
|
newTab
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
|
import React from "react";
|
||||||
import { Meta, StoryObj } from "@storybook/react";
|
import { Meta, StoryObj } from "@storybook/react";
|
||||||
|
|
||||||
|
import LastUpdatedText from "components/LastUpdatedText";
|
||||||
|
|
||||||
import SectionHeader from ".";
|
import SectionHeader from ".";
|
||||||
|
|
||||||
const meta: Meta<typeof SectionHeader> = {
|
const meta: Meta<typeof SectionHeader> = {
|
||||||
@ -13,3 +16,14 @@ export default meta;
|
|||||||
type Story = StoryObj<typeof SectionHeader>;
|
type Story = StoryObj<typeof SectionHeader>;
|
||||||
|
|
||||||
export const Basic: Story = {};
|
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 {
|
interface ISectionHeaderProps {
|
||||||
title: string;
|
title: string;
|
||||||
|
subTitle?: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SectionHeader = ({ title }: ISectionHeaderProps) => {
|
const SectionHeader = ({ title, subTitle }: ISectionHeaderProps) => {
|
||||||
return <h2 className={baseClass}>{title}</h2>;
|
return (
|
||||||
|
<div className={baseClass}>
|
||||||
|
<h2>{title}</h2>
|
||||||
|
{subTitle && <div className={`${baseClass}__sub-title`}>{subTitle}</div>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default SectionHeader;
|
export default SectionHeader;
|
||||||
|
@ -1,8 +1,14 @@
|
|||||||
.section-header {
|
.section-header {
|
||||||
margin: 0 0 $pad-large;
|
display: flex;
|
||||||
padding-bottom: $pad-small;
|
align-items: center;
|
||||||
font-size: $medium;
|
gap: $pad-small;
|
||||||
font-weight: $regular;
|
padding-bottom: $pad-medium;
|
||||||
color: $core-fleet-black;
|
border-bottom: 1px solid $ui-fleet-black-10;
|
||||||
border-bottom: solid 1px $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",
|
AddedScript = "added_script",
|
||||||
DeletedScript = "deleted_script",
|
DeletedScript = "deleted_script",
|
||||||
EditedScript = "edited_script",
|
EditedScript = "edited_script",
|
||||||
|
EditedWindowsUpdates = "edited_windows_updates",
|
||||||
}
|
}
|
||||||
export interface IActivity {
|
export interface IActivity {
|
||||||
created_at: string;
|
created_at: string;
|
||||||
@ -101,4 +102,6 @@ export interface IActivityDetails {
|
|||||||
name?: string;
|
name?: string;
|
||||||
script_execution_id?: string;
|
script_execution_id?: string;
|
||||||
script_name?: string;
|
script_name?: string;
|
||||||
|
deadline_days?: number;
|
||||||
|
grace_period_days?: number;
|
||||||
}
|
}
|
||||||
|
@ -37,8 +37,8 @@ export interface IMdmConfig {
|
|||||||
windows_enabled_and_configured: boolean;
|
windows_enabled_and_configured: boolean;
|
||||||
end_user_authentication: IEndUserAuthentication;
|
end_user_authentication: IEndUserAuthentication;
|
||||||
macos_updates: {
|
macos_updates: {
|
||||||
minimum_version: string;
|
minimum_version: string | null;
|
||||||
deadline: string;
|
deadline: string | null;
|
||||||
};
|
};
|
||||||
macos_settings: {
|
macos_settings: {
|
||||||
custom_settings: null;
|
custom_settings: null;
|
||||||
@ -50,6 +50,10 @@ export interface IMdmConfig {
|
|||||||
macos_setup_assistant: string | null;
|
macos_setup_assistant: string | null;
|
||||||
};
|
};
|
||||||
macos_migration: IMacOsMigrationSettings;
|
macos_migration: IMacOsMigrationSettings;
|
||||||
|
windows_updates: {
|
||||||
|
deadline_days: number | null;
|
||||||
|
grace_period_days: number | null;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IDeviceGlobalConfig {
|
export interface IDeviceGlobalConfig {
|
||||||
|
@ -46,8 +46,8 @@ export interface ITeam extends ITeamSummary {
|
|||||||
mdm?: {
|
mdm?: {
|
||||||
enable_disk_encryption: boolean;
|
enable_disk_encryption: boolean;
|
||||||
macos_updates: {
|
macos_updates: {
|
||||||
minimum_version: string;
|
minimum_version: string | null;
|
||||||
deadline: string;
|
deadline: string | null;
|
||||||
};
|
};
|
||||||
macos_settings: {
|
macos_settings: {
|
||||||
custom_settings: null; // TODO: types?
|
custom_settings: null; // TODO: types?
|
||||||
@ -58,6 +58,10 @@ export interface ITeam extends ITeamSummary {
|
|||||||
enable_end_user_authentication: boolean;
|
enable_end_user_authentication: boolean;
|
||||||
macos_setup_assistant: string | null; // TODO: types?
|
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) => {
|
deletedMultipleSavedQuery: (activity: IActivity) => {
|
||||||
return <> deleted multiple queries.</>;
|
return <> deleted multiple queries.</>;
|
||||||
},
|
},
|
||||||
@ -812,6 +833,9 @@ const getDetail = (
|
|||||||
case ActivityType.EditedScript: {
|
case ActivityType.EditedScript: {
|
||||||
return TAGGED_TEMPLATES.editedScript(activity);
|
return TAGGED_TEMPLATES.editedScript(activity);
|
||||||
}
|
}
|
||||||
|
case ActivityType.EditedWindowsUpdates: {
|
||||||
|
return TAGGED_TEMPLATES.editedWindowsUpdates(activity);
|
||||||
|
}
|
||||||
case ActivityType.DeletedMultipleSavedQuery: {
|
case ActivityType.DeletedMultipleSavedQuery: {
|
||||||
return TAGGED_TEMPLATES.deletedMultipleSavedQuery(activity);
|
return TAGGED_TEMPLATES.deletedMultipleSavedQuery(activity);
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,7 @@ import { NotificationContext } from "context/notification";
|
|||||||
import PATHS from "router/paths";
|
import PATHS from "router/paths";
|
||||||
|
|
||||||
import CustomLink from "components/CustomLink";
|
import CustomLink from "components/CustomLink";
|
||||||
|
import SectionHeader from "components/SectionHeader";
|
||||||
import Spinner from "components/Spinner";
|
import Spinner from "components/Spinner";
|
||||||
import DataError from "components/DataError";
|
import DataError from "components/DataError";
|
||||||
|
|
||||||
@ -152,7 +153,7 @@ const CustomSettings = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={baseClass}>
|
<div className={baseClass}>
|
||||||
<h2>Custom settings</h2>
|
<SectionHeader title="Custom settings" />
|
||||||
<p className={`${baseClass}__description`}>
|
<p className={`${baseClass}__description`}>
|
||||||
Create and upload configuration profiles to apply custom settings.{" "}
|
Create and upload configuration profiles to apply custom settings.{" "}
|
||||||
<CustomLink
|
<CustomLink
|
||||||
|
@ -1,14 +1,4 @@
|
|||||||
.custom-settings {
|
.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 {
|
&__description {
|
||||||
font-size: $x-small;
|
font-size: $x-small;
|
||||||
margin: $pad-xxlarge 0;
|
margin: $pad-xxlarge 0;
|
||||||
|
@ -13,6 +13,7 @@ import CustomLink from "components/CustomLink";
|
|||||||
import Checkbox from "components/forms/fields/Checkbox";
|
import Checkbox from "components/forms/fields/Checkbox";
|
||||||
import PremiumFeatureMessage from "components/PremiumFeatureMessage";
|
import PremiumFeatureMessage from "components/PremiumFeatureMessage";
|
||||||
import Spinner from "components/Spinner";
|
import Spinner from "components/Spinner";
|
||||||
|
import SectionHeader from "components/SectionHeader";
|
||||||
|
|
||||||
import DiskEncryptionTable from "./components/DiskEncryptionTable";
|
import DiskEncryptionTable from "./components/DiskEncryptionTable";
|
||||||
|
|
||||||
@ -114,7 +115,7 @@ const DiskEncryption = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={baseClass}>
|
<div className={baseClass}>
|
||||||
<h2>Disk encryption</h2>
|
<SectionHeader title="Disk encryption" />
|
||||||
{!isPremiumTier ? (
|
{!isPremiumTier ? (
|
||||||
<PremiumFeatureMessage
|
<PremiumFeatureMessage
|
||||||
className={`${baseClass}__premium-feature-message`}
|
className={`${baseClass}__premium-feature-message`}
|
||||||
|
@ -1,14 +1,4 @@
|
|||||||
.disk-encryption {
|
.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 {
|
&__premium-feature-message {
|
||||||
margin-top: 80px;
|
margin-top: 80px;
|
||||||
text-align: center;
|
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 { InjectedRouter } from "react-router";
|
||||||
|
|
||||||
|
import { IConfig } from "interfaces/config";
|
||||||
import { AppContext } from "context/app";
|
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 PremiumFeatureMessage from "components/PremiumFeatureMessage";
|
||||||
|
|
||||||
import OsMinVersionForm from "./components/OsMinVersionForm";
|
|
||||||
import NudgePreview from "./components/NudgePreview";
|
import NudgePreview from "./components/NudgePreview";
|
||||||
import TurnOnMdmMessage from "../components/TurnOnMdmMessage/TurnOnMdmMessage";
|
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 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 {
|
interface IOSUpdates {
|
||||||
router: InjectedRouter;
|
router: InjectedRouter;
|
||||||
teamIdForApi: number;
|
teamIdForApi?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const OSUpdates = ({ router, teamIdForApi }: IOSUpdates) => {
|
const OSUpdates = ({ router, teamIdForApi }: IOSUpdates) => {
|
||||||
const { config, isPremiumTier } = useContext(AppContext);
|
const { config, isPremiumTier } = useContext(AppContext);
|
||||||
|
|
||||||
const OperatingSystemCard = useInfoCard({
|
// the default platform is mac and we later update this value when we have
|
||||||
title: "macOS versions",
|
// done more checks.
|
||||||
children: (
|
const [
|
||||||
<OperatingSystems
|
selectedPlatform,
|
||||||
currentTeamId={teamIdForApi}
|
setSelectedPlatform,
|
||||||
selectedPlatform="darwin"
|
] = useState<OSUpdatesSupportedPlatform>("darwin");
|
||||||
showTitle
|
|
||||||
showDescription={false}
|
|
||||||
includeNameColumn={false}
|
|
||||||
setShowTitle={() => {
|
|
||||||
return null;
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
});
|
|
||||||
|
|
||||||
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 <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}>
|
<div className={baseClass}>
|
||||||
<p className={`${baseClass}__description`}>
|
<p className={`${baseClass}__description`}>
|
||||||
Remotely encourage the installation of macOS updates on hosts assigned
|
Remotely encourage the installation of macOS updates on hosts assigned
|
||||||
@ -50,22 +81,17 @@ const OSUpdates = ({ router, teamIdForApi }: IOSUpdates) => {
|
|||||||
</p>
|
</p>
|
||||||
<div className={`${baseClass}__content`}>
|
<div className={`${baseClass}__content`}>
|
||||||
<div className={`${baseClass}__form-table-content`}>
|
<div className={`${baseClass}__form-table-content`}>
|
||||||
<div className={`${baseClass}__os-versions-card`}>
|
<CurrentVersionSection currentTeamId={teamIdForApi} />
|
||||||
{OperatingSystemCard}
|
<TargetSection
|
||||||
</div>
|
currentTeamId={teamIdForApi}
|
||||||
<div className={`${baseClass}__os-version-form`}>
|
onSelectAccordionItem={handleSelectPlatform}
|
||||||
<OsMinVersionForm currentTeamId={teamIdForApi} key={teamIdForApi} />
|
/>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div className={`${baseClass}__nudge-preview`}>
|
<div className={`${baseClass}__nudge-preview`}>
|
||||||
<NudgePreview />
|
<NudgePreview platform={selectedPlatform} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
|
||||||
<PremiumFeatureMessage
|
|
||||||
className={`${baseClass}__premium-feature-message`}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -9,13 +9,13 @@
|
|||||||
max-width: $break-xxl;
|
max-width: $break-xxl;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: center;
|
||||||
gap: $pad-xxlarge;
|
gap: $pad-xxlarge;
|
||||||
|
|
||||||
h2 {
|
|
||||||
font-size: $small;
|
|
||||||
margin: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__form-table-content, &__nudge-preview {
|
||||||
|
flex-grow: 1;
|
||||||
|
max-width: 640px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__os-versions-card {
|
&__os-versions-card {
|
||||||
@ -30,5 +30,10 @@
|
|||||||
&__content {
|
&__content {
|
||||||
flex-direction: column;
|
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 React, { useContext, useState } from "react";
|
||||||
import { useQuery } from "react-query";
|
|
||||||
import { isEmpty } from "lodash";
|
import { isEmpty } from "lodash";
|
||||||
|
import classnames from "classnames";
|
||||||
|
|
||||||
|
import { APP_CONTEXT_NO_TEAM_ID } from "interfaces/team";
|
||||||
import { NotificationContext } from "context/notification";
|
import { NotificationContext } from "context/notification";
|
||||||
import { AppContext } from "context/app";
|
|
||||||
import configAPI from "services/entities/config";
|
import configAPI from "services/entities/config";
|
||||||
import teamsAPI, { ILoadTeamResponse } from "services/entities/teams";
|
import teamsAPI from "services/entities/teams";
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import InputField from "components/forms/fields/InputField";
|
import InputField from "components/forms/fields/InputField";
|
||||||
import Button from "components/buttons/Button";
|
import Button from "components/buttons/Button";
|
||||||
import validatePresence from "components/forms/validators/validate_presence";
|
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;
|
minOsVersion: string;
|
||||||
deadline: string;
|
deadline: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IMinOsVersionFormErrors {
|
interface IMacOSTargetFormErrors {
|
||||||
minOsVersion?: string;
|
minOsVersion?: string;
|
||||||
deadline?: 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);
|
return /^\d{4}-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])$/.test(value);
|
||||||
};
|
};
|
||||||
|
|
||||||
const validateForm = (formData: IMinOsVersionFormData) => {
|
const validateForm = (formData: IMacOSTargetFormData) => {
|
||||||
const errors: IMinOsVersionFormErrors = {};
|
const errors: IMacOSTargetFormErrors = {};
|
||||||
|
|
||||||
if (!validatePresence(formData.minOsVersion)) {
|
if (!validatePresence(formData.minOsVersion)) {
|
||||||
errors.minOsVersion = "The minimum version is required.";
|
errors.minOsVersion = "The minimum version is required.";
|
||||||
@ -62,49 +61,32 @@ const createMdmConfigData = (minOsVersion: string, deadline: string) => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
interface IOsMinVersionForm {
|
interface IMacOSTargetFormProps {
|
||||||
currentTeamId?: number;
|
currentTeamId: number;
|
||||||
|
defaultMinOsVersion: string;
|
||||||
|
defaultDeadline: string;
|
||||||
|
inAccordion?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const OsMinVersionForm = ({
|
const MacOSTargetForm = ({
|
||||||
currentTeamId = APP_CONTEXT_NO_TEAM_ID,
|
currentTeamId,
|
||||||
}: IOsMinVersionForm) => {
|
defaultMinOsVersion,
|
||||||
|
defaultDeadline,
|
||||||
|
inAccordion = false,
|
||||||
|
}: IMacOSTargetFormProps) => {
|
||||||
const { renderFlash } = useContext(NotificationContext);
|
const { renderFlash } = useContext(NotificationContext);
|
||||||
const { config } = useContext(AppContext);
|
|
||||||
|
|
||||||
const [isSaving, setIsSaving] = useState(false);
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
const [minOsVersion, setMinOsVersion] = useState(
|
const [minOsVersion, setMinOsVersion] = useState(defaultMinOsVersion);
|
||||||
currentTeamId === APP_CONTEXT_NO_TEAM_ID
|
const [deadline, setDeadline] = useState(defaultDeadline);
|
||||||
? config?.mdm.macos_updates.minimum_version ?? ""
|
|
||||||
: ""
|
|
||||||
);
|
|
||||||
const [deadline, setDeadline] = useState(
|
|
||||||
currentTeamId === APP_CONTEXT_NO_TEAM_ID
|
|
||||||
? config?.mdm.macos_updates.deadline ?? ""
|
|
||||||
: ""
|
|
||||||
);
|
|
||||||
const [minOsVersionError, setMinOsVersionError] = useState<
|
const [minOsVersionError, setMinOsVersionError] = useState<
|
||||||
string | undefined
|
string | undefined
|
||||||
>();
|
>();
|
||||||
const [deadlineError, setDeadlineError] = useState<string | undefined>();
|
const [deadlineError, setDeadlineError] = useState<string | undefined>();
|
||||||
|
|
||||||
useQuery<ILoadTeamResponse, Error>(
|
const classNames = classnames(baseClass, {
|
||||||
["apple mdm config", currentTeamId],
|
[`${baseClass}__accordion-form`]: inAccordion,
|
||||||
|
});
|
||||||
// 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 handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@ -141,7 +123,7 @@ const OsMinVersionForm = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form className={baseClass} onSubmit={handleSubmit}>
|
<form className={classNames} onSubmit={handleSubmit}>
|
||||||
<InputField
|
<InputField
|
||||||
label="Minimum version"
|
label="Minimum version"
|
||||||
tooltip="The end user sees the window until their macOS is at or above this 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 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 baseClass = "nudge-preview";
|
||||||
|
|
||||||
const NudgePreview = () => {
|
interface INudgeDescriptionProps {
|
||||||
return (
|
platform: OSUpdatesSupportedPlatform;
|
||||||
<div className={baseClass}>
|
}
|
||||||
<h2>End user experience</h2>
|
const NudgeDescription = ({ platform }: INudgeDescriptionProps) => {
|
||||||
|
return platform === "darwin" ? (
|
||||||
|
<>
|
||||||
<p>
|
<p>
|
||||||
When a minimum version is saved, the end user sees the below window
|
When a minimum version is saved, the end user sees the below window
|
||||||
until their macOS version is at or above the minimum version.
|
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"
|
url="https://fleetdm.com/docs/using-fleet/mdm-macos-updates"
|
||||||
newTab
|
newTab
|
||||||
/>
|
/>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<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
|
<img
|
||||||
className={`${baseClass}__preview-img`}
|
className={`${baseClass}__preview-img`}
|
||||||
src={OsUpdateScreenshot}
|
src={
|
||||||
|
platform === "darwin" ? MacOSUpdateScreenshot : WindowsUpdateScreenshot
|
||||||
|
}
|
||||||
alt="OS update preview screenshot"
|
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>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,8 +1,11 @@
|
|||||||
.nudge-preview {
|
.nudge-preview {
|
||||||
|
box-sizing: border-box;
|
||||||
background-color: $ui-off-white;
|
background-color: $ui-off-white;
|
||||||
border-radius: $border-radius;
|
border-radius: $border-radius;
|
||||||
border: 1px solid $ui-fleet-black-10;
|
border: 1px solid $ui-fleet-black-10;
|
||||||
padding: $pad-xxlarge;
|
padding: $pad-xxlarge;
|
||||||
|
max-width: 640px;
|
||||||
|
flex-grow: 1;
|
||||||
|
|
||||||
&__preview-img {
|
&__preview-img {
|
||||||
margin-top: $pad-xxlarge;
|
margin-top: $pad-xxlarge;
|
||||||
@ -11,4 +14,8 @@
|
|||||||
max-width: 540px;
|
max-width: 540px;
|
||||||
margin: 40px auto 0;
|
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 { NotificationContext } from "context/notification";
|
||||||
|
|
||||||
import Spinner from "components/Spinner";
|
import Spinner from "components/Spinner";
|
||||||
|
import SectionHeader from "components/SectionHeader";
|
||||||
|
|
||||||
import BootstrapPackagePreview from "./components/BootstrapPackagePreview";
|
import BootstrapPackagePreview from "./components/BootstrapPackagePreview";
|
||||||
import PackageUploader from "./components/BootstrapPackageUploader";
|
import PackageUploader from "./components/BootstrapPackageUploader";
|
||||||
import UploadedPackageView from "./components/UploadedPackageView";
|
import UploadedPackageView from "./components/UploadedPackageView";
|
||||||
@ -65,7 +67,7 @@ const BootstrapPackage = ({ currentTeamId }: IBootstrapPackageProps) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={baseClass}>
|
<div className={baseClass}>
|
||||||
<h2>Bootstrap package</h2>
|
<SectionHeader title="Bootstrap package" />
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<Spinner />
|
<Spinner />
|
||||||
) : (
|
) : (
|
||||||
|
@ -1,13 +1,4 @@
|
|||||||
.bootstrap-package {
|
.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 {
|
&__content {
|
||||||
max-width: $break-xxl;
|
max-width: $break-xxl;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
|
@ -1,16 +1,17 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { InjectedRouter } from "react-router";
|
import { InjectedRouter } from "react-router";
|
||||||
import PATHS from "router/paths";
|
import PATHS from "router/paths";
|
||||||
|
import { useQuery } from "react-query";
|
||||||
|
|
||||||
import configAPI from "services/entities/config";
|
import configAPI from "services/entities/config";
|
||||||
import teamsAPI, { ILoadTeamResponse } from "services/entities/teams";
|
import teamsAPI, { ILoadTeamResponse } from "services/entities/teams";
|
||||||
import { IConfig, IMdmConfig } from "interfaces/config";
|
import { IConfig, IMdmConfig } from "interfaces/config";
|
||||||
|
import { ITeamConfig } from "interfaces/team";
|
||||||
|
|
||||||
import SectionHeader from "components/SectionHeader/SectionHeader";
|
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 Spinner from "components/Spinner";
|
||||||
|
import EndUserExperiencePreview from "pages/ManageControlsPage/components/EndUserExperiencePreview";
|
||||||
|
|
||||||
import RequireEndUserAuth from "./components/RequireEndUserAuth/RequireEndUserAuth";
|
import RequireEndUserAuth from "./components/RequireEndUserAuth/RequireEndUserAuth";
|
||||||
import EndUserAuthForm from "./components/EndUserAuthForm/EndUserAuthForm";
|
import EndUserAuthForm from "./components/EndUserAuthForm/EndUserAuthForm";
|
||||||
|
|
||||||
|
@ -21,12 +21,13 @@ export interface IGetOSVersionsRequest {
|
|||||||
export interface IGetOSVersionsQueryKey extends IGetOSVersionsRequest {
|
export interface IGetOSVersionsQueryKey extends IGetOSVersionsRequest {
|
||||||
scope: string;
|
scope: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IOSVersionsResponse {
|
export interface IOSVersionsResponse {
|
||||||
counts_updated_at: string;
|
counts_updated_at: string;
|
||||||
os_versions: IOperatingSystemVersion[];
|
os_versions: IOperatingSystemVersion[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getOSVersions = async ({
|
export const getOSVersions = ({
|
||||||
id,
|
id,
|
||||||
platform,
|
platform,
|
||||||
teamId,
|
teamId,
|
||||||
|
@ -41,10 +41,14 @@ export interface IUpdateTeamFormData {
|
|||||||
webhook_settings: Partial<ITeamWebhookSettings>;
|
webhook_settings: Partial<ITeamWebhookSettings>;
|
||||||
integrations: IIntegrations;
|
integrations: IIntegrations;
|
||||||
mdm: {
|
mdm: {
|
||||||
macos_updates: {
|
macos_updates?: {
|
||||||
minimum_version: string;
|
minimum_version: string;
|
||||||
deadline: 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
|
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 {
|
type Slice[T any] struct {
|
||||||
Set bool
|
Set bool
|
||||||
Valid bool
|
Valid bool
|
||||||
|
@ -90,7 +90,7 @@ func TestString(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestBool(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 {
|
cases := []struct {
|
||||||
data string
|
data string
|
||||||
wantErr 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) {
|
func TestSlice(t *testing.T) {
|
||||||
t.Run("slice of ints", func(t *testing.T) {
|
t.Run("slice of ints", func(t *testing.T) {
|
||||||
cases := []struct {
|
cases := []struct {
|
||||||
|
@ -841,7 +841,6 @@ func testUpdateHostTablesOnMDMUnenroll(t *testing.T, ds *Datastore) {
|
|||||||
func expectAppleProfiles(
|
func expectAppleProfiles(
|
||||||
t *testing.T,
|
t *testing.T,
|
||||||
ds *Datastore,
|
ds *Datastore,
|
||||||
newSet []*fleet.MDMAppleConfigProfile,
|
|
||||||
tmID *uint,
|
tmID *uint,
|
||||||
want []*fleet.MDMAppleConfigProfile,
|
want []*fleet.MDMAppleConfigProfile,
|
||||||
) map[string]uint {
|
) map[string]uint {
|
||||||
@ -878,7 +877,7 @@ func testBatchSetMDMAppleProfiles(t *testing.T, ds *Datastore) {
|
|||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
err := ds.BatchSetMDMAppleProfiles(ctx, tmID, newSet)
|
err := ds.BatchSetMDMAppleProfiles(ctx, tmID, newSet)
|
||||||
require.NoError(t, err)
|
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 {
|
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/contexts/ctxerr"
|
||||||
"github.com/fleetdm/fleet/v4/server/fleet"
|
"github.com/fleetdm/fleet/v4/server/fleet"
|
||||||
"github.com/fleetdm/fleet/v4/server/mdm/apple/mobileconfig"
|
"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/go-kit/kit/log/level"
|
||||||
"github.com/jmoiron/sqlx"
|
"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) {
|
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
|
var profs []*fleet.MDMConfigProfilePayload
|
||||||
|
|
||||||
const selectStmt = `
|
const selectStmt = `
|
||||||
@ -142,7 +146,8 @@ FROM (
|
|||||||
FROM
|
FROM
|
||||||
mdm_windows_configuration_profiles
|
mdm_windows_configuration_profiles
|
||||||
WHERE
|
WHERE
|
||||||
team_id = ?
|
team_id = ? AND
|
||||||
|
name NOT IN (?)
|
||||||
) as combined_profiles
|
) as combined_profiles
|
||||||
`
|
`
|
||||||
|
|
||||||
@ -156,8 +161,13 @@ FROM (
|
|||||||
for k := range fleetIdentsMap {
|
for k := range fleetIdentsMap {
|
||||||
fleetIdentifiers = append(fleetIdentifiers, k)
|
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 := appendListOptionsWithCursorToSQL(selectStmt, args, &opt)
|
||||||
|
|
||||||
stmt, args, err := sqlx.In(stmt, args...)
|
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/fleet"
|
||||||
"github.com/fleetdm/fleet/v4/server/mdm/apple/mobileconfig"
|
"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/ptr"
|
||||||
"github.com/fleetdm/fleet/v4/server/test"
|
"github.com/fleetdm/fleet/v4/server/test"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
@ -183,8 +184,8 @@ func testBatchSetMDMProfiles(t *testing.T, ds *Datastore) {
|
|||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
err := ds.BatchSetMDMProfiles(ctx, tmID, newAppleSet, newWindowsSet)
|
err := ds.BatchSetMDMProfiles(ctx, tmID, newAppleSet, newWindowsSet)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
expectAppleProfiles(t, ds, newAppleSet, tmID, wantApple)
|
expectAppleProfiles(t, ds, tmID, wantApple)
|
||||||
expectWindowsProfiles(t, ds, newWindowsSet, tmID, wantWindows)
|
expectWindowsProfiles(t, ds, tmID, wantWindows)
|
||||||
}
|
}
|
||||||
|
|
||||||
withTeamIDApple := func(p *fleet.MDMAppleConfigProfile, tmID uint) *fleet.MDMAppleConfigProfile {
|
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.Len(t, profs, 0)
|
||||||
require.Equal(t, *meta, fleet.PaginationMetadata{})
|
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() {
|
for idf := range mobileconfig.FleetPayloadIdentifiers() {
|
||||||
_, err = ds.NewMDMAppleConfigProfile(ctx, *generateCP("name_"+idf, idf, team.ID))
|
_, err = ds.NewMDMAppleConfigProfile(ctx, *generateCP("name_"+idf, idf, team.ID))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@ -324,6 +325,25 @@ func testListMDMConfigProfiles(t *testing.T, ds *Datastore) {
|
|||||||
require.Len(t, profs, 0)
|
require.Len(t, profs, 0)
|
||||||
require.Equal(t, *meta, fleet.PaginationMetadata{})
|
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
|
// create a mac profile for global and a Windows profile for team
|
||||||
profA, err := ds.NewMDMAppleConfigProfile(ctx, *generateCP("A", "A", 0))
|
profA, err := ds.NewMDMAppleConfigProfile(ctx, *generateCP("A", "A", 0))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
@ -700,6 +700,18 @@ func (ds *Datastore) DeleteMDMWindowsConfigProfile(ctx context.Context, profileU
|
|||||||
return nil
|
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{}) {
|
func subqueryHostsMDMWindowsOSSettingsStatusFailed() (string, []interface{}) {
|
||||||
sql := `
|
sql := `
|
||||||
SELECT
|
SELECT
|
||||||
@ -1358,6 +1370,51 @@ INSERT INTO
|
|||||||
}, nil
|
}, 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(
|
func (ds *Datastore) batchSetMDMWindowsProfilesDB(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
tx sqlx.ExtContext,
|
tx sqlx.ExtContext,
|
||||||
|
@ -33,6 +33,7 @@ func TestMDMWindows(t *testing.T) {
|
|||||||
{"TestBulkOperationsMDMWindowsHostProfilesBatch3", testBulkOperationsMDMWindowsHostProfilesBatch3},
|
{"TestBulkOperationsMDMWindowsHostProfilesBatch3", testBulkOperationsMDMWindowsHostProfilesBatch3},
|
||||||
{"TestGetMDMWindowsProfilesContents", testGetMDMWindowsProfilesContents},
|
{"TestGetMDMWindowsProfilesContents", testGetMDMWindowsProfilesContents},
|
||||||
{"TestMDMWindowsConfigProfiles", testMDMWindowsConfigProfiles},
|
{"TestMDMWindowsConfigProfiles", testMDMWindowsConfigProfiles},
|
||||||
|
{"TestSetOrReplaceMDMWindowsConfigProfile", testSetOrReplaceMDMWindowsConfigProfile},
|
||||||
{"TestMDMWindowsDiskEncryption", testMDMWindowsDiskEncryption},
|
{"TestMDMWindowsDiskEncryption", testMDMWindowsDiskEncryption},
|
||||||
{"TestMDMWindowsProfilesSummary", testMDMWindowsProfilesSummary},
|
{"TestMDMWindowsProfilesSummary", testMDMWindowsProfilesSummary},
|
||||||
{"TestBatchSetMDMWindowsProfiles", testBatchSetMDMWindowsProfiles},
|
{"TestBatchSetMDMWindowsProfiles", testBatchSetMDMWindowsProfiles},
|
||||||
@ -1800,10 +1801,74 @@ func testMDMWindowsConfigProfiles(t *testing.T, ds *Datastore) {
|
|||||||
require.NoError(t, err)
|
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(
|
func expectWindowsProfiles(
|
||||||
t *testing.T,
|
t *testing.T,
|
||||||
ds *Datastore,
|
ds *Datastore,
|
||||||
newSet []*fleet.MDMWindowsConfigProfile,
|
|
||||||
tmID *uint,
|
tmID *uint,
|
||||||
want []*fleet.MDMWindowsConfigProfile,
|
want []*fleet.MDMWindowsConfigProfile,
|
||||||
) map[string]string {
|
) map[string]string {
|
||||||
@ -1844,7 +1909,7 @@ func testBatchSetMDMWindowsProfiles(t *testing.T, ds *Datastore) {
|
|||||||
return ds.batchSetMDMWindowsProfilesDB(ctx, tx, tmID, newSet)
|
return ds.batchSetMDMWindowsProfilesDB(ctx, tx, tmID, newSet)
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
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 {
|
withTeamID := func(p *fleet.MDMWindowsConfigProfile, tmID uint) *fleet.MDMWindowsConfigProfile {
|
||||||
|
@ -41,7 +41,7 @@ CREATE TABLE `app_config_json` (
|
|||||||
UNIQUE KEY `id` (`id`)
|
UNIQUE KEY `id` (`id`)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
/*!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 @saved_cs_client = @@character_set_client */;
|
||||||
/*!40101 SET character_set_client = utf8 */;
|
/*!40101 SET character_set_client = utf8 */;
|
||||||
CREATE TABLE `carve_blocks` (
|
CREATE TABLE `carve_blocks` (
|
||||||
|
@ -586,6 +586,10 @@ func testTeamsMDMConfig(t *testing.T, ds *Datastore) {
|
|||||||
MinimumVersion: optjson.SetString("10.15.0"),
|
MinimumVersion: optjson.SetString("10.15.0"),
|
||||||
Deadline: optjson.SetString("2025-10-01"),
|
Deadline: optjson.SetString("2025-10-01"),
|
||||||
},
|
},
|
||||||
|
WindowsUpdates: fleet.WindowsUpdates{
|
||||||
|
DeadlineDays: optjson.SetInt(7),
|
||||||
|
GracePeriodDays: optjson.SetInt(3),
|
||||||
|
},
|
||||||
MacOSSetup: fleet.MacOSSetup{
|
MacOSSetup: fleet.MacOSSetup{
|
||||||
BootstrapPackage: optjson.SetString("bootstrap"),
|
BootstrapPackage: optjson.SetString("bootstrap"),
|
||||||
MacOSSetupAssistant: optjson.SetString("assistant"),
|
MacOSSetupAssistant: optjson.SetString("assistant"),
|
||||||
@ -605,6 +609,10 @@ func testTeamsMDMConfig(t *testing.T, ds *Datastore) {
|
|||||||
MinimumVersion: optjson.SetString("10.15.0"),
|
MinimumVersion: optjson.SetString("10.15.0"),
|
||||||
Deadline: optjson.SetString("2025-10-01"),
|
Deadline: optjson.SetString("2025-10-01"),
|
||||||
},
|
},
|
||||||
|
WindowsUpdates: fleet.WindowsUpdates{
|
||||||
|
DeadlineDays: optjson.SetInt(7),
|
||||||
|
GracePeriodDays: optjson.SetInt(3),
|
||||||
|
},
|
||||||
MacOSSetup: fleet.MacOSSetup{
|
MacOSSetup: fleet.MacOSSetup{
|
||||||
BootstrapPackage: optjson.SetString("bootstrap"),
|
BootstrapPackage: optjson.SetString("bootstrap"),
|
||||||
MacOSSetupAssistant: optjson.SetString("assistant"),
|
MacOSSetupAssistant: optjson.SetString("assistant"),
|
||||||
|
@ -49,6 +49,7 @@ var ActivityDetailsList = []ActivityDetails{
|
|||||||
ActivityTypeMDMUnenrolled{},
|
ActivityTypeMDMUnenrolled{},
|
||||||
|
|
||||||
ActivityTypeEditedMacOSMinVersion{},
|
ActivityTypeEditedMacOSMinVersion{},
|
||||||
|
ActivityTypeEditedWindowsUpdates{},
|
||||||
|
|
||||||
ActivityTypeReadHostDiskEncryptionKey{},
|
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 {
|
type ActivityTypeReadHostDiskEncryptionKey struct {
|
||||||
HostID uint `json:"host_id"`
|
HostID uint `json:"host_id"`
|
||||||
HostDisplayName string `json:"host_display_name"`
|
HostDisplayName string `json:"host_display_name"`
|
||||||
|
@ -148,6 +148,8 @@ type MDM struct {
|
|||||||
EnabledAndConfigured bool `json:"enabled_and_configured"`
|
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"`
|
MacOSSettings MacOSSettings `json:"macos_settings"`
|
||||||
MacOSSetup MacOSSetup `json:"macos_setup"`
|
MacOSSetup MacOSSetup `json:"macos_setup"`
|
||||||
MacOSMigration MacOSMigration `json:"macos_migration"`
|
MacOSMigration MacOSMigration `json:"macos_migration"`
|
||||||
@ -231,6 +233,68 @@ func (m MacOSUpdates) Validate() error {
|
|||||||
return nil
|
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.
|
// MacOSSettings contains settings specific to macOS.
|
||||||
type MacOSSettings struct {
|
type MacOSSettings struct {
|
||||||
// CustomSettings is a slice of configuration profile file paths.
|
// 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) {
|
func TestMacOSUpdatesEnabledForHost(t *testing.T) {
|
||||||
hostWithRequirements := &Host{
|
hostWithRequirements := &Host{
|
||||||
OsqueryHostID: ptr.String("notempty"),
|
OsqueryHostID: ptr.String("notempty"),
|
||||||
|
@ -1077,6 +1077,10 @@ type Datastore interface {
|
|||||||
// the specified profile uuid.
|
// the specified profile uuid.
|
||||||
DeleteMDMWindowsConfigProfile(ctx context.Context, profileUUID string) error
|
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 returns the MDM profile information for the specified Windows host UUID.
|
||||||
GetHostMDMWindowsProfiles(ctx context.Context, hostUUID string) ([]HostMDMWindowsProfile, error)
|
GetHostMDMWindowsProfiles(ctx context.Context, hostUUID string) ([]HostMDMWindowsProfile, error)
|
||||||
|
|
||||||
@ -1137,6 +1141,11 @@ type Datastore interface {
|
|||||||
// NewMDMWindowsConfigProfile creates and returns a new configuration profile.
|
// NewMDMWindowsConfigProfile creates and returns a new configuration profile.
|
||||||
NewMDMWindowsConfigProfile(ctx context.Context, cp MDMWindowsConfigProfile) (*MDMWindowsConfigProfile, error)
|
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
|
// BatchSetMDMProfiles sets the MDM Apple or Windows profiles for the given team or
|
||||||
// no team in a single transaction.
|
// no team in a single transaction.
|
||||||
BatchSetMDMProfiles(ctx context.Context, tmID *uint, macProfiles []*MDMAppleConfigProfile, winProfiles []*MDMWindowsConfigProfile) error
|
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
|
DeleteMDMAppleSetupAssistant func(ctx context.Context, teamID *uint) error
|
||||||
MDMAppleSyncDEPProfiles func(ctx context.Context) error
|
MDMAppleSyncDEPProfiles func(ctx context.Context) error
|
||||||
DeleteMDMAppleBootstrapPackage func(ctx context.Context, teamID *uint) 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 {
|
type OsqueryService interface {
|
||||||
|
@ -34,6 +34,7 @@ type TeamPayload struct {
|
|||||||
type TeamPayloadMDM struct {
|
type TeamPayloadMDM struct {
|
||||||
EnableDiskEncryption optjson.Bool `json:"enable_disk_encryption"`
|
EnableDiskEncryption optjson.Bool `json:"enable_disk_encryption"`
|
||||||
MacOSUpdates *MacOSUpdates `json:"macos_updates"`
|
MacOSUpdates *MacOSUpdates `json:"macos_updates"`
|
||||||
|
WindowsUpdates *WindowsUpdates `json:"windows_updates"`
|
||||||
MacOSSettings *MacOSSettings `json:"macos_settings"`
|
MacOSSettings *MacOSSettings `json:"macos_settings"`
|
||||||
MacOSSetup *MacOSSetup `json:"macos_setup"`
|
MacOSSetup *MacOSSetup `json:"macos_setup"`
|
||||||
WindowsSettings *WindowsSettings `json:"windows_settings"`
|
WindowsSettings *WindowsSettings `json:"windows_settings"`
|
||||||
@ -151,6 +152,7 @@ type TeamWebhookSettings struct {
|
|||||||
type TeamMDM struct {
|
type TeamMDM struct {
|
||||||
EnableDiskEncryption bool `json:"enable_disk_encryption"`
|
EnableDiskEncryption bool `json:"enable_disk_encryption"`
|
||||||
MacOSUpdates MacOSUpdates `json:"macos_updates"`
|
MacOSUpdates MacOSUpdates `json:"macos_updates"`
|
||||||
|
WindowsUpdates WindowsUpdates `json:"windows_updates"`
|
||||||
MacOSSettings MacOSSettings `json:"macos_settings"`
|
MacOSSettings MacOSSettings `json:"macos_settings"`
|
||||||
MacOSSetup MacOSSetup `json:"macos_setup"`
|
MacOSSetup MacOSSetup `json:"macos_setup"`
|
||||||
|
|
||||||
@ -200,6 +202,7 @@ type TeamSpecMDM struct {
|
|||||||
EnableDiskEncryption optjson.Bool `json:"enable_disk_encryption"`
|
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
|
// 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
|
// 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
|
var mdmSpec TeamSpecMDM
|
||||||
mdmSpec.MacOSUpdates = t.Config.MDM.MacOSUpdates
|
mdmSpec.MacOSUpdates = t.Config.MDM.MacOSUpdates
|
||||||
|
mdmSpec.WindowsUpdates = t.Config.MDM.WindowsUpdates
|
||||||
mdmSpec.MacOSSettings = t.Config.MDM.MacOSSettings.ToMap()
|
mdmSpec.MacOSSettings = t.Config.MDM.MacOSSettings.ToMap()
|
||||||
delete(mdmSpec.MacOSSettings, "enable_disk_encryption")
|
delete(mdmSpec.MacOSSettings, "enable_disk_encryption")
|
||||||
mdmSpec.MacOSSetup = t.Config.MDM.MacOSSetup
|
mdmSpec.MacOSSetup = t.Config.MDM.MacOSSetup
|
||||||
|
@ -45,6 +45,10 @@ type MDMWindowsConfigProfile struct {
|
|||||||
//
|
//
|
||||||
// Returns an error if these conditions are not met.
|
// Returns an error if these conditions are not met.
|
||||||
func (m *MDMWindowsConfigProfile) ValidateUserProvided() error {
|
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" {
|
if mdm.GetRawProfilePlatform(m.SyncML) != "windows" {
|
||||||
// it doesn't start with <Replace>, check if it is still valid XML.
|
// it doesn't start with <Replace>, check if it is still valid XML.
|
||||||
if len(bytes.TrimSpace(m.SyncML)) == 0 {
|
if len(bytes.TrimSpace(m.SyncML)) == 0 {
|
||||||
|
@ -3,6 +3,7 @@ package fleet
|
|||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
microsoft_mdm "github.com/fleetdm/fleet/v4/server/mdm/microsoft"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -140,6 +141,14 @@ func TestValidateUserProvided(t *testing.T) {
|
|||||||
},
|
},
|
||||||
wantErr: true,
|
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 {
|
for _, tt := range tests {
|
||||||
|
@ -398,8 +398,16 @@ const (
|
|||||||
const (
|
const (
|
||||||
FleetBitLockerTargetLocURI = "/Vendor/MSFT/BitLocker"
|
FleetBitLockerTargetLocURI = "/Vendor/MSFT/BitLocker"
|
||||||
FleetOSUpdateTargetLocURI = "/Vendor/MSFT/Policy/Config/Update"
|
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) {
|
func ResolveWindowsMDMDiscovery(serverURL string) (string, error) {
|
||||||
return commonmdm.ResolveURL(serverURL, MDE2DiscoveryPath, false)
|
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 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 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)
|
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 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 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)
|
type NewHostScriptExecutionRequestFunc func(ctx context.Context, request *fleet.HostScriptRequestPayload) (*fleet.HostScriptResult, error)
|
||||||
@ -1784,6 +1788,9 @@ type DataStore struct {
|
|||||||
DeleteMDMWindowsConfigProfileFunc DeleteMDMWindowsConfigProfileFunc
|
DeleteMDMWindowsConfigProfileFunc DeleteMDMWindowsConfigProfileFunc
|
||||||
DeleteMDMWindowsConfigProfileFuncInvoked bool
|
DeleteMDMWindowsConfigProfileFuncInvoked bool
|
||||||
|
|
||||||
|
DeleteMDMWindowsConfigProfileByTeamAndNameFunc DeleteMDMWindowsConfigProfileByTeamAndNameFunc
|
||||||
|
DeleteMDMWindowsConfigProfileByTeamAndNameFuncInvoked bool
|
||||||
|
|
||||||
GetHostMDMWindowsProfilesFunc GetHostMDMWindowsProfilesFunc
|
GetHostMDMWindowsProfilesFunc GetHostMDMWindowsProfilesFunc
|
||||||
GetHostMDMWindowsProfilesFuncInvoked bool
|
GetHostMDMWindowsProfilesFuncInvoked bool
|
||||||
|
|
||||||
@ -1823,6 +1830,9 @@ type DataStore struct {
|
|||||||
NewMDMWindowsConfigProfileFunc NewMDMWindowsConfigProfileFunc
|
NewMDMWindowsConfigProfileFunc NewMDMWindowsConfigProfileFunc
|
||||||
NewMDMWindowsConfigProfileFuncInvoked bool
|
NewMDMWindowsConfigProfileFuncInvoked bool
|
||||||
|
|
||||||
|
SetOrUpdateMDMWindowsConfigProfileFunc SetOrUpdateMDMWindowsConfigProfileFunc
|
||||||
|
SetOrUpdateMDMWindowsConfigProfileFuncInvoked bool
|
||||||
|
|
||||||
BatchSetMDMProfilesFunc BatchSetMDMProfilesFunc
|
BatchSetMDMProfilesFunc BatchSetMDMProfilesFunc
|
||||||
BatchSetMDMProfilesFuncInvoked bool
|
BatchSetMDMProfilesFuncInvoked bool
|
||||||
|
|
||||||
@ -4263,6 +4273,13 @@ func (s *DataStore) DeleteMDMWindowsConfigProfile(ctx context.Context, profileUU
|
|||||||
return s.DeleteMDMWindowsConfigProfileFunc(ctx, profileUUID)
|
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) {
|
func (s *DataStore) GetHostMDMWindowsProfiles(ctx context.Context, hostUUID string) ([]fleet.HostMDMWindowsProfile, error) {
|
||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
s.GetHostMDMWindowsProfilesFuncInvoked = true
|
s.GetHostMDMWindowsProfilesFuncInvoked = true
|
||||||
@ -4354,6 +4371,13 @@ func (s *DataStore) NewMDMWindowsConfigProfile(ctx context.Context, cp fleet.MDM
|
|||||||
return s.NewMDMWindowsConfigProfileFunc(ctx, cp)
|
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 {
|
func (s *DataStore) BatchSetMDMProfiles(ctx context.Context, tmID *uint, macProfiles []*fleet.MDMAppleConfigProfile, winProfiles []*fleet.MDMWindowsConfigProfile) error {
|
||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
s.BatchSetMDMProfilesFuncInvoked = true
|
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 appConfig.MDM.EnableDiskEncryption.Valid && oldAppConfig.MDM.EnableDiskEncryption.Value != appConfig.MDM.EnableDiskEncryption.Value {
|
||||||
if oldAppConfig.MDM.EnabledAndConfigured {
|
if oldAppConfig.MDM.EnabledAndConfigured {
|
||||||
var act fleet.ActivityDetails
|
var act fleet.ActivityDetails
|
||||||
@ -691,6 +722,20 @@ func (svc *Service) validateMDM(
|
|||||||
invalid.Append("macos_updates", err.Error())
|
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
|
// EndUserAuthentication
|
||||||
// only validate SSO settings if they changed
|
// only validate SSO settings if they changed
|
||||||
if mdm.EndUserAuthentication.SSOProviderSettings != oldMdm.EndUserAuthentication.SSOProviderSettings {
|
if mdm.EndUserAuthentication.SSOProviderSettings != oldMdm.EndUserAuthentication.SSOProviderSettings {
|
||||||
|
@ -812,6 +812,7 @@ func TestMDMAppleConfig(t *testing.T) {
|
|||||||
expectedMDM: fleet.MDM{
|
expectedMDM: fleet.MDM{
|
||||||
MacOSSetup: fleet.MacOSSetup{BootstrapPackage: optjson.String{Set: true}, MacOSSetupAssistant: optjson.String{Set: true}},
|
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}},
|
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},
|
EnableDiskEncryption: optjson.Bool{Set: true, Valid: false},
|
||||||
WindowsSettings: fleet.WindowsSettings{CustomSettings: optjson.Slice[string]{Set: true, Value: []string{}}},
|
WindowsSettings: fleet.WindowsSettings{CustomSettings: optjson.Slice[string]{Set: true, Value: []string{}}},
|
||||||
},
|
},
|
||||||
@ -840,6 +841,7 @@ func TestMDMAppleConfig(t *testing.T) {
|
|||||||
AppleBMDefaultTeam: "foobar",
|
AppleBMDefaultTeam: "foobar",
|
||||||
MacOSSetup: fleet.MacOSSetup{BootstrapPackage: optjson.String{Set: true}, MacOSSetupAssistant: optjson.String{Set: true}},
|
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}},
|
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},
|
EnableDiskEncryption: optjson.Bool{Set: true, Valid: false},
|
||||||
WindowsSettings: fleet.WindowsSettings{CustomSettings: optjson.Slice[string]{Set: true, Value: []string{}}},
|
WindowsSettings: fleet.WindowsSettings{CustomSettings: optjson.Slice[string]{Set: true, Value: []string{}}},
|
||||||
},
|
},
|
||||||
@ -853,6 +855,7 @@ func TestMDMAppleConfig(t *testing.T) {
|
|||||||
AppleBMDefaultTeam: "foobar",
|
AppleBMDefaultTeam: "foobar",
|
||||||
MacOSSetup: fleet.MacOSSetup{BootstrapPackage: optjson.String{Set: true}, MacOSSetupAssistant: optjson.String{Set: true}},
|
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}},
|
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},
|
EnableDiskEncryption: optjson.Bool{Set: true, Valid: false},
|
||||||
WindowsSettings: fleet.WindowsSettings{CustomSettings: optjson.Slice[string]{Set: true, Value: []string{}}},
|
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"}},
|
EndUserAuthentication: fleet.MDMEndUserAuthentication{SSOProviderSettings: fleet.SSOProviderSettings{EntityID: "foo"}},
|
||||||
MacOSSetup: fleet.MacOSSetup{BootstrapPackage: optjson.String{Set: true}, MacOSSetupAssistant: optjson.String{Set: true}},
|
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}},
|
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},
|
EnableDiskEncryption: optjson.Bool{Set: true, Valid: false},
|
||||||
WindowsSettings: fleet.WindowsSettings{CustomSettings: optjson.Slice[string]{Set: true, Value: []string{}}},
|
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}},
|
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}},
|
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},
|
EnableDiskEncryption: optjson.Bool{Set: true, Valid: false},
|
||||||
WindowsSettings: fleet.WindowsSettings{CustomSettings: optjson.Slice[string]{Set: true, Value: []string{}}},
|
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" }
|
"mdm": { "apple_bm_default_team": "xyz" }
|
||||||
}`), http.StatusUnprocessableEntity, &acResp)
|
}`), 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
|
// try to enable Windows MDM, impossible without the feature flag
|
||||||
// (only set in mdm integrations tests)
|
// (only set in mdm integrations tests)
|
||||||
res = s.Do("PATCH", "/api/latest/fleet/config", json.RawMessage(`{
|
res = s.Do("PATCH", "/api/latest/fleet/config", json.RawMessage(`{
|
||||||
|
@ -3,8 +3,10 @@ package service
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"database/sql"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
@ -22,6 +24,7 @@ import (
|
|||||||
"github.com/fleetdm/fleet/v4/server/datastore/redis/redistest"
|
"github.com/fleetdm/fleet/v4/server/datastore/redis/redistest"
|
||||||
"github.com/fleetdm/fleet/v4/server/fleet"
|
"github.com/fleetdm/fleet/v4/server/fleet"
|
||||||
"github.com/fleetdm/fleet/v4/server/live_query/live_query_mock"
|
"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/ptr"
|
||||||
"github.com/fleetdm/fleet/v4/server/test"
|
"github.com/fleetdm/fleet/v4/server/test"
|
||||||
"github.com/go-kit/log"
|
"github.com/go-kit/log"
|
||||||
@ -130,6 +133,10 @@ func (s *integrationEnterpriseTestSuite) TestTeamSpecs() {
|
|||||||
MinimumVersion: optjson.SetString("10.15.0"),
|
MinimumVersion: optjson.SetString("10.15.0"),
|
||||||
Deadline: optjson.SetString("2021-01-01"),
|
Deadline: optjson.SetString("2021-01-01"),
|
||||||
},
|
},
|
||||||
|
WindowsUpdates: fleet.WindowsUpdates{
|
||||||
|
DeadlineDays: optjson.Int{Set: true},
|
||||||
|
GracePeriodDays: optjson.Int{Set: true},
|
||||||
|
},
|
||||||
MacOSSetup: fleet.MacOSSetup{
|
MacOSSetup: fleet.MacOSSetup{
|
||||||
// because the MacOSSetup was marshalled to JSON to be saved in the DB,
|
// 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
|
// 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
|
// 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)
|
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
|
// dry-run with invalid agent options
|
||||||
agentOpts = json.RawMessage(`{"config": {"nope": 1}}`)
|
agentOpts = json.RawMessage(`{"config": {"nope": 1}}`)
|
||||||
teamSpecs = map[string]any{
|
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")
|
s.Do("POST", "/api/latest/fleet/spec/teams", teamSpecs, http.StatusBadRequest, "dry_run", "true")
|
||||||
|
|
||||||
// dry-run with empty body
|
// 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)
|
errBody, err := io.ReadAll(res.Body)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Contains(t, string(errBody), `"Expected JSON Body"`)
|
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")
|
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.")
|
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
|
// 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)
|
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()
|
t := s.T()
|
||||||
|
|
||||||
// Create a team
|
// Create a team
|
||||||
@ -1734,6 +2024,24 @@ func (s *integrationEnterpriseTestSuite) TestMacOSUpdatesConfig() {
|
|||||||
require.Equal(t, "2025-10-01", tmResp.Team.Config.MDM.MacOSUpdates.Deadline.Value)
|
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)
|
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
|
// 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{
|
s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/teams/%d", team.ID), map[string]any{
|
||||||
"mdm": nil,
|
"mdm": nil,
|
||||||
@ -2040,6 +2348,165 @@ func (s *integrationEnterpriseTestSuite) TestDefaultAppleBMTeam() {
|
|||||||
require.Equal(t, tm.Name, acResp.MDM.AppleBMDefaultTeam)
|
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() {
|
func (s *integrationEnterpriseTestSuite) TestMDMMacOSUpdates() {
|
||||||
t := s.T()
|
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, "fleet_detail_query_software_linux")
|
||||||
require.Contains(t, dqResp.Queries, fmt.Sprintf("fleet_distributed_query_%s", t.Name()))
|
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
|
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
|
var tmPtr *uint
|
||||||
if teamID > 0 {
|
if teamID > 0 {
|
||||||
tmPtr = &teamID
|
tmPtr = &teamID
|
||||||
@ -8065,7 +8065,7 @@ func (s *integrationMDMTestSuite) TestMDMConfigProfileCRUD() {
|
|||||||
return resp.ProfileID
|
return resp.ProfileID
|
||||||
}
|
}
|
||||||
createWindowsProfile := func(name string, teamID uint) string {
|
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
|
var wantJSON string
|
||||||
if teamID == 0 {
|
if teamID == 0 {
|
||||||
@ -8086,29 +8086,31 @@ func (s *integrationMDMTestSuite) TestMDMConfigProfileCRUD() {
|
|||||||
teamWinProfID := createWindowsProfile("win-team-profile", testTeam.ID)
|
teamWinProfID := createWindowsProfile("win-team-profile", testTeam.ID)
|
||||||
|
|
||||||
// Windows profile name conflicts with Apple's for no team
|
// 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
|
// 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
|
// 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.")
|
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
|
// but no conflict for team 1
|
||||||
assertAppleProfile("win-global-profile.mobileconfig", "win-global-profile", "test-global-ident-2", testTeam.ID, http.StatusOK, "")
|
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
|
// 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
|
// 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
|
// 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.")
|
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
|
// but no conflict for no-team
|
||||||
assertAppleProfile("win-team-profile.mobileconfig", "win-team-profile", "test-team-ident-2", 0, http.StatusOK, "")
|
assertAppleProfile("win-team-profile.mobileconfig", "win-team-profile", "test-team-ident-2", 0, http.StatusOK, "")
|
||||||
|
|
||||||
// not an xml nor mobileconfig file
|
// 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.")
|
assertAppleProfile("foo.txt", "foo", "foo-ident", 0, http.StatusBadRequest, "Couldn't upload. The file should be a .mobileconfig or .xml file.")
|
||||||
|
|
||||||
// Windows-reserved LocURI
|
// 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("bitlocker.xml", 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("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
|
// Windows invalid content
|
||||||
body, headers := generateNewProfileMultipartRequest(t, nil, "win.xml", []byte("\x00\x01\x02"), s.token)
|
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
|
// try to delete the profile
|
||||||
s.DoJSON("DELETE", fmt.Sprintf("/api/latest/fleet/mdm/profiles/%d", profile.ProfileID), nil, http.StatusBadRequest, &deleteResp)
|
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() {
|
func (s *integrationMDMTestSuite) TestListMDMConfigProfiles() {
|
||||||
|
@ -24,6 +24,7 @@ import (
|
|||||||
"github.com/fleetdm/fleet/v4/server/fleet"
|
"github.com/fleetdm/fleet/v4/server/fleet"
|
||||||
"github.com/fleetdm/fleet/v4/server/mdm"
|
"github.com/fleetdm/fleet/v4/server/mdm"
|
||||||
apple_mdm "github.com/fleetdm/fleet/v4/server/mdm/apple"
|
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/fleetdm/fleet/v4/server/ptr"
|
||||||
"github.com/go-kit/kit/log/level"
|
"github.com/go-kit/kit/log/level"
|
||||||
"github.com/go-sql-driver/mysql"
|
"github.com/go-sql-driver/mysql"
|
||||||
@ -1116,6 +1117,12 @@ func (svc *Service) DeleteMDMWindowsConfigProfile(ctx context.Context, profileUU
|
|||||||
return ctxerr.Wrap(ctx, err)
|
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 {
|
if err := svc.ds.DeleteMDMWindowsConfigProfile(ctx, profileUUID); err != nil {
|
||||||
return ctxerr.Wrap(ctx, err)
|
return ctxerr.Wrap(ctx, err)
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user