mirror of
https://github.com/fleetdm/fleet
synced 2026-05-23 17:08:53 +00:00
macOS OOBE Prefill and Lock Local Account: feature branch (#17854)
Feature branch for #9147 .
This commit is contained in:
commit
79f9caf49d
81 changed files with 3397 additions and 1339 deletions
4
.github/workflows/test-go.yaml
vendored
4
.github/workflows/test-go.yaml
vendored
|
|
@ -49,7 +49,7 @@ jobs:
|
|||
|
||||
env:
|
||||
RACE_ENABLED: false
|
||||
GO_TEST_TIMEOUT: 15m
|
||||
GO_TEST_TIMEOUT: 20m
|
||||
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
|
|
@ -74,7 +74,7 @@ jobs:
|
|||
run: |
|
||||
sudo cp tools/smtp4dev/fleet.crt /usr/local/share/ca-certificates/
|
||||
sudo update-ca-certificates
|
||||
|
||||
|
||||
# It seems faster not to cache Go dependencies
|
||||
- name: Install Go Dependencies
|
||||
run: make deps-go
|
||||
|
|
|
|||
2
changes/17401-add-enable-release-device-manually
Normal file
2
changes/17401-add-enable-release-device-manually
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
* Added the `enable_release_device_manually` configuration setting for a team and no team. **Note** that the macOS automatic enrollment profile cannot set the `await_device_configured` option anymore, this setting is controlled by Fleet via the new `enable_release_device_manually` option.
|
||||
* Automatically release a macOS DEP-enrolled device after enrollment commands and profiles have been delivered, unless `enable_release_device_manually` is set to `true`.
|
||||
|
|
@ -14,6 +14,7 @@ import (
|
|||
"path/filepath"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
|
@ -207,12 +208,19 @@ spec:
|
|||
MinimumVersion: optjson.SetString("12.3.1"),
|
||||
Deadline: optjson.SetString("2011-03-01"),
|
||||
},
|
||||
MacOSSetup: fleet.MacOSSetup{
|
||||
EnableReleaseDeviceManually: optjson.SetBool(false),
|
||||
},
|
||||
}
|
||||
require.Equal(t, "[+] applied 2 teams\n", runAppForTest(t, []string{"apply", "-f", filename}))
|
||||
assert.JSONEq(t, string(agentOpts), string(*teamsByName["team2"].Config.AgentOptions))
|
||||
assert.JSONEq(t, string(newAgentOpts), string(*teamsByName["team1"].Config.AgentOptions))
|
||||
assert.Equal(t, []*fleet.EnrollSecret{{Secret: "AAA"}}, enrolledSecretsCalled[uint(42)])
|
||||
assert.Equal(t, fleet.TeamMDM{}, teamsByName["team2"].Config.MDM)
|
||||
assert.Equal(t, fleet.TeamMDM{
|
||||
MacOSSetup: fleet.MacOSSetup{
|
||||
EnableReleaseDeviceManually: optjson.SetBool(false),
|
||||
},
|
||||
}, teamsByName["team2"].Config.MDM)
|
||||
assert.Equal(t, newMDMSettings, teamsByName["team1"].Config.MDM)
|
||||
assert.True(t, ds.ApplyEnrollSecretsFuncInvoked)
|
||||
ds.ApplyEnrollSecretsFuncInvoked = false
|
||||
|
|
@ -240,6 +248,9 @@ spec:
|
|||
DeadlineDays: optjson.SetInt(5),
|
||||
GracePeriodDays: optjson.SetInt(1),
|
||||
},
|
||||
MacOSSetup: fleet.MacOSSetup{
|
||||
EnableReleaseDeviceManually: optjson.SetBool(false),
|
||||
},
|
||||
}
|
||||
assert.Equal(t, newMDMSettings, teamsByName["team1"].Config.MDM)
|
||||
|
||||
|
|
@ -268,6 +279,9 @@ spec:
|
|||
MacOSSettings: fleet.MacOSSettings{
|
||||
CustomSettings: []fleet.MDMProfileSpec{{Path: mobileCfgPath}},
|
||||
},
|
||||
MacOSSetup: fleet.MacOSSetup{
|
||||
EnableReleaseDeviceManually: optjson.SetBool(false),
|
||||
},
|
||||
}
|
||||
|
||||
assert.Contains(t, runAppForTest(t, []string{"apply", "-f", filename}), "[+] applied 1 teams\n")
|
||||
|
|
@ -309,6 +323,9 @@ spec:
|
|||
MacOSSettings: fleet.MacOSSettings{ // macos settings not provided, so not cleared
|
||||
CustomSettings: []fleet.MDMProfileSpec{{Path: mobileCfgPath}},
|
||||
},
|
||||
MacOSSetup: fleet.MacOSSetup{
|
||||
EnableReleaseDeviceManually: optjson.SetBool(false),
|
||||
},
|
||||
}
|
||||
newAgentOpts = json.RawMessage(`{"config":{"views":{"foo":"qux"}}}`)
|
||||
require.Equal(t, "[+] applied 1 teams\n", runAppForTest(t, []string{"apply", "-f", filename}))
|
||||
|
|
@ -382,6 +399,9 @@ spec:
|
|||
MacOSSettings: fleet.MacOSSettings{
|
||||
CustomSettings: []fleet.MDMProfileSpec{},
|
||||
},
|
||||
MacOSSetup: fleet.MacOSSetup{
|
||||
EnableReleaseDeviceManually: optjson.SetBool(false),
|
||||
},
|
||||
}
|
||||
|
||||
assert.Contains(t, runAppForTest(t, []string{"apply", "-f", filename}), "[+] applied 1 teams\n")
|
||||
|
|
@ -570,6 +590,9 @@ spec:
|
|||
MinimumVersion: optjson.SetString("12.1.1"),
|
||||
Deadline: optjson.SetString("2011-02-01"),
|
||||
},
|
||||
MacOSSetup: fleet.MacOSSetup{
|
||||
EnableReleaseDeviceManually: optjson.SetBool(false),
|
||||
},
|
||||
WindowsUpdates: fleet.WindowsUpdates{
|
||||
DeadlineDays: optjson.SetInt(5),
|
||||
GracePeriodDays: optjson.SetInt(1),
|
||||
|
|
@ -622,6 +645,9 @@ spec:
|
|||
MinimumVersion: optjson.SetString("12.1.1"),
|
||||
Deadline: optjson.SetString("2011-02-01"),
|
||||
},
|
||||
MacOSSetup: fleet.MacOSSetup{
|
||||
EnableReleaseDeviceManually: optjson.SetBool(false),
|
||||
},
|
||||
WindowsUpdates: fleet.WindowsUpdates{
|
||||
DeadlineDays: optjson.Int{Set: true},
|
||||
GracePeriodDays: optjson.Int{Set: true},
|
||||
|
|
@ -1182,7 +1208,8 @@ spec:
|
|||
assert.Equal(t, fleet.MDM{
|
||||
EnabledAndConfigured: true,
|
||||
MacOSSetup: fleet.MacOSSetup{
|
||||
MacOSSetupAssistant: optjson.SetString(emptySetupAsst),
|
||||
MacOSSetupAssistant: optjson.SetString(emptySetupAsst),
|
||||
EnableReleaseDeviceManually: optjson.SetBool(false),
|
||||
},
|
||||
MacOSUpdates: fleet.MacOSUpdates{
|
||||
MinimumVersion: optjson.SetString("10.10.10"),
|
||||
|
|
@ -1223,8 +1250,9 @@ spec:
|
|||
assert.Equal(t, fleet.MDM{
|
||||
EnabledAndConfigured: true,
|
||||
MacOSSetup: fleet.MacOSSetup{
|
||||
MacOSSetupAssistant: optjson.SetString(emptySetupAsst),
|
||||
BootstrapPackage: optjson.SetString(bootstrapURL),
|
||||
MacOSSetupAssistant: optjson.SetString(emptySetupAsst),
|
||||
BootstrapPackage: optjson.SetString(bootstrapURL),
|
||||
EnableReleaseDeviceManually: optjson.SetBool(false),
|
||||
},
|
||||
MacOSUpdates: fleet.MacOSUpdates{
|
||||
MinimumVersion: optjson.SetString("10.10.10"),
|
||||
|
|
@ -1281,6 +1309,9 @@ spec:
|
|||
MinimumVersion: optjson.SetString("10.10.10"),
|
||||
Deadline: optjson.SetString("1992-03-01"),
|
||||
},
|
||||
MacOSSetup: fleet.MacOSSetup{
|
||||
EnableReleaseDeviceManually: optjson.SetBool(false),
|
||||
},
|
||||
WindowsUpdates: fleet.WindowsUpdates{
|
||||
DeadlineDays: optjson.SetInt(0),
|
||||
GracePeriodDays: optjson.SetInt(1),
|
||||
|
|
@ -1325,7 +1356,8 @@ spec:
|
|||
GracePeriodDays: optjson.SetInt(1),
|
||||
},
|
||||
MacOSSetup: fleet.MacOSSetup{
|
||||
MacOSSetupAssistant: optjson.SetString(emptySetupAsst),
|
||||
MacOSSetupAssistant: optjson.SetString(emptySetupAsst),
|
||||
EnableReleaseDeviceManually: optjson.SetBool(false),
|
||||
},
|
||||
}, savedTeam.Config.MDM)
|
||||
|
||||
|
|
@ -1361,8 +1393,9 @@ spec:
|
|||
GracePeriodDays: optjson.SetInt(1),
|
||||
},
|
||||
MacOSSetup: fleet.MacOSSetup{
|
||||
MacOSSetupAssistant: optjson.SetString(emptySetupAsst),
|
||||
BootstrapPackage: optjson.SetString(bootstrapURL),
|
||||
MacOSSetupAssistant: optjson.SetString(emptySetupAsst),
|
||||
BootstrapPackage: optjson.SetString(bootstrapURL),
|
||||
EnableReleaseDeviceManually: optjson.SetBool(false),
|
||||
},
|
||||
}, savedTeam.Config.MDM)
|
||||
|
||||
|
|
@ -1774,6 +1807,9 @@ func TestApplyMacosSetup(t *testing.T) {
|
|||
invalidURLMacosSetup := writeTmpJSON(t, map[string]any{
|
||||
"url": "https://example.com",
|
||||
})
|
||||
invalidAwaitMacosSetup := writeTmpJSON(t, map[string]any{
|
||||
"await_device_configured": true,
|
||||
})
|
||||
|
||||
const (
|
||||
appConfigSpec = `
|
||||
|
|
@ -1784,6 +1820,9 @@ spec:
|
|||
macos_setup:
|
||||
bootstrap_package: %s
|
||||
macos_setup_assistant: %s
|
||||
`
|
||||
appConfigEnableReleaseSpec = appConfigSpec + `
|
||||
enable_release_device_manually: %s
|
||||
`
|
||||
appConfigNoKeySpec = `
|
||||
apiVersion: v1
|
||||
|
|
@ -1810,6 +1849,9 @@ spec:
|
|||
macos_setup:
|
||||
bootstrap_package: %s
|
||||
macos_setup_assistant: %s
|
||||
`
|
||||
team1EnableReleaseSpec = team1Spec + `
|
||||
enable_release_device_manually: %s
|
||||
`
|
||||
team1NoKeySpec = `
|
||||
apiVersion: v1
|
||||
|
|
@ -1971,11 +2013,17 @@ spec:
|
|||
b, err = os.ReadFile(filepath.Join("testdata", "macosSetupExpectedAppConfigSet.yml"))
|
||||
require.NoError(t, err)
|
||||
expectedAppCfgSet := fmt.Sprintf(string(b), "", emptyMacosSetup)
|
||||
expectedAppCfgSetReleaseEnabled := strings.ReplaceAll(expectedAppCfgSet, `enable_release_device_manually: false`, `enable_release_device_manually: true`)
|
||||
|
||||
b, err = os.ReadFile(filepath.Join("testdata", "macosSetupExpectedTeam1Empty.yml"))
|
||||
require.NoError(t, err)
|
||||
expectedEmptyTm1 := string(b)
|
||||
|
||||
b, err = os.ReadFile(filepath.Join("testdata", "macosSetupExpectedTeam1Set.yml"))
|
||||
require.NoError(t, err)
|
||||
expectedTm1Set := fmt.Sprintf(string(b), "", "")
|
||||
expectedTm1SetReleaseEnabled := strings.ReplaceAll(expectedTm1Set, `enable_release_device_manually: false`, `enable_release_device_manually: true`)
|
||||
|
||||
b, err = os.ReadFile(filepath.Join("testdata", "macosSetupExpectedTeam1And2Empty.yml"))
|
||||
require.NoError(t, err)
|
||||
expectedEmptyTm1And2 := string(b)
|
||||
|
|
@ -2004,8 +2052,8 @@ spec:
|
|||
assert.YAMLEq(t, expectedEmptyAppCfg, runAppForTest(t, []string{"get", "config", "--yaml"}))
|
||||
assert.YAMLEq(t, expectedEmptyTm1, runAppForTest(t, []string{"get", "teams", "--yaml"}))
|
||||
|
||||
// apply appconfig for real
|
||||
name = writeTmpYml(t, fmt.Sprintf(appConfigSpec, "", emptyMacosSetup))
|
||||
// apply appconfig for real, and enable release device
|
||||
name = writeTmpYml(t, fmt.Sprintf(appConfigEnableReleaseSpec, "", emptyMacosSetup, "true"))
|
||||
assert.Equal(t, "[+] applied fleet config\n", runAppForTest(t, []string{"apply", "-f", name}))
|
||||
assert.True(t, ds.SetOrUpdateMDMAppleSetupAssistantFuncInvoked)
|
||||
assert.True(t, ds.SaveAppConfigFuncInvoked)
|
||||
|
|
@ -2018,7 +2066,7 @@ spec:
|
|||
assert.True(t, ds.SaveTeamFuncInvoked)
|
||||
|
||||
// get, setup assistant is now set
|
||||
assert.YAMLEq(t, expectedAppCfgSet, runAppForTest(t, []string{"get", "config", "--yaml"}))
|
||||
assert.YAMLEq(t, expectedAppCfgSetReleaseEnabled, runAppForTest(t, []string{"get", "config", "--yaml"}))
|
||||
assert.YAMLEq(t, expectedTm1And2Set, runAppForTest(t, []string{"get", "teams", "--yaml"}))
|
||||
|
||||
// clear with dry-run, appconfig
|
||||
|
|
@ -2054,11 +2102,11 @@ spec:
|
|||
assert.True(t, ds.SaveTeamFuncInvoked)
|
||||
|
||||
// get, results unchanged
|
||||
assert.YAMLEq(t, expectedAppCfgSet, runAppForTest(t, []string{"get", "config", "--yaml"}))
|
||||
assert.YAMLEq(t, expectedAppCfgSetReleaseEnabled, runAppForTest(t, []string{"get", "config", "--yaml"}))
|
||||
assert.YAMLEq(t, expectedTm1And2Set, runAppForTest(t, []string{"get", "teams", "--yaml"}))
|
||||
|
||||
// clear appconfig for real
|
||||
name = writeTmpYml(t, fmt.Sprintf(appConfigSpec, "", ""))
|
||||
name = writeTmpYml(t, fmt.Sprintf(appConfigEnableReleaseSpec, "", "", "false"))
|
||||
ds.SaveAppConfigFuncInvoked = false
|
||||
assert.Equal(t, "[+] applied fleet config\n", runAppForTest(t, []string{"apply", "-f", name}))
|
||||
assert.False(t, ds.SetOrUpdateMDMAppleSetupAssistantFuncInvoked)
|
||||
|
|
@ -2077,16 +2125,40 @@ spec:
|
|||
assert.YAMLEq(t, expectedEmptyAppCfg, runAppForTest(t, []string{"get", "config", "--yaml"}))
|
||||
assert.YAMLEq(t, expectedEmptyTm1And2, runAppForTest(t, []string{"get", "teams", "--yaml"}))
|
||||
|
||||
// apply appconfig with invalid key
|
||||
// apply team 1 without the setup assistant key but enable device release
|
||||
name = writeTmpYml(t, fmt.Sprintf(team1EnableReleaseSpec, "", "", "true"))
|
||||
ds.SetOrUpdateMDMAppleSetupAssistantFuncInvoked = false
|
||||
ds.DeleteMDMAppleSetupAssistantFuncInvoked = false
|
||||
ds.SaveTeamFuncInvoked = false
|
||||
assert.Equal(t, "[+] applied 1 teams\n", runAppForTest(t, []string{"apply", "-f", name}))
|
||||
assert.False(t, ds.SetOrUpdateMDMAppleSetupAssistantFuncInvoked)
|
||||
assert.False(t, ds.DeleteMDMAppleSetupAssistantFuncInvoked)
|
||||
assert.True(t, ds.SaveTeamFuncInvoked)
|
||||
|
||||
assert.YAMLEq(t, expectedTm1SetReleaseEnabled, runAppForTest(t, []string{"get", "teams", "--yaml"}))
|
||||
|
||||
// apply appconfig with invalid URL key
|
||||
name = writeTmpYml(t, fmt.Sprintf(appConfigSpec, "", invalidURLMacosSetup))
|
||||
_, err = runAppNoChecks([]string{"apply", "-f", name})
|
||||
require.ErrorContains(t, err, "The automatic enrollment profile can’t include url.")
|
||||
require.ErrorContains(t, err, "The automatic enrollment profile can't include url.")
|
||||
assert.False(t, ds.SetOrUpdateMDMAppleSetupAssistantFuncInvoked)
|
||||
|
||||
// apply teams with invalid key
|
||||
// apply teams with invalid URL key
|
||||
name = writeTmpYml(t, fmt.Sprintf(team1And2Spec, "", invalidURLMacosSetup, "", invalidURLMacosSetup))
|
||||
_, err = runAppNoChecks([]string{"apply", "-f", name})
|
||||
require.ErrorContains(t, err, "The automatic enrollment profile can’t include url.")
|
||||
require.ErrorContains(t, err, "The automatic enrollment profile can't include url.")
|
||||
assert.False(t, ds.SetOrUpdateMDMAppleSetupAssistantFuncInvoked)
|
||||
|
||||
// apply appconfig with invalid await_device_configured key
|
||||
name = writeTmpYml(t, fmt.Sprintf(appConfigSpec, "", invalidAwaitMacosSetup))
|
||||
_, err = runAppNoChecks([]string{"apply", "-f", name})
|
||||
require.ErrorContains(t, err, `The profile can't include "await_device_configured" option.`)
|
||||
assert.False(t, ds.SetOrUpdateMDMAppleSetupAssistantFuncInvoked)
|
||||
|
||||
// apply teams with invalid await_device_configured key
|
||||
name = writeTmpYml(t, fmt.Sprintf(team1And2Spec, "", invalidAwaitMacosSetup, "", invalidAwaitMacosSetup))
|
||||
_, err = runAppNoChecks([]string{"apply", "-f", name})
|
||||
require.ErrorContains(t, err, `The profile can't include "await_device_configured" option.`)
|
||||
assert.False(t, ds.SetOrUpdateMDMAppleSetupAssistantFuncInvoked)
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -108,7 +108,8 @@
|
|||
"macos_setup": {
|
||||
"bootstrap_package": null,
|
||||
"enable_end_user_authentication": false,
|
||||
"macos_setup_assistant": null
|
||||
"macos_setup_assistant": null,
|
||||
"enable_release_device_manually": false
|
||||
},
|
||||
"windows_settings": {
|
||||
"custom_settings": null
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ spec:
|
|||
macos_setup:
|
||||
bootstrap_package:
|
||||
enable_end_user_authentication: false
|
||||
enable_release_device_manually: false
|
||||
macos_setup_assistant:
|
||||
windows_settings:
|
||||
custom_settings: null
|
||||
|
|
|
|||
|
|
@ -65,7 +65,8 @@
|
|||
"macos_setup": {
|
||||
"bootstrap_package": null,
|
||||
"enable_end_user_authentication": false,
|
||||
"macos_setup_assistant": null
|
||||
"macos_setup_assistant": null,
|
||||
"enable_release_device_manually": false
|
||||
},
|
||||
"windows_settings": {
|
||||
"custom_settings": null
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ spec:
|
|||
macos_setup:
|
||||
bootstrap_package:
|
||||
enable_end_user_authentication: false
|
||||
enable_release_device_manually: false
|
||||
macos_setup_assistant:
|
||||
windows_settings:
|
||||
custom_settings:
|
||||
|
|
|
|||
|
|
@ -45,7 +45,8 @@
|
|||
"macos_setup": {
|
||||
"bootstrap_package": null,
|
||||
"enable_end_user_authentication": false,
|
||||
"macos_setup_assistant": null
|
||||
"macos_setup_assistant": null,
|
||||
"enable_release_device_manually": false
|
||||
},
|
||||
"windows_settings": {
|
||||
"custom_settings": null
|
||||
|
|
@ -119,7 +120,8 @@
|
|||
"macos_setup": {
|
||||
"bootstrap_package": null,
|
||||
"enable_end_user_authentication": false,
|
||||
"macos_setup_assistant": null
|
||||
"macos_setup_assistant": null,
|
||||
"enable_release_device_manually": false
|
||||
},
|
||||
"windows_settings": {
|
||||
"custom_settings": null
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ spec:
|
|||
macos_setup:
|
||||
bootstrap_package:
|
||||
enable_end_user_authentication: false
|
||||
enable_release_device_manually: false
|
||||
macos_setup_assistant:
|
||||
scripts: null
|
||||
webhook_settings:
|
||||
|
|
@ -68,6 +69,7 @@ spec:
|
|||
macos_setup:
|
||||
bootstrap_package:
|
||||
enable_end_user_authentication: false
|
||||
enable_release_device_manually: false
|
||||
macos_setup_assistant:
|
||||
scripts: null
|
||||
webhook_settings:
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ spec:
|
|||
bootstrap_package: null
|
||||
enable_end_user_authentication: false
|
||||
macos_setup_assistant: null
|
||||
enable_release_device_manually: false
|
||||
macos_updates:
|
||||
deadline: null
|
||||
minimum_version: null
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ spec:
|
|||
bootstrap_package: %s
|
||||
enable_end_user_authentication: false
|
||||
macos_setup_assistant: %s
|
||||
enable_release_device_manually: false
|
||||
macos_updates:
|
||||
deadline: null
|
||||
minimum_version: null
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ spec:
|
|||
bootstrap_package: null
|
||||
enable_end_user_authentication: false
|
||||
macos_setup_assistant: null
|
||||
enable_release_device_manually: false
|
||||
macos_updates:
|
||||
deadline: null
|
||||
minimum_version: null
|
||||
|
|
@ -53,6 +54,7 @@ spec:
|
|||
macos_setup:
|
||||
bootstrap_package: null
|
||||
macos_setup_assistant: null
|
||||
enable_release_device_manually: false
|
||||
macos_updates:
|
||||
deadline: null
|
||||
minimum_version: null
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ spec:
|
|||
bootstrap_package: %s
|
||||
enable_end_user_authentication: false
|
||||
macos_setup_assistant: %s
|
||||
enable_release_device_manually: false
|
||||
macos_updates:
|
||||
deadline: null
|
||||
minimum_version: null
|
||||
|
|
@ -53,6 +54,7 @@ spec:
|
|||
macos_setup:
|
||||
bootstrap_package: %s
|
||||
macos_setup_assistant: %s
|
||||
enable_release_device_manually: false
|
||||
macos_updates:
|
||||
deadline: null
|
||||
minimum_version: null
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ spec:
|
|||
bootstrap_package: null
|
||||
enable_end_user_authentication: false
|
||||
macos_setup_assistant: null
|
||||
enable_release_device_manually: false
|
||||
macos_updates:
|
||||
deadline: null
|
||||
minimum_version: null
|
||||
|
|
|
|||
33
cmd/fleetctl/testdata/macosSetupExpectedTeam1Set.yml
vendored
Normal file
33
cmd/fleetctl/testdata/macosSetupExpectedTeam1Set.yml
vendored
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
apiVersion: v1
|
||||
kind: team
|
||||
spec:
|
||||
team:
|
||||
features:
|
||||
enable_host_users: true
|
||||
enable_software_inventory: true
|
||||
host_expiry_settings:
|
||||
host_expiry_enabled: false
|
||||
host_expiry_window: 0
|
||||
integrations:
|
||||
google_calendar: null
|
||||
mdm:
|
||||
enable_disk_encryption: false
|
||||
macos_settings:
|
||||
custom_settings: null
|
||||
windows_settings:
|
||||
custom_settings: null
|
||||
macos_setup:
|
||||
bootstrap_package: %s
|
||||
enable_end_user_authentication: false
|
||||
macos_setup_assistant: %s
|
||||
enable_release_device_manually: false
|
||||
macos_updates:
|
||||
deadline: null
|
||||
minimum_version: null
|
||||
windows_updates:
|
||||
deadline_days: null
|
||||
grace_period_days: null
|
||||
scripts: null
|
||||
webhook_settings:
|
||||
host_status_webhook: null
|
||||
name: tm1
|
||||
|
|
@ -237,6 +237,13 @@ func (svc *Service) updateAppConfigMDMAppleSetup(ctx context.Context, payload fl
|
|||
}
|
||||
}
|
||||
|
||||
if payload.EnableReleaseDeviceManually != nil {
|
||||
if ac.MDM.MacOSSetup.EnableReleaseDeviceManually.Value != *payload.EnableReleaseDeviceManually {
|
||||
ac.MDM.MacOSSetup.EnableReleaseDeviceManually = optjson.SetBool(*payload.EnableReleaseDeviceManually)
|
||||
didUpdate = true
|
||||
}
|
||||
}
|
||||
|
||||
if didUpdate {
|
||||
if err := svc.ds.SaveAppConfig(ctx, ac); err != nil {
|
||||
return err
|
||||
|
|
@ -550,7 +557,10 @@ func (svc *Service) SetOrUpdateMDMAppleSetupAssistant(ctx context.Context, asst
|
|||
}
|
||||
|
||||
if _, ok := m["url"]; ok {
|
||||
return nil, ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("profile", `Couldn’t edit macos_setup_assistant. The automatic enrollment profile can’t include url.`))
|
||||
return nil, ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("profile", `Couldn't edit macos_setup_assistant. The automatic enrollment profile can't include url.`))
|
||||
}
|
||||
if _, ok := m["await_device_configured"]; ok {
|
||||
return nil, ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("profile", `Couldn't edit macos_setup_assistant. The profile can't include "await_device_configured" option.`))
|
||||
}
|
||||
|
||||
// must read the existing setup assistant first to detect if it did change
|
||||
|
|
@ -925,22 +935,21 @@ func (svc *Service) getOrCreatePreassignTeam(ctx context.Context, groups []strin
|
|||
return nil, err
|
||||
}
|
||||
|
||||
payload.MDM = &fleet.TeamPayloadMDM{
|
||||
EnableDiskEncryption: optjson.SetBool(true),
|
||||
MacOSSetup: &fleet.MacOSSetup{
|
||||
MacOSSetupAssistant: ac.MDM.MacOSSetup.MacOSSetupAssistant,
|
||||
// NOTE: BootstrapPackage is currently ignored by svc.ModifyTeam and gets set
|
||||
// instead by CopyDefaultMDMAppleBootstrapPackage below
|
||||
// BootstrapPackage: ac.MDM.MacOSSetup.BootstrapPackage,
|
||||
EnableEndUserAuthentication: ac.MDM.MacOSSetup.EnableEndUserAuthentication,
|
||||
spec := &fleet.TeamSpec{
|
||||
Name: teamName,
|
||||
MDM: fleet.TeamSpecMDM{
|
||||
EnableDiskEncryption: optjson.SetBool(true),
|
||||
MacOSSetup: fleet.MacOSSetup{
|
||||
MacOSSetupAssistant: ac.MDM.MacOSSetup.MacOSSetupAssistant,
|
||||
// NOTE: BootstrapPackage gets set by
|
||||
// CopyDefaultMDMAppleBootstrapPackage below
|
||||
// BootstrapPackage: ac.MDM.MacOSSetup.BootstrapPackage,
|
||||
EnableEndUserAuthentication: ac.MDM.MacOSSetup.EnableEndUserAuthentication,
|
||||
EnableReleaseDeviceManually: ac.MDM.MacOSSetup.EnableReleaseDeviceManually,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// TODO: seems like we don't support enabling disk encryption
|
||||
// on team creation?
|
||||
// see https://github.com/fleetdm/fleet/issues/12220
|
||||
team, err = svc.ModifyTeam(ctx, team.ID, payload)
|
||||
if err != nil {
|
||||
if _, err := svc.ApplyTeamSpecs(ctx, []*fleet.TeamSpec{spec}, fleet.ApplySpecOptions{}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := svc.ds.CopyDefaultMDMAppleBootstrapPackage(ctx, ac, team.ID); err != nil {
|
||||
|
|
|
|||
27
ee/server/service/mdm_export_for_test.go
Normal file
27
ee/server/service/mdm_export_for_test.go
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/fleetdm/fleet/v4/server/fleet"
|
||||
)
|
||||
|
||||
// This file exports internal functions and methods only for testing purposes.
|
||||
// Those are used by mdm_external_test.go which runs the tests as an external
|
||||
// package to avoid import cycles, and as such needs to be able to call these
|
||||
// unexported symbols.
|
||||
|
||||
func (svc *Service) GetOrCreatePreassignTeam(ctx context.Context, groups []string) (*fleet.Team, error) {
|
||||
return svc.getOrCreatePreassignTeam(ctx, groups)
|
||||
}
|
||||
|
||||
func TeamNameFromPreassignGroups(groups []string) string {
|
||||
return teamNameFromPreassignGroups(groups)
|
||||
}
|
||||
|
||||
type NotFoundError = notFoundError
|
||||
|
||||
var (
|
||||
TestCert = testCert
|
||||
TestKey = testKey
|
||||
)
|
||||
533
ee/server/service/mdm_external_test.go
Normal file
533
ee/server/service/mdm_external_test.go
Normal file
|
|
@ -0,0 +1,533 @@
|
|||
package service_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/WatchBeam/clock"
|
||||
eeservice "github.com/fleetdm/fleet/v4/ee/server/service"
|
||||
"github.com/fleetdm/fleet/v4/pkg/optjson"
|
||||
"github.com/fleetdm/fleet/v4/server/config"
|
||||
authz_ctx "github.com/fleetdm/fleet/v4/server/contexts/authz"
|
||||
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
|
||||
"github.com/fleetdm/fleet/v4/server/contexts/license"
|
||||
"github.com/fleetdm/fleet/v4/server/contexts/viewer"
|
||||
"github.com/fleetdm/fleet/v4/server/fleet"
|
||||
"github.com/fleetdm/fleet/v4/server/mdm/apple/mobileconfig"
|
||||
nanodep_storage "github.com/fleetdm/fleet/v4/server/mdm/nanodep/storage"
|
||||
"github.com/fleetdm/fleet/v4/server/mock"
|
||||
nanodep_mock "github.com/fleetdm/fleet/v4/server/mock/nanodep"
|
||||
"github.com/fleetdm/fleet/v4/server/ptr"
|
||||
"github.com/fleetdm/fleet/v4/server/service"
|
||||
"github.com/fleetdm/fleet/v4/server/test"
|
||||
"github.com/fleetdm/fleet/v4/server/worker"
|
||||
kitlog "github.com/go-kit/kit/log"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func setupMockDatastorePremiumService() (*mock.Store, *eeservice.Service, context.Context) {
|
||||
ds := new(mock.Store)
|
||||
lic := &fleet.LicenseInfo{Tier: fleet.TierPremium}
|
||||
ctx := license.NewContext(context.Background(), lic)
|
||||
|
||||
logger := kitlog.NewNopLogger()
|
||||
fleetConfig := config.FleetConfig{
|
||||
MDM: config.MDMConfig{
|
||||
AppleSCEPCertBytes: eeservice.TestCert,
|
||||
AppleSCEPKeyBytes: eeservice.TestKey,
|
||||
},
|
||||
}
|
||||
var depStorage nanodep_storage.AllDEPStorage = &nanodep_mock.Storage{}
|
||||
|
||||
freeSvc, err := service.NewService(
|
||||
ctx,
|
||||
ds,
|
||||
nil,
|
||||
nil,
|
||||
logger,
|
||||
nil,
|
||||
fleetConfig,
|
||||
nil,
|
||||
clock.C,
|
||||
nil,
|
||||
nil,
|
||||
ds,
|
||||
nil,
|
||||
nil,
|
||||
&fleet.NoOpGeoIP{},
|
||||
nil,
|
||||
depStorage,
|
||||
nil,
|
||||
nil,
|
||||
"",
|
||||
nil,
|
||||
nil,
|
||||
)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
svc, err := eeservice.NewService(
|
||||
freeSvc,
|
||||
ds,
|
||||
logger,
|
||||
fleetConfig,
|
||||
nil,
|
||||
clock.C,
|
||||
depStorage,
|
||||
nil,
|
||||
"",
|
||||
nil,
|
||||
nil,
|
||||
)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return ds, svc, ctx
|
||||
}
|
||||
|
||||
func TestGetOrCreatePreassignTeam(t *testing.T) {
|
||||
ds, svc, ctx := setupMockDatastorePremiumService()
|
||||
|
||||
ssoSettings := fleet.SSOProviderSettings{
|
||||
EntityID: "foo",
|
||||
MetadataURL: "https://example.com/metadata.xml",
|
||||
IssuerURI: "https://example.com",
|
||||
}
|
||||
appConfig := &fleet.AppConfig{MDM: fleet.MDM{
|
||||
EnabledAndConfigured: true,
|
||||
EndUserAuthentication: fleet.MDMEndUserAuthentication{SSOProviderSettings: ssoSettings},
|
||||
MacOSSetup: fleet.MacOSSetup{
|
||||
BootstrapPackage: optjson.SetString("https://example.com/bootstrap.pkg"),
|
||||
EnableEndUserAuthentication: true,
|
||||
EnableReleaseDeviceManually: optjson.SetBool(true),
|
||||
},
|
||||
}}
|
||||
preassignGroups := []string{"one", "three"}
|
||||
|
||||
// initialize team store with team one and two already created, one matches
|
||||
// preassign group [0], two does not match any preassign group
|
||||
team1 := &fleet.Team{
|
||||
ID: 1,
|
||||
Name: preassignGroups[0],
|
||||
}
|
||||
team2 := &fleet.Team{
|
||||
ID: 2,
|
||||
Name: "two",
|
||||
Config: fleet.TeamConfig{
|
||||
MDM: fleet.TeamMDM{
|
||||
MacOSSetup: fleet.MacOSSetup{MacOSSetupAssistant: optjson.SetString("foo/bar")},
|
||||
},
|
||||
},
|
||||
}
|
||||
teamStore := map[uint]*fleet.Team{1: team1, 2: team2}
|
||||
|
||||
resetInvoked := func() {
|
||||
ds.TeamByNameFuncInvoked = false
|
||||
ds.NewTeamFuncInvoked = false
|
||||
ds.SaveTeamFuncInvoked = false
|
||||
ds.NewMDMAppleConfigProfileFuncInvoked = false
|
||||
ds.CopyDefaultMDMAppleBootstrapPackageFuncInvoked = false
|
||||
ds.AppConfigFuncInvoked = false
|
||||
ds.NewJobFuncInvoked = false
|
||||
ds.GetMDMAppleSetupAssistantFuncInvoked = false
|
||||
ds.SetOrUpdateMDMAppleSetupAssistantFuncInvoked = false
|
||||
}
|
||||
setupDS := func(t *testing.T) {
|
||||
resetInvoked()
|
||||
|
||||
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
|
||||
return appConfig, nil
|
||||
}
|
||||
ds.NewActivityFunc = func(ctx context.Context, u *fleet.User, a fleet.ActivityDetails) error {
|
||||
return nil
|
||||
}
|
||||
ds.TeamByNameFunc = func(ctx context.Context, name string) (*fleet.Team, error) {
|
||||
for _, team := range teamStore {
|
||||
if team.Name == name {
|
||||
return team, nil
|
||||
}
|
||||
}
|
||||
return nil, ctxerr.Wrap(ctx, &eeservice.NotFoundError{})
|
||||
}
|
||||
ds.TeamFunc = func(ctx context.Context, id uint) (*fleet.Team, error) {
|
||||
tm, ok := teamStore[id]
|
||||
if !ok {
|
||||
return nil, errors.New("team id not found")
|
||||
}
|
||||
if id != tm.ID {
|
||||
// sanity chec
|
||||
return nil, errors.New("team id mismatch")
|
||||
}
|
||||
return tm, nil
|
||||
}
|
||||
ds.NewTeamFunc = func(ctx context.Context, team *fleet.Team) (*fleet.Team, error) {
|
||||
return nil, errors.New("not implemented")
|
||||
}
|
||||
ds.SaveTeamFunc = func(ctx context.Context, team *fleet.Team) (*fleet.Team, error) {
|
||||
return nil, errors.New("not implemented")
|
||||
}
|
||||
ds.NewMDMAppleConfigProfileFunc = func(ctx context.Context, profile fleet.MDMAppleConfigProfile) (*fleet.MDMAppleConfigProfile, error) {
|
||||
return nil, errors.New("not implemented")
|
||||
}
|
||||
ds.DeleteMDMAppleConfigProfileByTeamAndIdentifierFunc = func(ctx context.Context, teamID *uint, profileIdentifier string) error {
|
||||
return errors.New("not implemented")
|
||||
}
|
||||
ds.CopyDefaultMDMAppleBootstrapPackageFunc = func(ctx context.Context, ac *fleet.AppConfig, toTeamID uint) error {
|
||||
return errors.New("not implemented")
|
||||
}
|
||||
ds.NewJobFunc = func(ctx context.Context, job *fleet.Job) (*fleet.Job, error) {
|
||||
return nil, errors.New("not implemented")
|
||||
}
|
||||
ds.GetMDMAppleSetupAssistantFunc = func(ctx context.Context, teamID *uint) (*fleet.MDMAppleSetupAssistant, error) {
|
||||
return nil, errors.New("not implemented")
|
||||
}
|
||||
}
|
||||
|
||||
authzCtx := &authz_ctx.AuthorizationContext{}
|
||||
ctx = authz_ctx.NewContext(ctx, authzCtx)
|
||||
ctx = viewer.NewContext(ctx, viewer.Viewer{User: test.UserAdmin})
|
||||
actx, _ := authz_ctx.FromContext(ctx)
|
||||
actx.SetChecked()
|
||||
|
||||
t.Run("get preassign team", func(t *testing.T) {
|
||||
setupDS(t)
|
||||
|
||||
// preasign group corresponds to existing team so simply get it
|
||||
team, err := svc.GetOrCreatePreassignTeam(ctx, preassignGroups[0:1])
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, uint(1), team.ID)
|
||||
require.Equal(t, preassignGroups[0], team.Name)
|
||||
require.True(t, ds.TeamByNameFuncInvoked)
|
||||
require.False(t, ds.NewTeamFuncInvoked)
|
||||
require.False(t, ds.SaveTeamFuncInvoked)
|
||||
require.False(t, ds.NewMDMAppleConfigProfileFuncInvoked)
|
||||
require.False(t, ds.CopyDefaultMDMAppleBootstrapPackageFuncInvoked)
|
||||
require.False(t, ds.AppConfigFuncInvoked)
|
||||
require.False(t, ds.NewJobFuncInvoked)
|
||||
resetInvoked()
|
||||
})
|
||||
|
||||
t.Run("create preassign team", func(t *testing.T) {
|
||||
setupDS(t)
|
||||
|
||||
lastTeamID := uint(0)
|
||||
ds.NewTeamFunc = func(ctx context.Context, team *fleet.Team) (*fleet.Team, error) {
|
||||
for _, tm := range teamStore {
|
||||
if tm.Name == team.Name {
|
||||
return nil, errors.New("team name already exists")
|
||||
}
|
||||
}
|
||||
id := uint(len(teamStore) + 1)
|
||||
_, ok := teamStore[id]
|
||||
require.False(t, ok) // sanity check
|
||||
team.ID = id
|
||||
teamStore[id] = team
|
||||
lastTeamID = id
|
||||
return team, nil
|
||||
}
|
||||
ds.SaveTeamFunc = func(ctx context.Context, team *fleet.Team) (*fleet.Team, error) {
|
||||
tm, ok := teamStore[team.ID]
|
||||
if !ok {
|
||||
return nil, errors.New("invalid team id")
|
||||
}
|
||||
require.Equal(t, tm.ID, team.ID) // sanity check
|
||||
require.Equal(t, tm.Name, team.Name) // sanity check
|
||||
// NOTE: BootstrapPackage gets set by CopyDefaultMDMAppleBootstrapPackage
|
||||
// require.Equal(t, appConfig.MDM.MacOSSetup.BootstrapPackage.Value, team.Config.MDM.MacOSSetup.BootstrapPackage.Value)
|
||||
require.Equal(t, appConfig.MDM.MacOSSetup.EnableEndUserAuthentication, team.Config.MDM.MacOSSetup.EnableEndUserAuthentication) // set to default
|
||||
require.Equal(t, appConfig.MDM.MacOSSetup.MacOSSetupAssistant, team.Config.MDM.MacOSSetup.MacOSSetupAssistant) // set to default
|
||||
require.Equal(t, appConfig.MDM.MacOSSetup.EnableReleaseDeviceManually.Value, team.Config.MDM.MacOSSetup.EnableReleaseDeviceManually.Value) // set to default
|
||||
teamStore[tm.ID] = team
|
||||
return team, nil
|
||||
}
|
||||
ds.NewMDMAppleConfigProfileFunc = func(ctx context.Context, profile fleet.MDMAppleConfigProfile) (*fleet.MDMAppleConfigProfile, error) {
|
||||
require.Equal(t, lastTeamID, *profile.TeamID)
|
||||
require.Equal(t, mobileconfig.FleetFileVaultPayloadIdentifier, profile.Identifier)
|
||||
return &profile, nil
|
||||
}
|
||||
ds.DeleteMDMAppleConfigProfileByTeamAndIdentifierFunc = func(ctx context.Context, teamID *uint, profileIdentifier string) error {
|
||||
require.Equal(t, lastTeamID, *teamID)
|
||||
require.Equal(t, mobileconfig.FleetFileVaultPayloadIdentifier, profileIdentifier)
|
||||
return nil
|
||||
}
|
||||
ds.CopyDefaultMDMAppleBootstrapPackageFunc = func(ctx context.Context, ac *fleet.AppConfig, toTeamID uint) error {
|
||||
require.Equal(t, lastTeamID, toTeamID)
|
||||
require.NotNil(t, ac)
|
||||
require.Equal(t, "https://example.com/bootstrap.pkg", ac.MDM.MacOSSetup.BootstrapPackage.Value)
|
||||
teamStore[toTeamID].Config.MDM.MacOSSetup.BootstrapPackage = optjson.SetString(ac.MDM.MacOSSetup.BootstrapPackage.Value)
|
||||
return nil
|
||||
}
|
||||
var jobTask string
|
||||
ds.NewJobFunc = func(ctx context.Context, job *fleet.Job) (*fleet.Job, error) {
|
||||
// first task is UpdateProfile, next is ProfileChanged (when setup assistant is set)
|
||||
if jobTask == "" {
|
||||
jobTask = string(worker.MacosSetupAssistantUpdateProfile)
|
||||
} else {
|
||||
jobTask = string(worker.MacosSetupAssistantProfileChanged)
|
||||
}
|
||||
wantArgs, err := json.Marshal(map[string]interface{}{
|
||||
"task": jobTask,
|
||||
"team_id": lastTeamID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
wantJob := &fleet.Job{
|
||||
Name: "macos_setup_assistant",
|
||||
Args: (*json.RawMessage)(&wantArgs),
|
||||
State: fleet.JobStateQueued,
|
||||
}
|
||||
require.Equal(t, wantJob.Name, job.Name)
|
||||
require.Equal(t, string(*wantJob.Args), string(*job.Args))
|
||||
require.Equal(t, wantJob.State, job.State)
|
||||
return job, nil
|
||||
}
|
||||
setupAsstByTeam := make(map[uint]*fleet.MDMAppleSetupAssistant)
|
||||
globalSetupAsst := &fleet.MDMAppleSetupAssistant{
|
||||
ID: 15,
|
||||
TeamID: nil,
|
||||
Name: "test asst",
|
||||
Profile: json.RawMessage(`{"foo": "bar"}`),
|
||||
ProfileUUID: "abc-def",
|
||||
}
|
||||
setupAsstByTeam[0] = globalSetupAsst
|
||||
ds.GetMDMAppleSetupAssistantFunc = func(ctx context.Context, teamID *uint) (*fleet.MDMAppleSetupAssistant, error) {
|
||||
var tmID uint
|
||||
if teamID != nil {
|
||||
tmID = *teamID
|
||||
}
|
||||
asst := setupAsstByTeam[tmID]
|
||||
if asst == nil {
|
||||
return nil, eeservice.NotFoundError{}
|
||||
}
|
||||
return asst, nil
|
||||
}
|
||||
ds.SetOrUpdateMDMAppleSetupAssistantFunc = func(ctx context.Context, asst *fleet.MDMAppleSetupAssistant) (*fleet.MDMAppleSetupAssistant, error) {
|
||||
require.Equal(t, globalSetupAsst.Name, asst.Name)
|
||||
require.JSONEq(t, string(globalSetupAsst.Profile), string(asst.Profile))
|
||||
require.NotNil(t, asst.TeamID)
|
||||
require.EqualValues(t, lastTeamID, *asst.TeamID)
|
||||
setupAsstByTeam[*asst.TeamID] = asst
|
||||
return asst, nil
|
||||
}
|
||||
|
||||
// new team ("one - three") is created with bootstrap package and end user auth based on app config
|
||||
team, err := svc.GetOrCreatePreassignTeam(ctx, preassignGroups)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, uint(3), team.ID)
|
||||
require.Equal(t, eeservice.TeamNameFromPreassignGroups(preassignGroups), team.Name)
|
||||
require.True(t, ds.TeamByNameFuncInvoked)
|
||||
require.True(t, ds.NewTeamFuncInvoked)
|
||||
require.True(t, ds.SaveTeamFuncInvoked)
|
||||
require.True(t, ds.NewMDMAppleConfigProfileFuncInvoked)
|
||||
require.True(t, ds.CopyDefaultMDMAppleBootstrapPackageFuncInvoked)
|
||||
require.True(t, ds.AppConfigFuncInvoked)
|
||||
require.True(t, ds.GetMDMAppleSetupAssistantFuncInvoked)
|
||||
require.True(t, ds.SetOrUpdateMDMAppleSetupAssistantFuncInvoked)
|
||||
require.NotEmpty(t, team.Config.MDM.MacOSSetup.BootstrapPackage.Value)
|
||||
require.Equal(t, appConfig.MDM.MacOSSetup.BootstrapPackage.Value, team.Config.MDM.MacOSSetup.BootstrapPackage.Value)
|
||||
require.Equal(t, appConfig.MDM.MacOSSetup.MacOSSetupAssistant.Value, team.Config.MDM.MacOSSetup.MacOSSetupAssistant.Value)
|
||||
require.True(t, team.Config.MDM.MacOSSetup.EnableEndUserAuthentication)
|
||||
require.Equal(t, appConfig.MDM.MacOSSetup.EnableEndUserAuthentication, team.Config.MDM.MacOSSetup.EnableEndUserAuthentication)
|
||||
require.True(t, team.Config.MDM.MacOSSetup.EnableReleaseDeviceManually.Value)
|
||||
require.Equal(t, appConfig.MDM.MacOSSetup.EnableReleaseDeviceManually.Value, team.Config.MDM.MacOSSetup.EnableReleaseDeviceManually.Value)
|
||||
require.True(t, ds.NewJobFuncInvoked)
|
||||
resetInvoked()
|
||||
|
||||
jobTask = ""
|
||||
|
||||
// when called again, simply get the previously created team
|
||||
team, err = svc.GetOrCreatePreassignTeam(ctx, preassignGroups)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, uint(3), team.ID)
|
||||
require.Equal(t, eeservice.TeamNameFromPreassignGroups(preassignGroups), team.Name)
|
||||
require.True(t, ds.TeamByNameFuncInvoked)
|
||||
require.False(t, ds.NewTeamFuncInvoked)
|
||||
require.False(t, ds.SaveTeamFuncInvoked)
|
||||
require.False(t, ds.NewMDMAppleConfigProfileFuncInvoked)
|
||||
require.False(t, ds.CopyDefaultMDMAppleBootstrapPackageFuncInvoked)
|
||||
require.False(t, ds.AppConfigFuncInvoked)
|
||||
require.False(t, ds.NewJobFuncInvoked)
|
||||
require.False(t, ds.GetMDMAppleSetupAssistantFuncInvoked)
|
||||
require.False(t, ds.SetOrUpdateMDMAppleSetupAssistantFuncInvoked)
|
||||
require.NotEmpty(t, team.Config.MDM.MacOSSetup.BootstrapPackage.Value)
|
||||
require.Equal(t, appConfig.MDM.MacOSSetup.BootstrapPackage.Value, team.Config.MDM.MacOSSetup.BootstrapPackage.Value)
|
||||
require.Equal(t, appConfig.MDM.MacOSSetup.MacOSSetupAssistant.Value, team.Config.MDM.MacOSSetup.MacOSSetupAssistant.Value)
|
||||
require.True(t, team.Config.MDM.MacOSSetup.EnableEndUserAuthentication)
|
||||
require.Equal(t, appConfig.MDM.MacOSSetup.EnableEndUserAuthentication, team.Config.MDM.MacOSSetup.EnableEndUserAuthentication)
|
||||
require.True(t, team.Config.MDM.MacOSSetup.EnableReleaseDeviceManually.Value)
|
||||
require.Equal(t, appConfig.MDM.MacOSSetup.EnableReleaseDeviceManually.Value, team.Config.MDM.MacOSSetup.EnableReleaseDeviceManually.Value)
|
||||
resetInvoked()
|
||||
|
||||
jobTask = ""
|
||||
|
||||
// when a custom setup assistant is not set for "no team", we don't create
|
||||
// a custom setup assistant
|
||||
setupAsstByTeam[0] = nil
|
||||
|
||||
preassignGrousWithFoo := append(preassignGroups, "foo")
|
||||
team, err = svc.GetOrCreatePreassignTeam(ctx, preassignGrousWithFoo)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, uint(4), team.ID)
|
||||
require.Equal(t, eeservice.TeamNameFromPreassignGroups(preassignGrousWithFoo), team.Name)
|
||||
require.True(t, ds.TeamByNameFuncInvoked)
|
||||
require.True(t, ds.NewTeamFuncInvoked)
|
||||
require.True(t, ds.SaveTeamFuncInvoked)
|
||||
require.True(t, ds.NewMDMAppleConfigProfileFuncInvoked)
|
||||
require.True(t, ds.CopyDefaultMDMAppleBootstrapPackageFuncInvoked)
|
||||
require.True(t, ds.AppConfigFuncInvoked)
|
||||
require.True(t, ds.GetMDMAppleSetupAssistantFuncInvoked)
|
||||
require.False(t, ds.SetOrUpdateMDMAppleSetupAssistantFuncInvoked)
|
||||
require.NotEmpty(t, team.Config.MDM.MacOSSetup.BootstrapPackage.Value)
|
||||
require.Equal(t, appConfig.MDM.MacOSSetup.BootstrapPackage.Value, team.Config.MDM.MacOSSetup.BootstrapPackage.Value)
|
||||
require.True(t, team.Config.MDM.MacOSSetup.EnableEndUserAuthentication)
|
||||
require.Equal(t, appConfig.MDM.MacOSSetup.EnableEndUserAuthentication, team.Config.MDM.MacOSSetup.EnableEndUserAuthentication)
|
||||
require.True(t, team.Config.MDM.MacOSSetup.EnableReleaseDeviceManually.Value)
|
||||
require.Equal(t, appConfig.MDM.MacOSSetup.EnableReleaseDeviceManually.Value, team.Config.MDM.MacOSSetup.EnableReleaseDeviceManually.Value)
|
||||
resetInvoked()
|
||||
})
|
||||
|
||||
t.Run("modify team via apply team spec", func(t *testing.T) {
|
||||
setupDS(t)
|
||||
|
||||
ds.SaveTeamFunc = func(ctx context.Context, team *fleet.Team) (*fleet.Team, error) {
|
||||
tm, ok := teamStore[team.ID]
|
||||
if !ok {
|
||||
return nil, errors.New("invalid team id")
|
||||
}
|
||||
require.Equal(t, tm.ID, team.ID) // sanity check
|
||||
require.Equal(t, tm.Name, team.Name) // sanity check
|
||||
require.Empty(t, team.Config.MDM.MacOSSetup.EnableEndUserAuthentication) // not modified
|
||||
require.Empty(t, team.Config.MDM.MacOSSetup.BootstrapPackage.Value) // not modified
|
||||
require.False(t, team.Config.MDM.MacOSSetup.EnableReleaseDeviceManually.Value) // not modified
|
||||
return teamStore[tm.ID], nil
|
||||
}
|
||||
|
||||
// apply team spec does not apply defaults
|
||||
spec := &fleet.TeamSpec{
|
||||
Name: team2.Name,
|
||||
}
|
||||
_, err := svc.ApplyTeamSpecs(ctx, []*fleet.TeamSpec{spec}, fleet.ApplySpecOptions{})
|
||||
require.NoError(t, err)
|
||||
require.True(t, ds.SaveTeamFuncInvoked)
|
||||
require.True(t, ds.AppConfigFuncInvoked)
|
||||
require.True(t, ds.TeamByNameFuncInvoked)
|
||||
require.False(t, ds.NewTeamFuncInvoked)
|
||||
require.False(t, ds.NewMDMAppleConfigProfileFuncInvoked)
|
||||
require.False(t, ds.CopyDefaultMDMAppleBootstrapPackageFuncInvoked)
|
||||
require.False(t, ds.NewJobFuncInvoked)
|
||||
resetInvoked()
|
||||
})
|
||||
|
||||
t.Run("new team", func(t *testing.T) {
|
||||
setupDS(t)
|
||||
|
||||
ds.NewTeamFunc = func(ctx context.Context, team *fleet.Team) (*fleet.Team, error) {
|
||||
for _, tm := range teamStore {
|
||||
if tm.Name == team.Name {
|
||||
return nil, errors.New("team name already exists")
|
||||
}
|
||||
}
|
||||
id := uint(len(teamStore) + 1)
|
||||
_, ok := teamStore[id]
|
||||
require.False(t, ok) // sanity check
|
||||
require.Equal(t, "new team", team.Name)
|
||||
require.Equal(t, "new description", team.Description)
|
||||
require.False(t, team.Config.MDM.MacOSSetup.EnableEndUserAuthentication) // not set
|
||||
require.Empty(t, team.Config.MDM.MacOSSetup.BootstrapPackage.Value) // not set
|
||||
require.False(t, team.Config.MDM.MacOSSetup.EnableReleaseDeviceManually.Value) // not set
|
||||
team.ID = id
|
||||
teamStore[id] = team
|
||||
return team, nil
|
||||
}
|
||||
|
||||
// new team does not apply defaults
|
||||
_, err := svc.NewTeam(ctx, fleet.TeamPayload{
|
||||
Name: ptr.String("new team"),
|
||||
Description: ptr.String("new description"),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.True(t, ds.NewTeamFuncInvoked)
|
||||
require.True(t, ds.AppConfigFuncInvoked)
|
||||
require.False(t, ds.TeamByNameFuncInvoked)
|
||||
require.False(t, ds.SaveTeamFuncInvoked)
|
||||
require.False(t, ds.NewMDMAppleConfigProfileFuncInvoked)
|
||||
require.False(t, ds.CopyDefaultMDMAppleBootstrapPackageFuncInvoked)
|
||||
require.False(t, ds.NewJobFuncInvoked)
|
||||
resetInvoked()
|
||||
})
|
||||
|
||||
t.Run("apply team spec", func(t *testing.T) {
|
||||
setupDS(t)
|
||||
|
||||
ds.NewTeamFunc = func(ctx context.Context, team *fleet.Team) (*fleet.Team, error) {
|
||||
for _, tm := range teamStore {
|
||||
if tm.Name == team.Name {
|
||||
return nil, errors.New("team name already exists")
|
||||
}
|
||||
}
|
||||
id := uint(len(teamStore) + 1)
|
||||
_, ok := teamStore[id]
|
||||
require.False(t, ok) // sanity check
|
||||
require.Equal(t, "new team spec", team.Name) // set
|
||||
require.Equal(t, "12.0", team.Config.MDM.MacOSUpdates.MinimumVersion.Value) // set
|
||||
require.Equal(t, "2024-01-01", team.Config.MDM.MacOSUpdates.Deadline.Value) // set
|
||||
require.False(t, team.Config.MDM.MacOSSetup.EnableEndUserAuthentication) // not set
|
||||
require.Empty(t, team.Config.MDM.MacOSSetup.BootstrapPackage.Value) // not set
|
||||
require.False(t, team.Config.MDM.MacOSSetup.EnableReleaseDeviceManually.Value) // not set
|
||||
team.ID = id
|
||||
teamStore[id] = team
|
||||
return team, nil
|
||||
}
|
||||
ds.SaveTeamFunc = func(ctx context.Context, team *fleet.Team) (*fleet.Team, error) {
|
||||
tm, ok := teamStore[team.ID]
|
||||
if !ok {
|
||||
return nil, errors.New("invalid team id")
|
||||
}
|
||||
require.Equal(t, tm.ID, team.ID) // sanity check
|
||||
require.Equal(t, tm.Name, team.Name) // sanity check
|
||||
require.Equal(t, "12.0", team.Config.MDM.MacOSUpdates.MinimumVersion.Value) // unchanged
|
||||
require.Equal(t, "2025-01-01", team.Config.MDM.MacOSUpdates.Deadline.Value) // modified
|
||||
require.Empty(t, team.Config.MDM.MacOSSetup.EnableEndUserAuthentication) // not set
|
||||
require.Empty(t, team.Config.MDM.MacOSSetup.BootstrapPackage.Value) // not set
|
||||
require.False(t, team.Config.MDM.MacOSSetup.EnableReleaseDeviceManually.Value) // not set
|
||||
|
||||
return teamStore[tm.ID], nil
|
||||
}
|
||||
|
||||
spec := &fleet.TeamSpec{
|
||||
Name: "new team spec",
|
||||
MDM: fleet.TeamSpecMDM{
|
||||
MacOSUpdates: fleet.MacOSUpdates{
|
||||
MinimumVersion: optjson.SetString("12.0"),
|
||||
Deadline: optjson.SetString("2024-01-01"),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// apply team spec creates new team without defaults
|
||||
_, err := svc.ApplyTeamSpecs(ctx, []*fleet.TeamSpec{spec}, fleet.ApplySpecOptions{})
|
||||
require.NoError(t, err)
|
||||
require.True(t, ds.NewTeamFuncInvoked)
|
||||
require.True(t, ds.AppConfigFuncInvoked)
|
||||
require.True(t, ds.TeamByNameFuncInvoked)
|
||||
require.False(t, ds.SaveTeamFuncInvoked)
|
||||
require.False(t, ds.NewMDMAppleConfigProfileFuncInvoked)
|
||||
require.False(t, ds.CopyDefaultMDMAppleBootstrapPackageFuncInvoked)
|
||||
require.False(t, ds.NewJobFuncInvoked)
|
||||
resetInvoked()
|
||||
|
||||
// apply team spec edits existing team without applying defaults
|
||||
spec.MDM.MacOSUpdates.Deadline = optjson.SetString("2025-01-01")
|
||||
_, err = svc.ApplyTeamSpecs(ctx, []*fleet.TeamSpec{spec}, fleet.ApplySpecOptions{})
|
||||
require.NoError(t, err)
|
||||
require.True(t, ds.SaveTeamFuncInvoked)
|
||||
require.True(t, ds.AppConfigFuncInvoked)
|
||||
require.True(t, ds.TeamByNameFuncInvoked)
|
||||
require.False(t, ds.NewTeamFuncInvoked)
|
||||
require.False(t, ds.NewMDMAppleConfigProfileFuncInvoked)
|
||||
require.False(t, ds.CopyDefaultMDMAppleBootstrapPackageFuncInvoked)
|
||||
require.False(t, ds.NewJobFuncInvoked)
|
||||
resetInvoked()
|
||||
})
|
||||
}
|
||||
|
|
@ -2,24 +2,16 @@ package service
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/fleetdm/fleet/v4/pkg/optjson"
|
||||
"github.com/fleetdm/fleet/v4/server/authz"
|
||||
"github.com/fleetdm/fleet/v4/server/config"
|
||||
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
|
||||
"github.com/fleetdm/fleet/v4/server/contexts/viewer"
|
||||
"github.com/fleetdm/fleet/v4/server/fleet"
|
||||
"github.com/fleetdm/fleet/v4/server/mdm"
|
||||
"github.com/fleetdm/fleet/v4/server/mdm/apple/mobileconfig"
|
||||
"github.com/fleetdm/fleet/v4/server/mock"
|
||||
"github.com/fleetdm/fleet/v4/server/ptr"
|
||||
"github.com/fleetdm/fleet/v4/server/test"
|
||||
"github.com/fleetdm/fleet/v4/server/worker"
|
||||
"github.com/go-kit/log"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
|
|
@ -90,428 +82,6 @@ func TestMDMAppleDisableFileVaultAndEscrow(t *testing.T) {
|
|||
require.True(t, ds.DeleteMDMAppleConfigProfileByTeamAndIdentifierFuncInvoked)
|
||||
}
|
||||
|
||||
func TestGetOrCreatePreassignTeam(t *testing.T) {
|
||||
t.Skip("this test requires a mock or a way to import service.Service")
|
||||
ds, svc := setup(t)
|
||||
a, err := authz.NewAuthorizer()
|
||||
require.NoError(t, err)
|
||||
svc.authz = a
|
||||
svc.logger = log.NewNopLogger()
|
||||
|
||||
ssoSettings := fleet.SSOProviderSettings{
|
||||
EntityID: "foo",
|
||||
MetadataURL: "https://example.com/metadata.xml",
|
||||
IssuerURI: "https://example.com",
|
||||
}
|
||||
appConfig := &fleet.AppConfig{MDM: fleet.MDM{
|
||||
EnabledAndConfigured: true,
|
||||
EndUserAuthentication: fleet.MDMEndUserAuthentication{SSOProviderSettings: ssoSettings},
|
||||
MacOSSetup: fleet.MacOSSetup{
|
||||
BootstrapPackage: optjson.SetString("https://example.com/bootstrap.pkg"),
|
||||
EnableEndUserAuthentication: true,
|
||||
},
|
||||
}}
|
||||
preassignGroups := []string{"one", "three"}
|
||||
// initialize team store with team one and two already created
|
||||
team1 := &fleet.Team{
|
||||
ID: 1,
|
||||
Name: preassignGroups[0],
|
||||
}
|
||||
team2 := &fleet.Team{
|
||||
ID: 2,
|
||||
Name: "two",
|
||||
Config: fleet.TeamConfig{
|
||||
MDM: fleet.TeamMDM{
|
||||
MacOSSetup: fleet.MacOSSetup{MacOSSetupAssistant: optjson.SetString("foo/bar")},
|
||||
},
|
||||
},
|
||||
}
|
||||
teamStore := map[uint]*fleet.Team{1: team1, 2: team2}
|
||||
|
||||
resetInvoked := func() {
|
||||
ds.TeamByNameFuncInvoked = false
|
||||
ds.NewTeamFuncInvoked = false
|
||||
ds.SaveTeamFuncInvoked = false
|
||||
ds.NewMDMAppleConfigProfileFuncInvoked = false
|
||||
ds.CopyDefaultMDMAppleBootstrapPackageFuncInvoked = false
|
||||
ds.AppConfigFuncInvoked = false
|
||||
ds.NewJobFuncInvoked = false
|
||||
ds.GetMDMAppleSetupAssistantFuncInvoked = false
|
||||
ds.SetOrUpdateMDMAppleSetupAssistantFuncInvoked = false
|
||||
}
|
||||
setupDS := func(t *testing.T) {
|
||||
resetInvoked()
|
||||
|
||||
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
|
||||
return appConfig, nil
|
||||
}
|
||||
ds.NewActivityFunc = func(ctx context.Context, u *fleet.User, a fleet.ActivityDetails) error {
|
||||
return nil
|
||||
}
|
||||
ds.TeamByNameFunc = func(ctx context.Context, name string) (*fleet.Team, error) {
|
||||
for _, team := range teamStore {
|
||||
if team.Name == name {
|
||||
return team, nil
|
||||
}
|
||||
}
|
||||
return nil, ctxerr.Wrap(ctx, ¬FoundError{})
|
||||
}
|
||||
ds.TeamFunc = func(ctx context.Context, id uint) (*fleet.Team, error) {
|
||||
tm, ok := teamStore[id]
|
||||
if !ok {
|
||||
return nil, errors.New("team id not found")
|
||||
}
|
||||
if id != tm.ID {
|
||||
// sanity chec
|
||||
return nil, errors.New("team id mismatch")
|
||||
}
|
||||
return tm, nil
|
||||
}
|
||||
ds.NewTeamFunc = func(ctx context.Context, team *fleet.Team) (*fleet.Team, error) {
|
||||
return nil, errors.New("not implemented")
|
||||
}
|
||||
ds.SaveTeamFunc = func(ctx context.Context, team *fleet.Team) (*fleet.Team, error) {
|
||||
return nil, errors.New("not implemented")
|
||||
}
|
||||
ds.NewMDMAppleConfigProfileFunc = func(ctx context.Context, profile fleet.MDMAppleConfigProfile) (*fleet.MDMAppleConfigProfile, error) {
|
||||
return nil, errors.New("not implemented")
|
||||
}
|
||||
ds.DeleteMDMAppleConfigProfileByTeamAndIdentifierFunc = func(ctx context.Context, teamID *uint, profileIdentifier string) error {
|
||||
return errors.New("not implemented")
|
||||
}
|
||||
ds.CopyDefaultMDMAppleBootstrapPackageFunc = func(ctx context.Context, ac *fleet.AppConfig, toTeamID uint) error {
|
||||
return errors.New("not implemented")
|
||||
}
|
||||
ds.NewJobFunc = func(ctx context.Context, job *fleet.Job) (*fleet.Job, error) {
|
||||
return nil, errors.New("not implemented")
|
||||
}
|
||||
ds.GetMDMAppleSetupAssistantFunc = func(ctx context.Context, teamID *uint) (*fleet.MDMAppleSetupAssistant, error) {
|
||||
return nil, errors.New("not implemented")
|
||||
}
|
||||
}
|
||||
|
||||
ctx := viewer.NewContext(context.Background(), viewer.Viewer{User: test.UserAdmin})
|
||||
|
||||
t.Run("get preassign team", func(t *testing.T) {
|
||||
setupDS(t)
|
||||
|
||||
// preasign group corresponds to existing team so simply get it
|
||||
team, err := svc.getOrCreatePreassignTeam(ctx, preassignGroups[0:1])
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, uint(1), team.ID)
|
||||
require.Equal(t, preassignGroups[0], team.Name)
|
||||
require.True(t, ds.TeamByNameFuncInvoked)
|
||||
require.False(t, ds.NewTeamFuncInvoked)
|
||||
require.False(t, ds.SaveTeamFuncInvoked)
|
||||
require.False(t, ds.NewMDMAppleConfigProfileFuncInvoked)
|
||||
require.False(t, ds.CopyDefaultMDMAppleBootstrapPackageFuncInvoked)
|
||||
require.False(t, ds.AppConfigFuncInvoked)
|
||||
require.False(t, ds.NewJobFuncInvoked)
|
||||
resetInvoked()
|
||||
})
|
||||
|
||||
t.Run("create preassign team", func(t *testing.T) {
|
||||
// setup ds with assertions for this test
|
||||
setupDS(t)
|
||||
lastTeamID := uint(0)
|
||||
ds.NewTeamFunc = func(ctx context.Context, team *fleet.Team) (*fleet.Team, error) {
|
||||
for _, tm := range teamStore {
|
||||
if tm.Name == team.Name {
|
||||
return nil, errors.New("team name already exists")
|
||||
}
|
||||
}
|
||||
id := uint(len(teamStore) + 1)
|
||||
_, ok := teamStore[id]
|
||||
require.False(t, ok) // sanity check
|
||||
team.ID = id
|
||||
teamStore[id] = team
|
||||
lastTeamID = id
|
||||
return team, nil
|
||||
}
|
||||
ds.SaveTeamFunc = func(ctx context.Context, team *fleet.Team) (*fleet.Team, error) {
|
||||
tm, ok := teamStore[team.ID]
|
||||
if !ok {
|
||||
return nil, errors.New("invalid team id")
|
||||
}
|
||||
require.Equal(t, tm.ID, team.ID) // sanity check
|
||||
require.Equal(t, tm.Name, team.Name) // sanity check
|
||||
// // NOTE: BootstrapPackage is currently ignored by svc.ModifyTeam and gets set
|
||||
// // instead by CopyDefaultMDMAppleBootstrapPackage below
|
||||
// require.Equal(t, appConfig.MDM.MacOSSetup.BootstrapPackage.Value, team.Config.MDM.MacOSSetup.BootstrapPackage.Value)
|
||||
require.Equal(t, appConfig.MDM.MacOSSetup.EnableEndUserAuthentication, team.Config.MDM.MacOSSetup.EnableEndUserAuthentication) // set to default
|
||||
require.Equal(t, appConfig.MDM.MacOSSetup.MacOSSetupAssistant, team.Config.MDM.MacOSSetup.MacOSSetupAssistant) // set to default
|
||||
teamStore[tm.ID] = team
|
||||
return team, nil
|
||||
}
|
||||
ds.NewMDMAppleConfigProfileFunc = func(ctx context.Context, profile fleet.MDMAppleConfigProfile) (*fleet.MDMAppleConfigProfile, error) {
|
||||
require.Equal(t, lastTeamID, *profile.TeamID)
|
||||
require.Equal(t, mobileconfig.FleetFileVaultPayloadIdentifier, profile.Identifier)
|
||||
return &profile, nil
|
||||
}
|
||||
ds.DeleteMDMAppleConfigProfileByTeamAndIdentifierFunc = func(ctx context.Context, teamID *uint, profileIdentifier string) error {
|
||||
require.Equal(t, lastTeamID, *teamID)
|
||||
require.Equal(t, mobileconfig.FleetFileVaultPayloadIdentifier, profileIdentifier)
|
||||
return nil
|
||||
}
|
||||
ds.CopyDefaultMDMAppleBootstrapPackageFunc = func(ctx context.Context, ac *fleet.AppConfig, toTeamID uint) error {
|
||||
require.Equal(t, lastTeamID, toTeamID)
|
||||
require.NotNil(t, ac)
|
||||
require.Equal(t, "https://example.com/bootstrap.pkg", ac.MDM.MacOSSetup.BootstrapPackage.Value)
|
||||
teamStore[toTeamID].Config.MDM.MacOSSetup.BootstrapPackage = optjson.SetString(ac.MDM.MacOSSetup.BootstrapPackage.Value)
|
||||
return nil
|
||||
}
|
||||
ds.NewJobFunc = func(ctx context.Context, job *fleet.Job) (*fleet.Job, error) {
|
||||
wantArgs, err := json.Marshal(map[string]interface{}{
|
||||
"task": worker.MacosSetupAssistantUpdateProfile,
|
||||
"team_id": lastTeamID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
wantJob := &fleet.Job{
|
||||
Name: "macos_setup_assistant",
|
||||
Args: (*json.RawMessage)(&wantArgs),
|
||||
State: fleet.JobStateQueued,
|
||||
}
|
||||
require.Equal(t, wantJob.Name, job.Name)
|
||||
require.Equal(t, string(*wantJob.Args), string(*job.Args))
|
||||
require.Equal(t, wantJob.State, job.State)
|
||||
return job, nil
|
||||
}
|
||||
globalSetupAsst := &fleet.MDMAppleSetupAssistant{
|
||||
ID: 15,
|
||||
TeamID: nil,
|
||||
Name: "test asst",
|
||||
Profile: json.RawMessage(`{"foo": "bar"}`),
|
||||
ProfileUUID: "abc-def",
|
||||
}
|
||||
getSetupAsstFuncCalls := 0
|
||||
ds.GetMDMAppleSetupAssistantFunc = func(ctx context.Context, teamID *uint) (*fleet.MDMAppleSetupAssistant, error) {
|
||||
// first call is to grab the global team setup assistant, the
|
||||
// rest are for the team being created
|
||||
if getSetupAsstFuncCalls == 0 {
|
||||
require.Nil(t, teamID)
|
||||
} else {
|
||||
require.NotNil(t, teamID)
|
||||
require.EqualValues(t, lastTeamID, *teamID)
|
||||
}
|
||||
getSetupAsstFuncCalls++
|
||||
return globalSetupAsst, nil
|
||||
}
|
||||
ds.SetOrUpdateMDMAppleSetupAssistantFunc = func(ctx context.Context, asst *fleet.MDMAppleSetupAssistant) (*fleet.MDMAppleSetupAssistant, error) {
|
||||
require.Equal(t, globalSetupAsst.Name, asst.Name)
|
||||
require.JSONEq(t, string(globalSetupAsst.Profile), string(asst.Profile))
|
||||
require.NotNil(t, asst.TeamID)
|
||||
require.EqualValues(t, lastTeamID, *asst.TeamID)
|
||||
return asst, nil
|
||||
}
|
||||
|
||||
// new team is created with bootstrap package and end user auth based on app config
|
||||
team, err := svc.getOrCreatePreassignTeam(ctx, preassignGroups)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, uint(3), team.ID)
|
||||
require.Equal(t, teamNameFromPreassignGroups(preassignGroups), team.Name)
|
||||
require.True(t, ds.TeamByNameFuncInvoked)
|
||||
require.True(t, ds.NewTeamFuncInvoked)
|
||||
require.True(t, ds.SaveTeamFuncInvoked)
|
||||
require.True(t, ds.NewMDMAppleConfigProfileFuncInvoked)
|
||||
require.True(t, ds.CopyDefaultMDMAppleBootstrapPackageFuncInvoked)
|
||||
require.True(t, ds.AppConfigFuncInvoked)
|
||||
require.True(t, ds.GetMDMAppleSetupAssistantFuncInvoked)
|
||||
require.True(t, ds.SetOrUpdateMDMAppleSetupAssistantFuncInvoked)
|
||||
require.NotEmpty(t, team.Config.MDM.MacOSSetup.BootstrapPackage.Value)
|
||||
require.Equal(t, appConfig.MDM.MacOSSetup.BootstrapPackage.Value, team.Config.MDM.MacOSSetup.BootstrapPackage.Value)
|
||||
require.Equal(t, appConfig.MDM.MacOSSetup.MacOSSetupAssistant.Value, team.Config.MDM.MacOSSetup.MacOSSetupAssistant.Value)
|
||||
require.True(t, team.Config.MDM.MacOSSetup.EnableEndUserAuthentication)
|
||||
require.Equal(t, appConfig.MDM.MacOSSetup.EnableEndUserAuthentication, team.Config.MDM.MacOSSetup.EnableEndUserAuthentication)
|
||||
require.True(t, ds.NewJobFuncInvoked)
|
||||
resetInvoked()
|
||||
|
||||
// when called again, simply get the previously created team
|
||||
team, err = svc.getOrCreatePreassignTeam(ctx, preassignGroups)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, uint(3), team.ID)
|
||||
require.Equal(t, teamNameFromPreassignGroups(preassignGroups), team.Name)
|
||||
require.True(t, ds.TeamByNameFuncInvoked)
|
||||
require.False(t, ds.NewTeamFuncInvoked)
|
||||
require.False(t, ds.SaveTeamFuncInvoked)
|
||||
require.False(t, ds.NewMDMAppleConfigProfileFuncInvoked)
|
||||
require.False(t, ds.CopyDefaultMDMAppleBootstrapPackageFuncInvoked)
|
||||
require.False(t, ds.AppConfigFuncInvoked)
|
||||
require.False(t, ds.NewJobFuncInvoked)
|
||||
require.False(t, ds.GetMDMAppleSetupAssistantFuncInvoked)
|
||||
require.False(t, ds.SetOrUpdateMDMAppleSetupAssistantFuncInvoked)
|
||||
require.NotEmpty(t, team.Config.MDM.MacOSSetup.BootstrapPackage.Value)
|
||||
require.Equal(t, appConfig.MDM.MacOSSetup.BootstrapPackage.Value, team.Config.MDM.MacOSSetup.BootstrapPackage.Value)
|
||||
require.Equal(t, appConfig.MDM.MacOSSetup.MacOSSetupAssistant.Value, team.Config.MDM.MacOSSetup.MacOSSetupAssistant.Value)
|
||||
require.True(t, team.Config.MDM.MacOSSetup.EnableEndUserAuthentication)
|
||||
require.Equal(t, appConfig.MDM.MacOSSetup.EnableEndUserAuthentication, team.Config.MDM.MacOSSetup.EnableEndUserAuthentication)
|
||||
resetInvoked()
|
||||
|
||||
// when a custom setup assistant is not set for "no team", we don't create a custom setup assistant
|
||||
ds.GetMDMAppleSetupAssistantFunc = func(ctx context.Context, teamID *uint) (*fleet.MDMAppleSetupAssistant, error) {
|
||||
require.Nil(t, teamID)
|
||||
return nil, ctxerr.Wrap(ctx, ¬FoundError{})
|
||||
}
|
||||
preassignGrousWithFoo := append(preassignGroups, "foo")
|
||||
team, err = svc.getOrCreatePreassignTeam(ctx, preassignGrousWithFoo)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, uint(4), team.ID)
|
||||
require.Equal(t, teamNameFromPreassignGroups(preassignGrousWithFoo), team.Name)
|
||||
require.True(t, ds.TeamByNameFuncInvoked)
|
||||
require.True(t, ds.NewTeamFuncInvoked)
|
||||
require.True(t, ds.SaveTeamFuncInvoked)
|
||||
require.True(t, ds.NewMDMAppleConfigProfileFuncInvoked)
|
||||
require.True(t, ds.CopyDefaultMDMAppleBootstrapPackageFuncInvoked)
|
||||
require.True(t, ds.AppConfigFuncInvoked)
|
||||
require.True(t, ds.GetMDMAppleSetupAssistantFuncInvoked)
|
||||
require.False(t, ds.SetOrUpdateMDMAppleSetupAssistantFuncInvoked)
|
||||
require.NotEmpty(t, team.Config.MDM.MacOSSetup.BootstrapPackage.Value)
|
||||
require.Equal(t, appConfig.MDM.MacOSSetup.BootstrapPackage.Value, team.Config.MDM.MacOSSetup.BootstrapPackage.Value)
|
||||
require.True(t, team.Config.MDM.MacOSSetup.EnableEndUserAuthentication)
|
||||
require.Equal(t, appConfig.MDM.MacOSSetup.EnableEndUserAuthentication, team.Config.MDM.MacOSSetup.EnableEndUserAuthentication)
|
||||
resetInvoked()
|
||||
})
|
||||
|
||||
t.Run("modify team", func(t *testing.T) {
|
||||
// setup ds with assertions this test
|
||||
setupDS(t)
|
||||
ds.SaveTeamFunc = func(ctx context.Context, team *fleet.Team) (*fleet.Team, error) {
|
||||
tm, ok := teamStore[team.ID]
|
||||
if !ok {
|
||||
return nil, errors.New("invalid team id")
|
||||
}
|
||||
require.Equal(t, tm.ID, team.ID) // sanity check
|
||||
require.Equal(t, tm.Name, team.Name) // sanity check
|
||||
require.Empty(t, team.Config.MDM.MacOSSetup.EnableEndUserAuthentication) // not modified
|
||||
require.Empty(t, team.Config.MDM.MacOSSetup.BootstrapPackage.Value) // not modified
|
||||
require.NotEmpty(t, team.Description) // modified
|
||||
teamStore[tm.ID].Description = team.Description
|
||||
return teamStore[tm.ID], nil
|
||||
}
|
||||
|
||||
// modify team does not apply defaults
|
||||
_, err := svc.ModifyTeam(ctx, 2, fleet.TeamPayload{Description: ptr.String("new description")})
|
||||
require.NoError(t, err)
|
||||
require.True(t, ds.SaveTeamFuncInvoked)
|
||||
require.True(t, ds.AppConfigFuncInvoked)
|
||||
require.False(t, ds.TeamByNameFuncInvoked)
|
||||
require.False(t, ds.NewTeamFuncInvoked)
|
||||
require.False(t, ds.NewMDMAppleConfigProfileFuncInvoked)
|
||||
require.False(t, ds.CopyDefaultMDMAppleBootstrapPackageFuncInvoked)
|
||||
require.False(t, ds.NewJobFuncInvoked)
|
||||
resetInvoked()
|
||||
})
|
||||
|
||||
t.Run("new team", func(t *testing.T) {
|
||||
// setup ds with assertions this test
|
||||
setupDS(t)
|
||||
ds.NewTeamFunc = func(ctx context.Context, team *fleet.Team) (*fleet.Team, error) {
|
||||
for _, tm := range teamStore {
|
||||
if tm.Name == team.Name {
|
||||
return nil, errors.New("team name already exists")
|
||||
}
|
||||
}
|
||||
id := uint(len(teamStore) + 1)
|
||||
_, ok := teamStore[id]
|
||||
require.False(t, ok) // sanity check
|
||||
require.Equal(t, "new team", team.Name)
|
||||
require.Equal(t, "new description", team.Description)
|
||||
require.Empty(t, team.Config.MDM.MacOSSetup.EnableEndUserAuthentication) // not set
|
||||
require.Empty(t, team.Config.MDM.MacOSSetup.BootstrapPackage.Value) // not set
|
||||
team.ID = id
|
||||
teamStore[id] = team
|
||||
return team, nil
|
||||
}
|
||||
|
||||
// new team does not apply defaults
|
||||
_, err := svc.NewTeam(ctx, fleet.TeamPayload{
|
||||
Name: ptr.String("new team"),
|
||||
Description: ptr.String("new description"),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.True(t, ds.NewTeamFuncInvoked)
|
||||
require.True(t, ds.AppConfigFuncInvoked)
|
||||
require.False(t, ds.TeamByNameFuncInvoked)
|
||||
require.False(t, ds.SaveTeamFuncInvoked)
|
||||
require.False(t, ds.NewMDMAppleConfigProfileFuncInvoked)
|
||||
require.False(t, ds.CopyDefaultMDMAppleBootstrapPackageFuncInvoked)
|
||||
require.False(t, ds.NewJobFuncInvoked)
|
||||
resetInvoked()
|
||||
})
|
||||
|
||||
t.Run("apply team spec", func(t *testing.T) {
|
||||
// setup ds with assertions this test
|
||||
setupDS(t)
|
||||
ds.NewTeamFunc = func(ctx context.Context, team *fleet.Team) (*fleet.Team, error) {
|
||||
for _, tm := range teamStore {
|
||||
if tm.Name == team.Name {
|
||||
return nil, errors.New("team name already exists")
|
||||
}
|
||||
}
|
||||
id := uint(len(teamStore) + 1)
|
||||
_, ok := teamStore[id]
|
||||
require.False(t, ok) // sanity check
|
||||
require.Equal(t, "new team spec", team.Name) // set
|
||||
require.Equal(t, "12.0", team.Config.MDM.MacOSUpdates.MinimumVersion.Value) // set
|
||||
require.Equal(t, "2024-01-01", team.Config.MDM.MacOSUpdates.Deadline.Value) // set // not set
|
||||
require.Empty(t, team.Config.MDM.MacOSSetup.EnableEndUserAuthentication) // not set
|
||||
require.Empty(t, team.Config.MDM.MacOSSetup.BootstrapPackage.Value) // not set
|
||||
team.ID = id
|
||||
teamStore[id] = team
|
||||
return team, nil
|
||||
}
|
||||
ds.SaveTeamFunc = func(ctx context.Context, team *fleet.Team) (*fleet.Team, error) {
|
||||
tm, ok := teamStore[team.ID]
|
||||
if !ok {
|
||||
return nil, errors.New("invalid team id")
|
||||
}
|
||||
require.Equal(t, tm.ID, team.ID) // sanity check
|
||||
require.Equal(t, tm.Name, team.Name) // sanity check
|
||||
require.Equal(t, "12.0", team.Config.MDM.MacOSUpdates.MinimumVersion.Value) // unchanged
|
||||
require.Equal(t, "2025-01-01", team.Config.MDM.MacOSUpdates.Deadline.Value) // modified // not set
|
||||
require.Empty(t, team.Config.MDM.MacOSSetup.EnableEndUserAuthentication) // not set
|
||||
require.Empty(t, team.Config.MDM.MacOSSetup.BootstrapPackage.Value) // not set
|
||||
|
||||
teamStore[tm.ID].Description = team.Description
|
||||
return teamStore[tm.ID], nil
|
||||
}
|
||||
|
||||
spec := &fleet.TeamSpec{
|
||||
Name: "new team spec",
|
||||
MDM: fleet.TeamSpecMDM{
|
||||
MacOSUpdates: fleet.MacOSUpdates{
|
||||
MinimumVersion: optjson.SetString("12.0"),
|
||||
Deadline: optjson.SetString("2024-01-01"),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// apply team spec creates new team without defaults
|
||||
_, err := svc.ApplyTeamSpecs(ctx, []*fleet.TeamSpec{spec}, fleet.ApplySpecOptions{})
|
||||
require.NoError(t, err)
|
||||
require.True(t, ds.NewTeamFuncInvoked)
|
||||
require.True(t, ds.AppConfigFuncInvoked)
|
||||
require.True(t, ds.TeamByNameFuncInvoked)
|
||||
require.False(t, ds.SaveTeamFuncInvoked)
|
||||
require.False(t, ds.NewMDMAppleConfigProfileFuncInvoked)
|
||||
require.False(t, ds.CopyDefaultMDMAppleBootstrapPackageFuncInvoked)
|
||||
require.False(t, ds.NewJobFuncInvoked)
|
||||
resetInvoked()
|
||||
|
||||
// apply team spec edits existing team without applying defaults
|
||||
spec.MDM.MacOSUpdates.Deadline = optjson.SetString("2025-01-01")
|
||||
_, err = svc.ApplyTeamSpecs(ctx, []*fleet.TeamSpec{spec}, fleet.ApplySpecOptions{})
|
||||
require.NoError(t, err)
|
||||
require.True(t, ds.SaveTeamFuncInvoked)
|
||||
require.True(t, ds.AppConfigFuncInvoked)
|
||||
require.True(t, ds.TeamByNameFuncInvoked)
|
||||
require.False(t, ds.NewTeamFuncInvoked)
|
||||
require.False(t, ds.NewMDMAppleConfigProfileFuncInvoked)
|
||||
require.False(t, ds.CopyDefaultMDMAppleBootstrapPackageFuncInvoked)
|
||||
require.False(t, ds.NewJobFuncInvoked)
|
||||
resetInvoked()
|
||||
})
|
||||
}
|
||||
|
||||
var (
|
||||
testCert = `-----BEGIN CERTIFICATE-----
|
||||
MIID6DCCAdACFGX99Sw4aF2qKGLucoIWQRAXHrs1MA0GCSqGSIb3DQEBCwUAMDUx
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import (
|
|||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/fleetdm/fleet/v4/pkg/optjson"
|
||||
"github.com/fleetdm/fleet/v4/server"
|
||||
"github.com/fleetdm/fleet/v4/server/authz"
|
||||
authz_ctx "github.com/fleetdm/fleet/v4/server/contexts/authz"
|
||||
|
|
@ -868,12 +869,16 @@ func (svc *Service) createTeamFromSpec(
|
|||
return nil, err
|
||||
}
|
||||
macOSSetup := spec.MDM.MacOSSetup
|
||||
if macOSSetup.MacOSSetupAssistant.Value != "" || macOSSetup.BootstrapPackage.Value != "" {
|
||||
if !macOSSetup.EnableReleaseDeviceManually.Valid {
|
||||
macOSSetup.EnableReleaseDeviceManually = optjson.SetBool(false)
|
||||
}
|
||||
if macOSSetup.MacOSSetupAssistant.Value != "" || macOSSetup.BootstrapPackage.Value != "" || macOSSetup.EnableReleaseDeviceManually.Value {
|
||||
if !appCfg.MDM.EnabledAndConfigured {
|
||||
return nil, ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("macos_setup",
|
||||
`Couldn't update macos_setup because MDM features aren't turned on in Fleet. Use fleetctl generate mdm-apple and then fleet serve with mdm configuration to turn on MDM features.`))
|
||||
}
|
||||
}
|
||||
|
||||
enableDiskEncryption := spec.MDM.EnableDiskEncryption.Value
|
||||
if !spec.MDM.EnableDiskEncryption.Valid {
|
||||
if de := macOSSettings.DeprecatedEnableDiskEncryption; de != nil {
|
||||
|
|
@ -1005,8 +1010,11 @@ func (svc *Service) editTeamFromSpec(
|
|||
`Couldn't edit enable_disk_encryption. Neither macOS MDM nor Windows is turned on. Visit https://fleetdm.com/docs/using-fleet to learn how to turn on MDM.`))
|
||||
}
|
||||
|
||||
if !team.Config.MDM.MacOSSetup.EnableReleaseDeviceManually.Valid {
|
||||
team.Config.MDM.MacOSSetup.EnableReleaseDeviceManually = optjson.SetBool(false)
|
||||
}
|
||||
oldMacOSSetup := team.Config.MDM.MacOSSetup
|
||||
var didUpdateSetupAssistant, didUpdateBootstrapPackage bool
|
||||
var didUpdateSetupAssistant, didUpdateBootstrapPackage, didUpdateEnableReleaseManually bool
|
||||
if spec.MDM.MacOSSetup.MacOSSetupAssistant.Set {
|
||||
didUpdateSetupAssistant = oldMacOSSetup.MacOSSetupAssistant.Value != spec.MDM.MacOSSetup.MacOSSetupAssistant.Value
|
||||
team.Config.MDM.MacOSSetup.MacOSSetupAssistant = spec.MDM.MacOSSetup.MacOSSetupAssistant
|
||||
|
|
@ -1015,12 +1023,17 @@ func (svc *Service) editTeamFromSpec(
|
|||
didUpdateBootstrapPackage = oldMacOSSetup.BootstrapPackage.Value != spec.MDM.MacOSSetup.BootstrapPackage.Value
|
||||
team.Config.MDM.MacOSSetup.BootstrapPackage = spec.MDM.MacOSSetup.BootstrapPackage
|
||||
}
|
||||
if spec.MDM.MacOSSetup.EnableReleaseDeviceManually.Valid {
|
||||
didUpdateEnableReleaseManually = oldMacOSSetup.EnableReleaseDeviceManually.Value != spec.MDM.MacOSSetup.EnableReleaseDeviceManually.Value
|
||||
team.Config.MDM.MacOSSetup.EnableReleaseDeviceManually = spec.MDM.MacOSSetup.EnableReleaseDeviceManually
|
||||
}
|
||||
// TODO(mna): doesn't look like we create an activity for macos updates when
|
||||
// modified via spec? Doing the same for Windows, but should we?
|
||||
|
||||
if !appCfg.MDM.EnabledAndConfigured &&
|
||||
((didUpdateSetupAssistant && team.Config.MDM.MacOSSetup.MacOSSetupAssistant.Value != "") ||
|
||||
(didUpdateBootstrapPackage && team.Config.MDM.MacOSSetup.BootstrapPackage.Value != "")) {
|
||||
(didUpdateBootstrapPackage && team.Config.MDM.MacOSSetup.BootstrapPackage.Value != "") ||
|
||||
(didUpdateEnableReleaseManually && team.Config.MDM.MacOSSetup.EnableReleaseDeviceManually.Value)) {
|
||||
return ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("macos_setup",
|
||||
`Couldn't update macos_setup because MDM features aren't turned on in Fleet. Use fleetctl generate mdm-apple and then fleet serve with mdm configuration to turn on MDM features.`))
|
||||
}
|
||||
|
|
@ -1280,6 +1293,13 @@ func (svc *Service) updateTeamMDMAppleSetup(ctx context.Context, tm *fleet.Team,
|
|||
}
|
||||
}
|
||||
|
||||
if payload.EnableReleaseDeviceManually != nil {
|
||||
if tm.Config.MDM.MacOSSetup.EnableReleaseDeviceManually.Value != *payload.EnableReleaseDeviceManually {
|
||||
tm.Config.MDM.MacOSSetup.EnableReleaseDeviceManually = optjson.SetBool(*payload.EnableReleaseDeviceManually)
|
||||
didUpdate = true
|
||||
}
|
||||
}
|
||||
|
||||
if didUpdate {
|
||||
if _, err := svc.ds.SaveTeam(ctx, tm); err != nil {
|
||||
return err
|
||||
|
|
|
|||
|
|
@ -146,6 +146,7 @@ const DEFAULT_CONFIG_MOCK: IConfig = {
|
|||
bootstrap_package: "",
|
||||
enable_end_user_authentication: false,
|
||||
macos_setup_assistant: null,
|
||||
enable_release_device_manually: false,
|
||||
},
|
||||
macos_migration: {
|
||||
enable: false,
|
||||
|
|
|
|||
|
|
@ -8,14 +8,20 @@ type CardColor = "white" | "gray" | "purple" | "yellow";
|
|||
|
||||
interface ICardProps {
|
||||
children?: React.ReactNode;
|
||||
/** The size of the border radius. Defaults to `small` */
|
||||
/** The size of the border radius. Defaults to `small`. */
|
||||
borderRadiusSize?: BorderRadiusSize;
|
||||
/** Includes the card shadows. Defaults to `false` */
|
||||
includeShadow?: boolean;
|
||||
/** The color of the card. Defaults to `white` */
|
||||
color?: CardColor;
|
||||
className?: string;
|
||||
/** Increases to 40px padding. Defaults to `false` */
|
||||
/** The size of the padding around the content of the card. Defaults to `large`.
|
||||
*
|
||||
* These correspond to the padding sizes in the design system. Look at `padding.scss` for values */
|
||||
paddingSize?: "small" | "medium" | "large" | "xlarge" | "xxlarge";
|
||||
/** NOTE: DEPRICATED. Use `paddingSize` prop instead.
|
||||
*
|
||||
* Increases to 40px padding. Defaults to `false` */
|
||||
largePadding?: boolean;
|
||||
}
|
||||
|
||||
|
|
@ -30,12 +36,16 @@ const Card = ({
|
|||
color = "white",
|
||||
className,
|
||||
largePadding = false,
|
||||
paddingSize = "large",
|
||||
}: ICardProps) => {
|
||||
const classNames = classnames(
|
||||
baseClass,
|
||||
`${baseClass}__${color}`,
|
||||
`${baseClass}__radius-${borderRadiusSize}`,
|
||||
{
|
||||
// TODO: simplify this when we've replaced largePadding prop with paddingSize
|
||||
[`${baseClass}__padding-${paddingSize}`]:
|
||||
!largePadding && paddingSize !== undefined,
|
||||
[`${baseClass}__shadow`]: includeShadow,
|
||||
[`${baseClass}__large-padding`]: largePadding,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -22,7 +22,29 @@
|
|||
box-shadow: $box-shadow;
|
||||
}
|
||||
|
||||
&__padding-small {
|
||||
padding: $pad-small;
|
||||
}
|
||||
|
||||
&__padding-medium {
|
||||
padding: $pad-medium;
|
||||
}
|
||||
|
||||
&__padding-large {
|
||||
padding: $pad-large;
|
||||
}
|
||||
|
||||
&__padding-xlarge {
|
||||
padding: $pad-xlarge;
|
||||
}
|
||||
|
||||
&__padding-xxlarge {
|
||||
padding: $pad-xxlarge;
|
||||
}
|
||||
|
||||
// 40px padding
|
||||
// TODO: remove when we've replaced all instances of largePadding with
|
||||
// paddingSize prop
|
||||
&__large-padding {
|
||||
padding: $pad-xxlarge;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,6 +32,8 @@ interface IFileUploaderProps {
|
|||
* https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/accept
|
||||
*/
|
||||
accept?: string;
|
||||
/** The text to display on the upload button */
|
||||
buttonMessage?: string;
|
||||
className?: string;
|
||||
onFileUpload: (files: FileList | null) => void;
|
||||
}
|
||||
|
|
@ -45,6 +47,7 @@ const FileUploader = ({
|
|||
additionalInfo,
|
||||
isLoading = false,
|
||||
accept,
|
||||
buttonMessage = "Upload",
|
||||
className,
|
||||
onFileUpload,
|
||||
}: IFileUploaderProps) => {
|
||||
|
|
@ -73,11 +76,11 @@ const FileUploader = ({
|
|||
variant="brand"
|
||||
isLoading={isLoading}
|
||||
>
|
||||
<label htmlFor="upload-profile">Upload</label>
|
||||
<label htmlFor="upload-file">{buttonMessage}</label>
|
||||
</Button>
|
||||
<input
|
||||
accept={accept}
|
||||
id="upload-profile"
|
||||
id="upload-file"
|
||||
type="file"
|
||||
onChange={(e) => {
|
||||
onFileUpload(e.target.files);
|
||||
|
|
|
|||
|
|
@ -30,12 +30,12 @@
|
|||
|
||||
&__upload-button {
|
||||
margin-top: 8px;
|
||||
// we handle the padding in the label so the entire button is clickable
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
label {
|
||||
height: 36px;
|
||||
width: 78.2667px;
|
||||
padding: $pad-small $pad-medium;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ export interface IMdmConfig {
|
|||
bootstrap_package: string | null;
|
||||
enable_end_user_authentication: boolean;
|
||||
macos_setup_assistant: string | null;
|
||||
enable_release_device_manually: boolean | null;
|
||||
};
|
||||
macos_migration: IMacOsMigrationSettings;
|
||||
windows_updates: {
|
||||
|
|
|
|||
|
|
@ -56,7 +56,8 @@ export interface ITeam extends ITeamSummary {
|
|||
macos_setup: {
|
||||
bootstrap_package: string | null;
|
||||
enable_end_user_authentication: boolean;
|
||||
macos_setup_assistant: string | null; // TODO: types?
|
||||
macos_setup_assistant: string | null;
|
||||
enable_release_device_manually: boolean | null;
|
||||
};
|
||||
windows_updates: {
|
||||
deadline_days: number | null;
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { ISideNavItem } from "pages/admin/components/SideNav/SideNav";
|
|||
|
||||
import EndUserAuthentication from "./cards/EndUserAuthentication/EndUserAuthentication";
|
||||
import BootstrapPackage from "./cards/BootstrapPackage";
|
||||
import SetupAssistant from "./cards/SetupAssistant";
|
||||
|
||||
interface ISetupExperienceCardProps {
|
||||
currentTeamId?: number;
|
||||
|
|
@ -25,6 +26,12 @@ const SETUP_EXPERIENCE_NAV_ITEMS: ISideNavItem<
|
|||
path: PATHS.CONTROLS_BOOTSTRAP_PACKAGE,
|
||||
Card: BootstrapPackage,
|
||||
},
|
||||
{
|
||||
title: "Setup assistant",
|
||||
urlSection: "setup-assistant",
|
||||
path: PATHS.CONTROLS_SETUP_ASSITANT,
|
||||
Card: SetupAssistant,
|
||||
},
|
||||
];
|
||||
|
||||
export default SETUP_EXPERIENCE_NAV_ITEMS;
|
||||
|
|
|
|||
|
|
@ -6,7 +6,9 @@
|
|||
font-size: $x-small;
|
||||
|
||||
h2 {
|
||||
font-size: $x-small; // needed to override global h2 style
|
||||
margin: 0;
|
||||
font-size: $small;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
&__preview-img {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,142 @@
|
|||
import React, { useState } from "react";
|
||||
import { useQuery } from "react-query";
|
||||
import { AxiosError } from "axios";
|
||||
|
||||
import { IConfig } from "interfaces/config";
|
||||
import { API_NO_TEAM_ID, ITeamConfig } from "interfaces/team";
|
||||
import configAPI from "services/entities/config";
|
||||
import teamsAPI, { ILoadTeamResponse } from "services/entities/teams";
|
||||
import mdmAPI, {
|
||||
IAppleSetupEnrollmentProfileResponse,
|
||||
} from "services/entities/mdm";
|
||||
import { DEFAULT_USE_QUERY_OPTIONS } from "utilities/constants";
|
||||
|
||||
import SectionHeader from "components/SectionHeader";
|
||||
import Spinner from "components/Spinner";
|
||||
import CustomLink from "components/CustomLink";
|
||||
|
||||
import SetupAssistantPreview from "./components/SetupAssistantPreview";
|
||||
import SetupAssistantProfileUploader from "./components/SetupAssistantProfileUploader";
|
||||
import SetuAssistantProfileCard from "./components/SetupAssistantProfileCard/SetupAssistantProfileCard";
|
||||
import DeleteAutoEnrollmentProfile from "./components/DeleteAutoEnrollmentProfile";
|
||||
import AdvancedOptionsForm from "./components/AdvancedOptionsForm";
|
||||
|
||||
const baseClass = "setup-assistant";
|
||||
|
||||
interface ISetupAssistantProps {
|
||||
currentTeamId: number;
|
||||
}
|
||||
|
||||
const SetupAssistant = ({ currentTeamId }: ISetupAssistantProps) => {
|
||||
const [showDeleteProfileModal, setShowDeleteProfileModal] = useState(false);
|
||||
|
||||
const { data: globalConfig, isLoading: isLoadingGlobalConfig } = useQuery<
|
||||
IConfig,
|
||||
Error
|
||||
>(["config", currentTeamId], () => configAPI.loadAll(), {
|
||||
...DEFAULT_USE_QUERY_OPTIONS,
|
||||
retry: false,
|
||||
enabled: currentTeamId === API_NO_TEAM_ID,
|
||||
});
|
||||
|
||||
const { data: teamConfig, isLoading: isLoadingTeamConfig } = useQuery<
|
||||
ILoadTeamResponse,
|
||||
Error,
|
||||
ITeamConfig
|
||||
>(["team", currentTeamId], () => teamsAPI.load(currentTeamId), {
|
||||
...DEFAULT_USE_QUERY_OPTIONS,
|
||||
refetchOnWindowFocus: false,
|
||||
retry: false,
|
||||
enabled: currentTeamId !== API_NO_TEAM_ID,
|
||||
select: (res) => res.team,
|
||||
});
|
||||
|
||||
const {
|
||||
data: enrollmentProfileData,
|
||||
isLoading: isLoadingEnrollmentProfile,
|
||||
error: enrollmentProfileError,
|
||||
refetch: refetchEnrollmentProfile,
|
||||
} = useQuery<IAppleSetupEnrollmentProfileResponse, AxiosError>(
|
||||
["enrollment_profile", currentTeamId],
|
||||
() => mdmAPI.getSetupEnrollmentProfile(currentTeamId),
|
||||
{
|
||||
...DEFAULT_USE_QUERY_OPTIONS,
|
||||
retry: false,
|
||||
}
|
||||
);
|
||||
|
||||
const getReleaseDeviceSetting = () => {
|
||||
if (currentTeamId === API_NO_TEAM_ID) {
|
||||
return (
|
||||
globalConfig?.mdm.macos_setup.enable_release_device_manually || false
|
||||
);
|
||||
}
|
||||
return teamConfig?.mdm?.macos_setup.enable_release_device_manually || false;
|
||||
};
|
||||
|
||||
const onUpload = () => {
|
||||
refetchEnrollmentProfile();
|
||||
};
|
||||
|
||||
const onDelete = () => {
|
||||
setShowDeleteProfileModal(false);
|
||||
refetchEnrollmentProfile();
|
||||
};
|
||||
|
||||
const defaultReleaseDeviceSetting = getReleaseDeviceSetting();
|
||||
|
||||
const isLoading =
|
||||
isLoadingGlobalConfig || isLoadingTeamConfig || isLoadingEnrollmentProfile;
|
||||
const enrollmentProfileNotFound = enrollmentProfileError?.status === 404;
|
||||
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
<SectionHeader title="Setup assistant" />
|
||||
{isLoading ? (
|
||||
<Spinner />
|
||||
) : (
|
||||
<div className={`${baseClass}__content`}>
|
||||
<div className={`${baseClass}__upload-container`}>
|
||||
<p className={`${baseClass}__section-description`}>
|
||||
Add an automatic enrollment profile to customize the macOS Setup
|
||||
Assistant.
|
||||
<CustomLink
|
||||
url="https://fleetdm.com/learn-more-about/setup-assistant"
|
||||
text="Learn how"
|
||||
newTab
|
||||
/>
|
||||
</p>
|
||||
{enrollmentProfileNotFound || !enrollmentProfileData ? (
|
||||
<SetupAssistantProfileUploader
|
||||
currentTeamId={currentTeamId}
|
||||
onUpload={onUpload}
|
||||
/>
|
||||
) : (
|
||||
<SetuAssistantProfileCard
|
||||
profile={enrollmentProfileData}
|
||||
onDelete={() => setShowDeleteProfileModal(true)}
|
||||
/>
|
||||
)}
|
||||
<AdvancedOptionsForm
|
||||
key={String(defaultReleaseDeviceSetting)}
|
||||
currentTeamId={currentTeamId}
|
||||
defaultReleaseDevice={defaultReleaseDeviceSetting}
|
||||
/>
|
||||
</div>
|
||||
<div className={`${baseClass}__preview-container`}>
|
||||
<SetupAssistantPreview />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{showDeleteProfileModal && (
|
||||
<DeleteAutoEnrollmentProfile
|
||||
currentTeamId={currentTeamId}
|
||||
onDelete={onDelete}
|
||||
onCancel={() => setShowDeleteProfileModal(false)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SetupAssistant;
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
.setup-assistant {
|
||||
&__content {
|
||||
max-width: $break-xxl;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: $pad-xxlarge;
|
||||
}
|
||||
|
||||
&__upload-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $pad-large;
|
||||
}
|
||||
|
||||
&__section-description {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@media (max-width: $break-md) {
|
||||
&__content {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
import React, { useContext, useState } from "react";
|
||||
|
||||
import mdmAPI from "services/entities/mdm";
|
||||
|
||||
import TooltipWrapper from "components/TooltipWrapper";
|
||||
import Checkbox from "components/forms/fields/Checkbox";
|
||||
import Button from "components/buttons/Button";
|
||||
import { NotificationContext } from "context/notification";
|
||||
import RevealButton from "components/buttons/RevealButton";
|
||||
|
||||
const baseClass = "advanced-options-form";
|
||||
|
||||
interface IAdvancedOptionsFormProps {
|
||||
currentTeamId: number;
|
||||
defaultReleaseDevice: boolean;
|
||||
}
|
||||
|
||||
const AdvancedOptionsForm = ({
|
||||
currentTeamId,
|
||||
defaultReleaseDevice,
|
||||
}: IAdvancedOptionsFormProps) => {
|
||||
const [showAdvancedOptions, setShowAdvancedOptions] = useState(false);
|
||||
const [releaseDevice, setReleaseDevice] = useState(defaultReleaseDevice);
|
||||
const { renderFlash } = useContext(NotificationContext);
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
|
||||
try {
|
||||
await mdmAPI.updateReleaseDeviceSetting(currentTeamId, releaseDevice);
|
||||
renderFlash("success", "Successfully updated.");
|
||||
} catch {
|
||||
renderFlash("error", "Something went wrong. Please try again.");
|
||||
}
|
||||
};
|
||||
|
||||
const tooltip = (
|
||||
<>
|
||||
When enabled, you're responsible for sending the DeviceConfigured
|
||||
command. (Default: <b>Off</b>)
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
<RevealButton
|
||||
className={`${baseClass}__accordion-title`}
|
||||
isShowing={showAdvancedOptions}
|
||||
showText="Hide advanced options"
|
||||
hideText="Show advanced options"
|
||||
caretPosition="after"
|
||||
onClick={() => setShowAdvancedOptions(!showAdvancedOptions)}
|
||||
/>
|
||||
{showAdvancedOptions && (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<Checkbox
|
||||
value={releaseDevice}
|
||||
onChange={() => setReleaseDevice(!releaseDevice)}
|
||||
>
|
||||
<TooltipWrapper tipContent={tooltip}>
|
||||
Release device manually
|
||||
</TooltipWrapper>
|
||||
</Checkbox>
|
||||
<Button type="submit">Save</Button>
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdvancedOptionsForm;
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
.advanced-options-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $pad-large;
|
||||
|
||||
&__accordion-title {
|
||||
// use this so we dont center the text.
|
||||
justify-content: normal;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from "./AdvancedOptionsForm";
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
import React, { useContext } from "react";
|
||||
|
||||
import mdmAPI from "services/entities/mdm";
|
||||
|
||||
import Modal from "components/Modal";
|
||||
import Button from "components/buttons/Button";
|
||||
import { NotificationContext } from "context/notification";
|
||||
|
||||
interface DeleteAutoEnrollProfileProps {
|
||||
currentTeamId: number;
|
||||
onCancel: () => void;
|
||||
onDelete: () => void;
|
||||
}
|
||||
|
||||
const baseClass = "delete-auto-enrollment-profile-modal";
|
||||
|
||||
const DeleteAutoEnrollProfile = ({
|
||||
currentTeamId,
|
||||
onCancel,
|
||||
onDelete,
|
||||
}: DeleteAutoEnrollProfileProps) => {
|
||||
const { renderFlash } = useContext(NotificationContext);
|
||||
|
||||
const handleDelete = async () => {
|
||||
try {
|
||||
await mdmAPI.deleteSetupEnrollmentProfile(currentTeamId);
|
||||
renderFlash("success", "Successfully deleted!");
|
||||
} catch {
|
||||
renderFlash("error", "Couldn’t delete. Please try again.");
|
||||
}
|
||||
onDelete();
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
className={baseClass}
|
||||
title="Delete automatic enrollment profile"
|
||||
onExit={onCancel}
|
||||
>
|
||||
<>
|
||||
<p>Delete the automatic enrollment profile to upload a new one.</p>
|
||||
<p>
|
||||
Without an automatic enrollment profile, new macOS hosts will
|
||||
automatically enroll with the default setup settings.
|
||||
</p>
|
||||
<div className="modal-cta-wrap">
|
||||
<Button type="button" onClick={handleDelete} variant="alert">
|
||||
Delete
|
||||
</Button>
|
||||
<Button onClick={onCancel} variant="inverse-alert">
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default DeleteAutoEnrollProfile;
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from "./DeleteAutoEnrollmentProfile";
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
import React from "react";
|
||||
|
||||
import Card from "components/Card";
|
||||
|
||||
import OsSetupPreview from "../../../../../../../../assets/images/os-setup-preview.gif";
|
||||
|
||||
const baseClass = "setup-assistant-preview";
|
||||
|
||||
const SetupAssistantPreview = () => {
|
||||
return (
|
||||
<Card
|
||||
color="gray"
|
||||
borderRadiusSize="medium"
|
||||
paddingSize="xxlarge"
|
||||
className={baseClass}
|
||||
>
|
||||
<h2>End user experience</h2>
|
||||
<p>
|
||||
After the end user continues past the <b>Remote Management</b> screen,
|
||||
macOS Setup Assistant displays several screens by default.
|
||||
</p>
|
||||
<p>
|
||||
By adding an automatic enrollment profile you can customize which
|
||||
screens are displayed and more.
|
||||
</p>
|
||||
<img
|
||||
className={`${baseClass}__preview-img`}
|
||||
src={OsSetupPreview}
|
||||
alt="OS setup preview"
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default SetupAssistantPreview;
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
.setup-assistant-preview {
|
||||
font-size: $x-small;
|
||||
|
||||
h2 {
|
||||
margin: 0;
|
||||
font-size: $small;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
&__preview-img {
|
||||
margin-top: $pad-xxlarge;
|
||||
width: 100%;
|
||||
display: block;
|
||||
margin: 40px auto 0;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from "./SetupAssistantPreview";
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
import React from "react";
|
||||
import FileSaver from "file-saver";
|
||||
|
||||
import { uploadedFromNow } from "utilities/date_format";
|
||||
|
||||
import Icon from "components/Icon";
|
||||
import Card from "components/Card";
|
||||
import Graphic from "components/Graphic";
|
||||
import Button from "components/buttons/Button";
|
||||
import { IAppleSetupEnrollmentProfileResponse } from "services/entities/mdm";
|
||||
|
||||
const baseClass = "setup-assistant-profile-card";
|
||||
interface ISetupAssistantProfileCardProps {
|
||||
profile: IAppleSetupEnrollmentProfileResponse;
|
||||
onDelete: () => void;
|
||||
}
|
||||
|
||||
const SetupAssistantProfileCard = ({
|
||||
profile,
|
||||
onDelete,
|
||||
}: ISetupAssistantProfileCardProps) => {
|
||||
const onDownload = () => {
|
||||
const date = new Date();
|
||||
const filename = `${date.getFullYear()}-${date.getMonth()}-${date.getDate()}_${
|
||||
profile.name
|
||||
}`;
|
||||
const file = new global.window.File(
|
||||
[JSON.stringify(profile.enrollment_profile)],
|
||||
filename
|
||||
);
|
||||
|
||||
FileSaver.saveAs(file);
|
||||
};
|
||||
|
||||
return (
|
||||
<Card paddingSize="medium" className={baseClass}>
|
||||
<Graphic name="file-configuration-profile" />
|
||||
<div className={`${baseClass}__info`}>
|
||||
<span className={`${baseClass}__profile-name`}>{profile.name}</span>
|
||||
<span className={`${baseClass}__uploaded-at`}>
|
||||
{uploadedFromNow(profile.uploaded_at)}
|
||||
</span>
|
||||
</div>
|
||||
<div className={`${baseClass}__actions`}>
|
||||
<Button
|
||||
className={`${baseClass}__download-button`}
|
||||
variant="text-icon"
|
||||
onClick={onDownload}
|
||||
>
|
||||
<Icon name="download" />
|
||||
</Button>
|
||||
<Button
|
||||
className={`${baseClass}__delete-button`}
|
||||
variant="text-icon"
|
||||
onClick={onDelete}
|
||||
>
|
||||
<Icon name="trash" color="ui-fleet-black-75" />
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default SetupAssistantProfileCard;
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
.setup-assistant-profile-card {
|
||||
display: flex;
|
||||
gap: $pad-medium;
|
||||
align-items: center;
|
||||
|
||||
// TODO: create reusable list item component and use instead of all these styles.
|
||||
&__info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
&__profile-name {
|
||||
font-size: $x-small;
|
||||
font-weight: $bold;
|
||||
}
|
||||
&__uploaded-at {
|
||||
font-size: $xx-small;
|
||||
}
|
||||
|
||||
&__actions {
|
||||
display: flex;
|
||||
gap: $pad-medium;
|
||||
flex: 1;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
&__download-button, &__delete-button {
|
||||
padding: 11px; // TODO: use a padding value from existing variables. talk to design.
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from "./SetupAssistantProfileCard";
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
import React, { useContext, useState } from "react";
|
||||
import { AxiosResponse } from "axios";
|
||||
|
||||
import { IApiError } from "interfaces/errors";
|
||||
import { NotificationContext } from "context/notification";
|
||||
import mdmAPI from "services/entities/mdm";
|
||||
|
||||
import FileUploader from "components/FileUploader";
|
||||
|
||||
import { getErrorMessage } from "./helpers";
|
||||
|
||||
const baseClass = "setup-assistant-profile-uploader";
|
||||
|
||||
interface ISetupAssistantProfileUploaderProps {
|
||||
currentTeamId: number;
|
||||
onUpload: () => void;
|
||||
}
|
||||
|
||||
const SetupAssistantProfileUploader = ({
|
||||
currentTeamId,
|
||||
onUpload,
|
||||
}: ISetupAssistantProfileUploaderProps) => {
|
||||
const { renderFlash } = useContext(NotificationContext);
|
||||
const [showLoading, setShowLoading] = useState(false);
|
||||
|
||||
const onUploadFile = async (files: FileList | null) => {
|
||||
setShowLoading(true);
|
||||
|
||||
if (!files || files.length === 0) {
|
||||
setShowLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const file = files[0];
|
||||
|
||||
try {
|
||||
await mdmAPI.uploadSetupEnrollmentProfile(file, currentTeamId);
|
||||
renderFlash("success", "Successfully uploaded!");
|
||||
onUpload();
|
||||
} catch (e) {
|
||||
const error = e as AxiosResponse<IApiError>;
|
||||
const errMessage = getErrorMessage(error);
|
||||
renderFlash("error", errMessage);
|
||||
} finally {
|
||||
setShowLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<FileUploader
|
||||
message="Automatic enrollment profile (.json)"
|
||||
graphicName="file-configuration-profile"
|
||||
accept=".json"
|
||||
buttonMessage="Add profile"
|
||||
onFileUpload={onUploadFile}
|
||||
isLoading={showLoading}
|
||||
className={baseClass}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default SetupAssistantProfileUploader;
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
import { getErrorReason } from "interfaces/errors";
|
||||
|
||||
const UPLOAD_ERROR_MESSAGES = {
|
||||
default: {
|
||||
message: "Couldn't upload. Please try again.",
|
||||
},
|
||||
};
|
||||
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export const getErrorMessage = (err: unknown) => {
|
||||
if (typeof err === "string") return err;
|
||||
return getErrorReason(err) || UPLOAD_ERROR_MESSAGES.default.message;
|
||||
};
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from "./SetupAssistantProfileUploader";
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from "./SetupAssistant";
|
||||
|
|
@ -14,6 +14,7 @@ export default {
|
|||
CONTROLS_SETUP_EXPERIENCE: `${URL_PREFIX}/controls/setup-experience`,
|
||||
CONTROLS_END_USER_AUTHENTICATION: `${URL_PREFIX}/controls/setup-experience/end-user-auth`,
|
||||
CONTROLS_BOOTSTRAP_PACKAGE: `${URL_PREFIX}/controls/setup-experience/bootstrap-package`,
|
||||
CONTROLS_SETUP_ASSITANT: `${URL_PREFIX}/controls/setup-experience/setup-assistant`,
|
||||
CONTROLS_SCRIPTS: `${URL_PREFIX}/controls/scripts`,
|
||||
|
||||
DASHBOARD: `${URL_PREFIX}/dashboard`,
|
||||
|
|
|
|||
|
|
@ -57,8 +57,8 @@ export default {
|
|||
},
|
||||
|
||||
/**
|
||||
* updateMDMConfig is a special case of update that is used to update the MDM
|
||||
* config.
|
||||
* updateMDMConfig is a special case of update that is used to update the app
|
||||
* MDM config.
|
||||
*
|
||||
* If the request fails and `skipParseError` is `true`, the caller is
|
||||
* responsible for verifying that the value of the rejected promise is an AxiosError
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import {
|
|||
IMdmProfile,
|
||||
MdmProfileStatus,
|
||||
} from "interfaces/mdm";
|
||||
import { API_NO_TEAM_ID } from "interfaces/team";
|
||||
import sendRequest from "services";
|
||||
import endpoints from "utilities/endpoints";
|
||||
import { buildQueryStringFromParams } from "utilities/url";
|
||||
|
|
@ -46,6 +47,19 @@ export interface IUploadProfileApiParams {
|
|||
labels?: string[];
|
||||
}
|
||||
|
||||
interface IUpdateSetupExperienceBody {
|
||||
team_id?: number;
|
||||
enable_release_device_manually: boolean;
|
||||
}
|
||||
|
||||
export interface IAppleSetupEnrollmentProfileResponse {
|
||||
team_id: number | null;
|
||||
name: string;
|
||||
uploaded_at: string;
|
||||
// enrollment profile is an object with keys found here https://developer.apple.com/documentation/devicemanagement/profile.
|
||||
enrollment_profile: Record<string, any>;
|
||||
}
|
||||
|
||||
const mdmService = {
|
||||
downloadDeviceUserEnrollmentProfile: (token: string) => {
|
||||
const { DEVICE_USER_MDM_ENROLLMENT_PROFILE } = endpoints;
|
||||
|
|
@ -226,6 +240,68 @@ const mdmService = {
|
|||
enable_end_user_authentication: isEnabled,
|
||||
});
|
||||
},
|
||||
|
||||
updateReleaseDeviceSetting: (teamId: number, isEnabled: boolean) => {
|
||||
const { MDM_SETUP_EXPERIENCE } = endpoints;
|
||||
|
||||
const body: IUpdateSetupExperienceBody = {
|
||||
enable_release_device_manually: isEnabled,
|
||||
};
|
||||
|
||||
if (teamId !== API_NO_TEAM_ID) {
|
||||
body.team_id = teamId;
|
||||
}
|
||||
|
||||
return sendRequest("PATCH", MDM_SETUP_EXPERIENCE, body);
|
||||
},
|
||||
getSetupEnrollmentProfile: (teamId?: number) => {
|
||||
const { MDM_APPLE_SETUP_ENROLLMENT_PROFILE } = endpoints;
|
||||
if (!teamId || teamId === API_NO_TEAM_ID) {
|
||||
return sendRequest("GET", MDM_APPLE_SETUP_ENROLLMENT_PROFILE);
|
||||
}
|
||||
|
||||
const path = `${MDM_APPLE_SETUP_ENROLLMENT_PROFILE}?${buildQueryStringFromParams(
|
||||
{ team_id: teamId }
|
||||
)}`;
|
||||
return sendRequest("GET", path);
|
||||
},
|
||||
uploadSetupEnrollmentProfile: (file: File, teamId: number) => {
|
||||
const { MDM_APPLE_SETUP_ENROLLMENT_PROFILE } = endpoints;
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.readAsText(file);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
reader.addEventListener("load", () => {
|
||||
try {
|
||||
const body: Record<string, any> = {
|
||||
name: file.name,
|
||||
enrollment_profile: JSON.parse(reader.result as string),
|
||||
};
|
||||
if (teamId !== API_NO_TEAM_ID) {
|
||||
body.team_id = teamId;
|
||||
}
|
||||
resolve(
|
||||
sendRequest("POST", MDM_APPLE_SETUP_ENROLLMENT_PROFILE, body)
|
||||
);
|
||||
} catch {
|
||||
// catches invalid JSON
|
||||
reject("Couldn’t upload. The file should include valid JSON.");
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
deleteSetupEnrollmentProfile: (teamId: number) => {
|
||||
const { MDM_APPLE_SETUP_ENROLLMENT_PROFILE } = endpoints;
|
||||
if (teamId === API_NO_TEAM_ID) {
|
||||
return sendRequest("DELETE", MDM_APPLE_SETUP_ENROLLMENT_PROFILE);
|
||||
}
|
||||
|
||||
const path = `${MDM_APPLE_SETUP_ENROLLMENT_PROFILE}?${buildQueryStringFromParams(
|
||||
{ team_id: teamId }
|
||||
)}`;
|
||||
return sendRequest("DELETE", path);
|
||||
},
|
||||
};
|
||||
|
||||
export default mdmService;
|
||||
|
|
|
|||
|
|
@ -140,6 +140,16 @@ export default {
|
|||
|
||||
return sendRequest("PATCH", path, requestBody);
|
||||
},
|
||||
|
||||
/**
|
||||
* updates the team config. This can take any partial data that is in the team config.
|
||||
*/
|
||||
updateConfig: (data: any, teamId: number): Promise<ITeamConfig> => {
|
||||
const { TEAMS } = endpoints;
|
||||
const path = `${TEAMS}/${teamId}`;
|
||||
return sendRequest("PATCH", path, data);
|
||||
},
|
||||
|
||||
addUsers: (teamId: number | undefined, newUsers: INewTeamUsersBody) => {
|
||||
if (!teamId || teamId <= API_NO_TEAM_ID) {
|
||||
return Promise.reject(
|
||||
|
|
|
|||
13
frontend/utilities/date_format/date_format.tests.ts
Normal file
13
frontend/utilities/date_format/date_format.tests.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import { uploadedFromNow } from ".";
|
||||
|
||||
describe("date_format", () => {
|
||||
describe("uploadedFromNow util", () => {
|
||||
it("returns an user friendly uploaded at message", () => {
|
||||
const currentDate = new Date();
|
||||
currentDate.setDate(currentDate.getDate() - 2);
|
||||
const twoDaysAgo = currentDate.toISOString();
|
||||
|
||||
expect(uploadedFromNow(twoDaysAgo)).toEqual("Uploaded 2 days ago");
|
||||
});
|
||||
});
|
||||
});
|
||||
6
frontend/utilities/date_format/index.ts
Normal file
6
frontend/utilities/date_format/index.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import { formatDistanceToNow } from "date-fns";
|
||||
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export const uploadedFromNow = (date: string) => {
|
||||
return `Uploaded ${formatDistanceToNow(new Date(date))} ago`;
|
||||
};
|
||||
|
|
@ -79,11 +79,13 @@ export default {
|
|||
}
|
||||
return `/api/mdm/apple/enroll?${query}`;
|
||||
},
|
||||
MDM_APPLE_SETUP_ENROLLMENT_PROFILE: `/${API_VERSION}/fleet/mdm/apple/enrollment_profile`,
|
||||
MDM_BOOTSTRAP_PACKAGE_METADATA: (teamId: number) =>
|
||||
`/${API_VERSION}/fleet/mdm/bootstrap/${teamId}/metadata`,
|
||||
MDM_BOOTSTRAP_PACKAGE: `/${API_VERSION}/fleet/mdm/bootstrap`,
|
||||
MDM_BOOTSTRAP_PACKAGE_SUMMARY: `/${API_VERSION}/fleet/mdm/bootstrap/summary`,
|
||||
MDM_SETUP: `/${API_VERSION}/fleet/mdm/apple/setup`,
|
||||
MDM_SETUP_EXPERIENCE: `/${API_VERSION}/fleet/setup_experience`,
|
||||
MDM_EULA: (token: string) => `/${API_VERSION}/fleet/mdm/setup/eula/${token}`,
|
||||
MDM_EULA_UPLOAD: `/${API_VERSION}/fleet/mdm/setup/eula`,
|
||||
MDM_EULA_METADATA: `/${API_VERSION}/fleet/mdm/setup/eula/metadata`,
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ VALUES (?, ?, ?, ?, ?, COALESCE(?, NOW()))
|
|||
return job, nil
|
||||
}
|
||||
|
||||
func (ds *Datastore) GetQueuedJobs(ctx context.Context, maxNumJobs int) ([]*fleet.Job, error) {
|
||||
func (ds *Datastore) GetQueuedJobs(ctx context.Context, maxNumJobs int, now time.Time) ([]*fleet.Job, error) {
|
||||
query := `
|
||||
SELECT
|
||||
id, created_at, updated_at, name, args, state, retries, error, not_before
|
||||
|
|
@ -43,14 +43,18 @@ FROM
|
|||
jobs
|
||||
WHERE
|
||||
state = ? AND
|
||||
not_before <= NOW()
|
||||
not_before <= ?
|
||||
ORDER BY
|
||||
updated_at ASC
|
||||
LIMIT ?
|
||||
`
|
||||
|
||||
if now.IsZero() {
|
||||
now = time.Now().UTC()
|
||||
}
|
||||
|
||||
var jobs []*fleet.Job
|
||||
err := sqlx.SelectContext(ctx, ds.reader(ctx), &jobs, query, fleet.JobStateQueued, maxNumJobs)
|
||||
err := sqlx.SelectContext(ctx, ds.reader(ctx), &jobs, query, fleet.JobStateQueued, now, maxNumJobs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,9 @@ import (
|
|||
|
||||
func TestJobs(t *testing.T) {
|
||||
ds := CreateMySQLDS(t)
|
||||
// call TruncateTables before the first test, because a DB migation may have
|
||||
// created job entries.
|
||||
TruncateTables(t, ds)
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
|
|
@ -30,7 +33,7 @@ func testQueueAndProcessJobs(t *testing.T, ds *Datastore) {
|
|||
ctx := context.Background()
|
||||
|
||||
// no jobs yet
|
||||
jobs, err := ds.GetQueuedJobs(ctx, 10)
|
||||
jobs, err := ds.GetQueuedJobs(ctx, 10, time.Time{})
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, jobs)
|
||||
|
||||
|
|
@ -45,7 +48,7 @@ func testQueueAndProcessJobs(t *testing.T, ds *Datastore) {
|
|||
require.NotZero(t, j2.ID)
|
||||
|
||||
// only j1 is returned
|
||||
jobs, err = ds.GetQueuedJobs(ctx, 10)
|
||||
jobs, err = ds.GetQueuedJobs(ctx, 10, time.Time{})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, jobs, 1)
|
||||
require.Equal(t, j1.ID, jobs[0].ID)
|
||||
|
|
@ -58,7 +61,7 @@ func testQueueAndProcessJobs(t *testing.T, ds *Datastore) {
|
|||
require.NoError(t, err)
|
||||
|
||||
// no jobs queued for now
|
||||
jobs, err = ds.GetQueuedJobs(ctx, 10)
|
||||
jobs, err = ds.GetQueuedJobs(ctx, 10, time.Time{})
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, jobs)
|
||||
|
||||
|
|
@ -68,7 +71,7 @@ func testQueueAndProcessJobs(t *testing.T, ds *Datastore) {
|
|||
require.NoError(t, err)
|
||||
|
||||
// j2 is returned
|
||||
jobs, err = ds.GetQueuedJobs(ctx, 10)
|
||||
jobs, err = ds.GetQueuedJobs(ctx, 10, time.Time{})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, jobs, 1)
|
||||
require.Equal(t, j2.ID, jobs[0].ID)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,64 @@
|
|||
package tables
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
func init() {
|
||||
MigrationClient.AddMigration(Up_20240320145650, Down_20240320145650)
|
||||
}
|
||||
|
||||
func Up_20240320145650(tx *sql.Tx) error {
|
||||
// This migration is to re-generate and re-register with Apple the DEP
|
||||
// enrollment profile(s) so that await_device_configured is set to true.
|
||||
// We do this by doing the equivalent of:
|
||||
//
|
||||
// worker.QueueMacosSetupAssistantJob(ctx, ds, logger,
|
||||
// worker.MacosSetupAssistantUpdateAllProfiles, nil)
|
||||
//
|
||||
// but without calling that function, in case the code changes in the future,
|
||||
// breaking this migration. Instead we insert directly the job in the
|
||||
// database, and the worker will process it shortly after Fleet restarts.
|
||||
|
||||
const (
|
||||
jobName = "macos_setup_assistant"
|
||||
taskName = "update_all_profiles"
|
||||
jobStateQueued = "queued"
|
||||
)
|
||||
|
||||
type macosSetupAssistantArgs struct {
|
||||
Task string `json:"task"`
|
||||
TeamID *uint `json:"team_id,omitempty"`
|
||||
HostSerialNumbers []string `json:"host_serial_numbers,omitempty"`
|
||||
}
|
||||
argsJSON, err := json.Marshal(macosSetupAssistantArgs{Task: taskName})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to JSON marshal the job arguments: %w", err)
|
||||
}
|
||||
|
||||
// hard-coded timestamps are used so that schema.sql is stable
|
||||
const query = `
|
||||
INSERT INTO jobs (
|
||||
name,
|
||||
args,
|
||||
state,
|
||||
error,
|
||||
not_before,
|
||||
created_at,
|
||||
updated_at
|
||||
)
|
||||
VALUES (?, ?, ?, '', ?, ?, ?)
|
||||
`
|
||||
ts := time.Date(2024, 3, 20, 0, 0, 0, 0, time.UTC)
|
||||
if _, err := tx.Exec(query, jobName, argsJSON, jobStateQueued, ts, ts, ts); err != nil {
|
||||
return fmt.Errorf("failed to insert worker job: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func Down_20240320145650(tx *sql.Tx) error {
|
||||
return nil
|
||||
}
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
package tables
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestUp_20240320145650(t *testing.T) {
|
||||
db := applyUpToPrev(t)
|
||||
|
||||
type macosSetupAssistantArgs struct {
|
||||
Task string `json:"task"`
|
||||
TeamID *uint `json:"team_id,omitempty"`
|
||||
HostSerialNumbers []string `json:"host_serial_numbers,omitempty"`
|
||||
}
|
||||
|
||||
type job struct {
|
||||
ID uint `json:"id" db:"id"`
|
||||
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||
UpdatedAt *time.Time `json:"updated_at" db:"updated_at"`
|
||||
Name string `json:"name" db:"name"`
|
||||
Args *json.RawMessage `json:"args" db:"args"`
|
||||
State string `json:"state" db:"state"`
|
||||
Retries int `json:"retries" db:"retries"`
|
||||
Error string `json:"error" db:"error"`
|
||||
NotBefore time.Time `json:"not_before" db:"not_before"`
|
||||
}
|
||||
|
||||
var jobs []*job
|
||||
err := db.Select(&jobs, `SELECT id, name, args, state, retries, error, not_before FROM jobs`)
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, jobs)
|
||||
|
||||
applyNext(t, db)
|
||||
|
||||
err = db.Select(&jobs, `SELECT id, name, args, state, retries, error, not_before FROM jobs`)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, jobs, 1)
|
||||
|
||||
require.Equal(t, "macos_setup_assistant", jobs[0].Name)
|
||||
require.Equal(t, 0, jobs[0].Retries)
|
||||
require.LessOrEqual(t, jobs[0].NotBefore, time.Now().UTC())
|
||||
require.NotNil(t, jobs[0].Args)
|
||||
|
||||
var args macosSetupAssistantArgs
|
||||
err = json.Unmarshal(*jobs[0].Args, &args)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "update_all_profiles", args.Task)
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -613,8 +613,9 @@ func testTeamsMDMConfig(t *testing.T, ds *Datastore) {
|
|||
GracePeriodDays: optjson.SetInt(3),
|
||||
},
|
||||
MacOSSetup: fleet.MacOSSetup{
|
||||
BootstrapPackage: optjson.SetString("bootstrap"),
|
||||
MacOSSetupAssistant: optjson.SetString("assistant"),
|
||||
BootstrapPackage: optjson.SetString("bootstrap"),
|
||||
MacOSSetupAssistant: optjson.SetString("assistant"),
|
||||
EnableReleaseDeviceManually: optjson.SetBool(false),
|
||||
},
|
||||
WindowsSettings: fleet.WindowsSettings{
|
||||
CustomSettings: optjson.SetSlice([]fleet.MDMProfileSpec{{Path: "foo"}, {Path: "bar"}}),
|
||||
|
|
|
|||
|
|
@ -381,6 +381,7 @@ type MacOSSetup struct {
|
|||
BootstrapPackage optjson.String `json:"bootstrap_package"`
|
||||
EnableEndUserAuthentication bool `json:"enable_end_user_authentication"`
|
||||
MacOSSetupAssistant optjson.String `json:"macos_setup_assistant"`
|
||||
EnableReleaseDeviceManually optjson.Bool `json:"enable_release_device_manually"`
|
||||
}
|
||||
|
||||
// MacOSMigration contains settings related to the MDM migration work flow.
|
||||
|
|
@ -819,6 +820,9 @@ func (c AppConfig) MarshalJSON() ([]byte, error) {
|
|||
if !c.MDM.EnableDiskEncryption.Valid {
|
||||
c.MDM.EnableDiskEncryption = optjson.SetBool(false)
|
||||
}
|
||||
if !c.MDM.MacOSSetup.EnableReleaseDeviceManually.Valid {
|
||||
c.MDM.MacOSSetup.EnableReleaseDeviceManually = optjson.SetBool(false)
|
||||
}
|
||||
|
||||
type aliasConfig AppConfig
|
||||
aa := aliasConfig(c)
|
||||
|
|
|
|||
|
|
@ -416,6 +416,7 @@ func (p MDMAppleSettingsPayload) AuthzType() string {
|
|||
type MDMAppleSetupPayload struct {
|
||||
TeamID *uint `json:"team_id"`
|
||||
EnableEndUserAuthentication *bool `json:"enable_end_user_authentication"`
|
||||
EnableReleaseDeviceManually *bool `json:"enable_release_device_manually"`
|
||||
}
|
||||
|
||||
// AuthzType implements authz.AuthzTyper.
|
||||
|
|
|
|||
|
|
@ -860,8 +860,9 @@ type Datastore interface {
|
|||
// NewJob inserts a new job into the jobs table (queue).
|
||||
NewJob(ctx context.Context, job *Job) (*Job, error)
|
||||
|
||||
// GetQueuedJobs gets queued jobs from the jobs table (queue).
|
||||
GetQueuedJobs(ctx context.Context, maxNumJobs int) ([]*Job, error)
|
||||
// GetQueuedJobs gets queued jobs from the jobs table (queue) ready to be
|
||||
// processed. If now is the zero time, the current time will be used.
|
||||
GetQueuedJobs(ctx context.Context, maxNumJobs int, now time.Time) ([]*Job, error)
|
||||
|
||||
// UpdateJobs updates an existing job. Call this after processing a job.
|
||||
UpdateJob(ctx context.Context, id uint, job *Job) (*Job, error)
|
||||
|
|
|
|||
|
|
@ -120,6 +120,9 @@ func (t *Team) UnmarshalJSON(b []byte) error {
|
|||
return err
|
||||
}
|
||||
|
||||
if !x.MDM.MacOSSetup.EnableReleaseDeviceManually.Valid {
|
||||
x.MDM.MacOSSetup.EnableReleaseDeviceManually = optjson.SetBool(false)
|
||||
}
|
||||
*t = Team{
|
||||
ID: x.ID,
|
||||
CreatedAt: x.CreatedAt,
|
||||
|
|
@ -241,6 +244,10 @@ func (t *TeamConfig) Scan(val interface{}) error {
|
|||
|
||||
// Value implements the sql.Valuer interface
|
||||
func (t TeamConfig) Value() (driver.Value, error) {
|
||||
// force-save as the default `false` value if not set
|
||||
if !t.MDM.MacOSSetup.EnableReleaseDeviceManually.Valid {
|
||||
t.MDM.MacOSSetup.EnableReleaseDeviceManually = optjson.SetBool(false)
|
||||
}
|
||||
return json.Marshal(t)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -91,17 +91,16 @@ type DEPService struct {
|
|||
// getDefaultProfile returns a godep.Profile with default values set.
|
||||
func (d *DEPService) getDefaultProfile() *godep.Profile {
|
||||
return &godep.Profile{
|
||||
ProfileName: "FleetDM default enrollment profile",
|
||||
AllowPairing: true,
|
||||
AutoAdvanceSetup: false,
|
||||
AwaitDeviceConfigured: false,
|
||||
IsSupervised: false,
|
||||
IsMultiUser: false,
|
||||
IsMandatory: false,
|
||||
IsMDMRemovable: true,
|
||||
Language: "en",
|
||||
OrgMagic: "1",
|
||||
Region: "US",
|
||||
ProfileName: "FleetDM default enrollment profile",
|
||||
AllowPairing: true,
|
||||
AutoAdvanceSetup: false,
|
||||
IsSupervised: false,
|
||||
IsMultiUser: false,
|
||||
IsMandatory: false,
|
||||
IsMDMRemovable: true,
|
||||
Language: "en",
|
||||
OrgMagic: "1",
|
||||
Region: "US",
|
||||
SkipSetupItems: []string{
|
||||
"Accessibility",
|
||||
"Appearance",
|
||||
|
|
@ -207,6 +206,10 @@ func (d *DEPService) RegisterProfileWithAppleDEPServer(ctx context.Context, team
|
|||
// ensure `url` is the same as `configuration_web_url`, to not leak the URL
|
||||
// to get a token without SSO enabled
|
||||
jsonProf.URL = jsonProf.ConfigurationWebURL
|
||||
// always set await_device_configured to true - it will be released either
|
||||
// automatically by Fleet or manually by the user if
|
||||
// enable_release_device_manually is true.
|
||||
jsonProf.AwaitDeviceConfigured = true
|
||||
|
||||
depClient := NewDEPClient(d.depStorage, d.ds, d.logger)
|
||||
res, err := depClient.DefineProfile(ctx, DEPName, &jsonProf)
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ func TestDEPService(t *testing.T) {
|
|||
require.Contains(t, got.ConfigurationWebURL, serverURL+"api/mdm/apple/enroll?token=")
|
||||
got.URL = ""
|
||||
got.ConfigurationWebURL = ""
|
||||
defaultProfile.AwaitDeviceConfigured = true // this is now always set to true
|
||||
require.Equal(t, defaultProfile, &got)
|
||||
default:
|
||||
require.Fail(t, "unexpected path: %s", r.URL.Path)
|
||||
|
|
|
|||
|
|
@ -226,6 +226,24 @@ func (svc *MDMAppleCommander) AccountConfiguration(ctx context.Context, hostUUID
|
|||
return svc.EnqueueCommand(ctx, hostUUIDs, raw)
|
||||
}
|
||||
|
||||
func (svc *MDMAppleCommander) DeviceConfigured(ctx context.Context, hostUUID, cmdUUID string) error {
|
||||
raw := fmt.Sprintf(`<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>Command</key>
|
||||
<dict>
|
||||
<key>RequestType</key>
|
||||
<string>DeviceConfigured</string>
|
||||
</dict>
|
||||
<key>CommandUUID</key>
|
||||
<string>%s</string>
|
||||
</dict>
|
||||
</plist>`, cmdUUID)
|
||||
|
||||
return svc.EnqueueCommand(ctx, []string{hostUUID}, raw)
|
||||
}
|
||||
|
||||
// EnqueueCommand takes care of enqueuing the commands and sending push
|
||||
// notifications to the devices.
|
||||
//
|
||||
|
|
|
|||
|
|
@ -8,12 +8,14 @@ import (
|
|||
// Profile corresponds to the Apple DEP API "Profile" structure.
|
||||
// See https://developer.apple.com/documentation/devicemanagement/profile
|
||||
type Profile struct {
|
||||
ProfileName string `json:"profile_name"`
|
||||
URL string `json:"url"`
|
||||
AllowPairing bool `json:"allow_pairing,omitempty"`
|
||||
IsSupervised bool `json:"is_supervised,omitempty"`
|
||||
IsMultiUser bool `json:"is_multi_user,omitempty"`
|
||||
IsMandatory bool `json:"is_mandatory,omitempty"`
|
||||
ProfileName string `json:"profile_name"`
|
||||
URL string `json:"url"`
|
||||
AllowPairing bool `json:"allow_pairing,omitempty"`
|
||||
IsSupervised bool `json:"is_supervised,omitempty"`
|
||||
IsMultiUser bool `json:"is_multi_user,omitempty"`
|
||||
IsMandatory bool `json:"is_mandatory,omitempty"`
|
||||
// AwaitDeviceConfigured should never be set in the profiles we store in the
|
||||
// database - it is now always forced to true when registering with Apple.
|
||||
AwaitDeviceConfigured bool `json:"await_device_configured,omitempty"`
|
||||
IsMDMRemovable bool `json:"is_mdm_removable"` // default true
|
||||
SupportPhoneNumber string `json:"support_phone_number,omitempty"`
|
||||
|
|
|
|||
|
|
@ -602,7 +602,7 @@ type SerialUpdateHostFunc func(ctx context.Context, host *fleet.Host) error
|
|||
|
||||
type NewJobFunc func(ctx context.Context, job *fleet.Job) (*fleet.Job, error)
|
||||
|
||||
type GetQueuedJobsFunc func(ctx context.Context, maxNumJobs int) ([]*fleet.Job, error)
|
||||
type GetQueuedJobsFunc func(ctx context.Context, maxNumJobs int, now time.Time) ([]*fleet.Job, error)
|
||||
|
||||
type UpdateJobFunc func(ctx context.Context, id uint, job *fleet.Job) (*fleet.Job, error)
|
||||
|
||||
|
|
@ -4221,11 +4221,11 @@ func (s *DataStore) NewJob(ctx context.Context, job *fleet.Job) (*fleet.Job, err
|
|||
return s.NewJobFunc(ctx, job)
|
||||
}
|
||||
|
||||
func (s *DataStore) GetQueuedJobs(ctx context.Context, maxNumJobs int) ([]*fleet.Job, error) {
|
||||
func (s *DataStore) GetQueuedJobs(ctx context.Context, maxNumJobs int, now time.Time) ([]*fleet.Job, error) {
|
||||
s.mu.Lock()
|
||||
s.GetQueuedJobsFuncInvoked = true
|
||||
s.mu.Unlock()
|
||||
return s.GetQueuedJobsFunc(ctx, maxNumJobs)
|
||||
return s.GetQueuedJobsFunc(ctx, maxNumJobs, now)
|
||||
}
|
||||
|
||||
func (s *DataStore) UpdateJob(ctx context.Context, id uint, job *fleet.Job) (*fleet.Job, error) {
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import (
|
|||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/fleetdm/fleet/v4/pkg/optjson"
|
||||
"github.com/fleetdm/fleet/v4/pkg/rawjson"
|
||||
"github.com/fleetdm/fleet/v4/server/authz"
|
||||
authz_ctx "github.com/fleetdm/fleet/v4/server/contexts/authz"
|
||||
|
|
@ -343,6 +344,19 @@ func (svc *Service) ModifyAppConfig(ctx context.Context, p []byte, applyOpts fle
|
|||
} else if appConfig.MDM.EnableDiskEncryption.Set && !appConfig.MDM.EnableDiskEncryption.Valid {
|
||||
appConfig.MDM.EnableDiskEncryption = oldAppConfig.MDM.EnableDiskEncryption
|
||||
}
|
||||
// this is to handle the case where `enable_release_device_manually: null` is
|
||||
// passed in the request payload, which should be treated as "not present/not
|
||||
// changed" by the PATCH. We should really try to find a more general way to
|
||||
// handle this.
|
||||
if !oldAppConfig.MDM.MacOSSetup.EnableReleaseDeviceManually.Valid {
|
||||
// this makes a DB migration unnecessary, will update the field to its default false value as necessary
|
||||
oldAppConfig.MDM.MacOSSetup.EnableReleaseDeviceManually = optjson.SetBool(false)
|
||||
}
|
||||
if newAppConfig.MDM.MacOSSetup.EnableReleaseDeviceManually.Valid {
|
||||
appConfig.MDM.MacOSSetup.EnableReleaseDeviceManually = newAppConfig.MDM.MacOSSetup.EnableReleaseDeviceManually
|
||||
} else {
|
||||
appConfig.MDM.MacOSSetup.EnableReleaseDeviceManually = oldAppConfig.MDM.MacOSSetup.EnableReleaseDeviceManually
|
||||
}
|
||||
|
||||
var legacyUsedWarning error
|
||||
if legacyKeys := appConfig.DidUnmarshalLegacySettings(); len(legacyKeys) > 0 {
|
||||
|
|
@ -679,6 +693,9 @@ func (svc *Service) validateMDM(
|
|||
if mdm.MacOSSetup.MacOSSetupAssistant.Value != "" && oldMdm.MacOSSetup.MacOSSetupAssistant.Value != mdm.MacOSSetup.MacOSSetupAssistant.Value && !license.IsPremium() {
|
||||
invalid.Append("macos_setup.macos_setup_assistant", ErrMissingLicense.Error())
|
||||
}
|
||||
if mdm.MacOSSetup.EnableReleaseDeviceManually.Value && oldMdm.MacOSSetup.EnableReleaseDeviceManually.Value != mdm.MacOSSetup.EnableReleaseDeviceManually.Value && !license.IsPremium() {
|
||||
invalid.Append("macos_setup.enable_release_device_manually", ErrMissingLicense.Error())
|
||||
}
|
||||
if mdm.MacOSSetup.BootstrapPackage.Value != "" && oldMdm.MacOSSetup.BootstrapPackage.Value != mdm.MacOSSetup.BootstrapPackage.Value && !license.IsPremium() {
|
||||
invalid.Append("macos_setup.bootstrap_package", ErrMissingLicense.Error())
|
||||
}
|
||||
|
|
@ -699,6 +716,11 @@ func (svc *Service) validateMDM(
|
|||
`Couldn't update macos_setup because MDM features aren't turned on in Fleet. Use fleetctl generate mdm-apple and then fleet serve with mdm configuration to turn on MDM features.`)
|
||||
}
|
||||
|
||||
if mdm.MacOSSetup.EnableReleaseDeviceManually.Value && oldMdm.MacOSSetup.EnableReleaseDeviceManually.Value != mdm.MacOSSetup.EnableReleaseDeviceManually.Value {
|
||||
invalid.Append("macos_setup.enable_release_device_manually",
|
||||
`Couldn't update macos_setup because MDM features aren't turned on in Fleet. Use fleetctl generate mdm-apple and then fleet serve with mdm configuration to turn on MDM features.`)
|
||||
}
|
||||
|
||||
if mdm.MacOSSetup.BootstrapPackage.Value != "" && oldMdm.MacOSSetup.BootstrapPackage.Value != mdm.MacOSSetup.BootstrapPackage.Value {
|
||||
invalid.Append("macos_setup.bootstrap_package",
|
||||
`Couldn't update macos_setup because MDM features aren't turned on in Fleet. Use fleetctl generate mdm-apple and then fleet serve with mdm configuration to turn on MDM features.`)
|
||||
|
|
|
|||
|
|
@ -815,7 +815,7 @@ func TestMDMAppleConfig(t *testing.T) {
|
|||
name: "nochange",
|
||||
licenseTier: "free",
|
||||
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}, EnableReleaseDeviceManually: optjson.SetBool(false)},
|
||||
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}},
|
||||
WindowsSettings: fleet.WindowsSettings{
|
||||
|
|
@ -845,7 +845,7 @@ func TestMDMAppleConfig(t *testing.T) {
|
|||
newMDM: fleet.MDM{AppleBMDefaultTeam: "foobar"},
|
||||
expectedMDM: fleet.MDM{
|
||||
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}, EnableReleaseDeviceManually: optjson.SetBool(false)},
|
||||
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}},
|
||||
WindowsSettings: fleet.WindowsSettings{
|
||||
|
|
@ -860,7 +860,7 @@ func TestMDMAppleConfig(t *testing.T) {
|
|||
newMDM: fleet.MDM{AppleBMDefaultTeam: "foobar"},
|
||||
expectedMDM: fleet.MDM{
|
||||
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}, EnableReleaseDeviceManually: optjson.SetBool(false)},
|
||||
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}},
|
||||
WindowsSettings: fleet.WindowsSettings{
|
||||
|
|
@ -881,7 +881,7 @@ func TestMDMAppleConfig(t *testing.T) {
|
|||
oldMDM: fleet.MDM{EndUserAuthentication: fleet.MDMEndUserAuthentication{SSOProviderSettings: fleet.SSOProviderSettings{EntityID: "foo"}}},
|
||||
expectedMDM: fleet.MDM{
|
||||
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}, EnableReleaseDeviceManually: optjson.SetBool(false)},
|
||||
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}},
|
||||
WindowsSettings: fleet.WindowsSettings{
|
||||
|
|
@ -905,7 +905,7 @@ func TestMDMAppleConfig(t *testing.T) {
|
|||
MetadataURL: "http://isser.metadata.com",
|
||||
IDPName: "onelogin",
|
||||
}},
|
||||
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}, EnableReleaseDeviceManually: optjson.SetBool(false)},
|
||||
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}},
|
||||
WindowsSettings: fleet.WindowsSettings{
|
||||
|
|
@ -963,7 +963,7 @@ func TestMDMAppleConfig(t *testing.T) {
|
|||
},
|
||||
expectedMDM: fleet.MDM{
|
||||
EnableDiskEncryption: optjson.Bool{Set: true, Valid: true, Value: false},
|
||||
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}, EnableReleaseDeviceManually: optjson.SetBool(false)},
|
||||
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}},
|
||||
WindowsSettings: fleet.WindowsSettings{
|
||||
|
|
|
|||
|
|
@ -5601,6 +5601,19 @@ func (s *integrationTestSuite) TestPremiumEndpointsWithoutLicense() {
|
|||
s.Do("POST", "/api/v1/fleet/hosts/123/lock", nil, http.StatusPaymentRequired)
|
||||
s.Do("POST", "/api/v1/fleet/hosts/123/unlock", nil, http.StatusPaymentRequired)
|
||||
s.Do("POST", "/api/v1/fleet/hosts/123/wipe", nil, http.StatusPaymentRequired)
|
||||
|
||||
// try to update the enable_release_device_manually setting, requires premium
|
||||
// (but /setup_experience catches the error of the MDM middleware check, so not
|
||||
// StatusPaymentRequired)
|
||||
res = s.Do("PATCH", "/api/v1/fleet/setup_experience", fleet.MDMAppleSetupPayload{EnableReleaseDeviceManually: ptr.Bool(true)}, http.StatusBadRequest)
|
||||
errMsg = extractServerErrorText(res.Body)
|
||||
require.Contains(t, errMsg, fleet.ErrMDMNotConfigured.Error())
|
||||
|
||||
res = s.Do("PATCH", "/api/v1/fleet/config", json.RawMessage(`{
|
||||
"mdm": { "macos_setup": { "enable_release_device_manually": true } }
|
||||
}`), http.StatusUnprocessableEntity)
|
||||
errMsg = extractServerErrorText(res.Body)
|
||||
require.Contains(t, errMsg, "missing or invalid license")
|
||||
}
|
||||
|
||||
func (s *integrationTestSuite) TestScriptsEndpointsWithoutLicense() {
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ import (
|
|||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/fleetdm/fleet/v4/server/pubsub"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
|
|
@ -20,6 +19,8 @@ import (
|
|||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/fleetdm/fleet/v4/server/pubsub"
|
||||
|
||||
"github.com/fleetdm/fleet/v4/pkg/optjson"
|
||||
"github.com/fleetdm/fleet/v4/server/datastore/mysql"
|
||||
"github.com/fleetdm/fleet/v4/server/datastore/redis/redistest"
|
||||
|
|
@ -168,8 +169,9 @@ func (s *integrationEnterpriseTestSuite) TestTeamSpecs() {
|
|||
// 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
|
||||
// null).
|
||||
MacOSSetupAssistant: optjson.String{Set: true},
|
||||
BootstrapPackage: optjson.String{Set: true},
|
||||
MacOSSetupAssistant: optjson.String{Set: true},
|
||||
BootstrapPackage: optjson.String{Set: true},
|
||||
EnableReleaseDeviceManually: optjson.SetBool(false),
|
||||
},
|
||||
// because the WindowsSettings was marshalled to JSON to be saved in the DB,
|
||||
// it did get marshalled, and then when unmarshalled it was set (but
|
||||
|
|
@ -261,8 +263,9 @@ func (s *integrationEnterpriseTestSuite) TestTeamSpecs() {
|
|||
GracePeriodDays: optjson.SetInt(1),
|
||||
},
|
||||
MacOSSetup: fleet.MacOSSetup{
|
||||
MacOSSetupAssistant: optjson.String{Set: true},
|
||||
BootstrapPackage: optjson.String{Set: true},
|
||||
MacOSSetupAssistant: optjson.String{Set: true},
|
||||
BootstrapPackage: optjson.String{Set: true},
|
||||
EnableReleaseDeviceManually: optjson.SetBool(false),
|
||||
},
|
||||
WindowsSettings: fleet.WindowsSettings{
|
||||
CustomSettings: optjson.Slice[fleet.MDMProfileSpec]{Set: true, Value: []fleet.MDMProfileSpec{}},
|
||||
|
|
@ -282,8 +285,9 @@ func (s *integrationEnterpriseTestSuite) TestTeamSpecs() {
|
|||
GracePeriodDays: optjson.SetInt(1),
|
||||
},
|
||||
MacOSSetup: fleet.MacOSSetup{
|
||||
MacOSSetupAssistant: optjson.String{Set: true},
|
||||
BootstrapPackage: optjson.String{Set: true},
|
||||
MacOSSetupAssistant: optjson.String{Set: true},
|
||||
BootstrapPackage: optjson.String{Set: true},
|
||||
EnableReleaseDeviceManually: optjson.SetBool(false),
|
||||
},
|
||||
WindowsSettings: fleet.WindowsSettings{
|
||||
CustomSettings: optjson.Slice[fleet.MDMProfileSpec]{Set: true, Value: []fleet.MDMProfileSpec{}},
|
||||
|
|
@ -305,8 +309,9 @@ func (s *integrationEnterpriseTestSuite) TestTeamSpecs() {
|
|||
GracePeriodDays: optjson.SetInt(1),
|
||||
},
|
||||
MacOSSetup: fleet.MacOSSetup{
|
||||
MacOSSetupAssistant: optjson.String{Set: true},
|
||||
BootstrapPackage: optjson.String{Set: true},
|
||||
MacOSSetupAssistant: optjson.String{Set: true},
|
||||
BootstrapPackage: optjson.String{Set: true},
|
||||
EnableReleaseDeviceManually: optjson.SetBool(false),
|
||||
},
|
||||
WindowsSettings: fleet.WindowsSettings{
|
||||
CustomSettings: optjson.Slice[fleet.MDMProfileSpec]{Set: true, Value: []fleet.MDMProfileSpec{}},
|
||||
|
|
@ -395,6 +400,40 @@ func (s *integrationEnterpriseTestSuite) TestTeamSpecs() {
|
|||
errMsg = extractServerErrorText(res.Body)
|
||||
require.Contains(t, errMsg, "Couldn't update macos_settings because MDM features aren't turned on in Fleet.")
|
||||
|
||||
// dry-run with macos enable release device set to false, no error
|
||||
teamSpecs = map[string]any{
|
||||
"specs": []any{
|
||||
map[string]any{
|
||||
"name": teamName,
|
||||
"mdm": map[string]any{
|
||||
"macos_setup": map[string]any{
|
||||
"enable_release_device_manually": false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
applyResp = applyTeamSpecsResponse{}
|
||||
s.DoJSON("POST", "/api/latest/fleet/spec/teams", teamSpecs, http.StatusOK, &applyResp, "dry_run", "true")
|
||||
assert.Equal(t, map[string]uint{teamName: team.ID}, applyResp.TeamIDsByName)
|
||||
|
||||
// dry-run with macos enable release device manually set to true
|
||||
teamSpecs = map[string]any{
|
||||
"specs": []any{
|
||||
map[string]any{
|
||||
"name": teamName,
|
||||
"mdm": map[string]any{
|
||||
"macos_setup": map[string]any{
|
||||
"enable_release_device_manually": true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
res = s.Do("POST", "/api/latest/fleet/spec/teams", teamSpecs, http.StatusUnprocessableEntity, "dry_run", "true")
|
||||
errMsg = extractServerErrorText(res.Body)
|
||||
require.Contains(t, errMsg, "Couldn't update macos_setup because MDM features aren't turned on in Fleet.")
|
||||
|
||||
// dry-run with invalid host_expiry_settings.host_expiry_window
|
||||
teamSpecs = map[string]any{
|
||||
"specs": []map[string]any{
|
||||
|
|
@ -1986,8 +2025,9 @@ func (s *integrationEnterpriseTestSuite) TestWindowsUpdatesTeamConfig() {
|
|||
GracePeriodDays: optjson.SetInt(2),
|
||||
},
|
||||
MacOSSetup: fleet.MacOSSetup{
|
||||
MacOSSetupAssistant: optjson.String{Set: true},
|
||||
BootstrapPackage: optjson.String{Set: true},
|
||||
MacOSSetupAssistant: optjson.String{Set: true},
|
||||
BootstrapPackage: optjson.String{Set: true},
|
||||
EnableReleaseDeviceManually: optjson.SetBool(false),
|
||||
},
|
||||
WindowsSettings: fleet.WindowsSettings{
|
||||
CustomSettings: optjson.Slice[fleet.MDMProfileSpec]{Set: true, Value: []fleet.MDMProfileSpec{}},
|
||||
|
|
@ -3597,6 +3637,17 @@ func (s *integrationEnterpriseTestSuite) TestMDMNotConfiguredEndpoints() {
|
|||
var reqCSRResp requestMDMAppleCSRResponse
|
||||
s.DoJSON("POST", "/api/latest/fleet/mdm/apple/request_csr", requestMDMAppleCSRRequest{EmailAddress: "a@b.c", Organization: "test"}, http.StatusOK, &reqCSRResp)
|
||||
s.Do("POST", "/api/latest/fleet/mdm/apple/dep/key_pair", nil, http.StatusOK)
|
||||
|
||||
// setting enable release device manually requires MDM
|
||||
res := s.Do("PATCH", "/api/v1/fleet/setup_experience", fleet.MDMAppleSetupPayload{EnableReleaseDeviceManually: ptr.Bool(true)}, http.StatusBadRequest)
|
||||
errMsg := extractServerErrorText(res.Body)
|
||||
require.Contains(t, errMsg, fleet.ErrMDMNotConfigured.Error())
|
||||
|
||||
res = s.Do("PATCH", "/api/v1/fleet/config", json.RawMessage(`{
|
||||
"mdm": { "macos_setup": { "enable_release_device_manually": true } }
|
||||
}`), http.StatusUnprocessableEntity)
|
||||
errMsg = extractServerErrorText(res.Body)
|
||||
require.Contains(t, errMsg, `Couldn't update macos_setup because MDM features aren't turned on in Fleet.`)
|
||||
}
|
||||
|
||||
func (s *integrationEnterpriseTestSuite) TestGlobalPolicyCreateReadPatch() {
|
||||
|
|
|
|||
1083
server/service/integration_mdm_dep_test.go
Normal file
1083
server/service/integration_mdm_dep_test.go
Normal file
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -4,6 +4,8 @@ import (
|
|||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
|
||||
"github.com/fleetdm/fleet/v4/server/fleet"
|
||||
|
|
@ -25,6 +27,7 @@ type AppleMDMTask string
|
|||
const (
|
||||
AppleMDMPostDEPEnrollmentTask AppleMDMTask = "post_dep_enrollment"
|
||||
AppleMDMPostManualEnrollmentTask AppleMDMTask = "post_manual_enrollment"
|
||||
AppleMDMPostDEPReleaseDeviceTask AppleMDMTask = "post_dep_release_device"
|
||||
)
|
||||
|
||||
// AppleMDM is the job processor for the apple_mdm job.
|
||||
|
|
@ -41,10 +44,11 @@ func (a *AppleMDM) Name() string {
|
|||
|
||||
// appleMDMArgs is the payload for the Apple MDM job.
|
||||
type appleMDMArgs struct {
|
||||
Task AppleMDMTask `json:"task"`
|
||||
HostUUID string `json:"host_uuid"`
|
||||
TeamID *uint `json:"team_id,omitempty"`
|
||||
EnrollReference string `json:"enroll_reference,omitempty"`
|
||||
Task AppleMDMTask `json:"task"`
|
||||
HostUUID string `json:"host_uuid"`
|
||||
TeamID *uint `json:"team_id,omitempty"`
|
||||
EnrollReference string `json:"enroll_reference,omitempty"`
|
||||
EnrollmentCommands []string `json:"enrollment_commands,omitempty"`
|
||||
}
|
||||
|
||||
// Run executes the apple_mdm job.
|
||||
|
|
@ -64,16 +68,22 @@ func (a *AppleMDM) Run(ctx context.Context, argsJSON json.RawMessage) error {
|
|||
case AppleMDMPostDEPEnrollmentTask:
|
||||
err := a.runPostDEPEnrollment(ctx, args)
|
||||
return ctxerr.Wrap(ctx, err, "running post Apple DEP enrollment task")
|
||||
|
||||
case AppleMDMPostManualEnrollmentTask:
|
||||
err := a.runPostManualEnrollment(ctx, args)
|
||||
return ctxerr.Wrap(ctx, err, "running post Apple manual enrollment task")
|
||||
|
||||
case AppleMDMPostDEPReleaseDeviceTask:
|
||||
err := a.runPostDEPReleaseDevice(ctx, args)
|
||||
return ctxerr.Wrap(ctx, err, "running post Apple DEP release device task")
|
||||
|
||||
default:
|
||||
return ctxerr.Errorf(ctx, "unknown task: %v", args.Task)
|
||||
}
|
||||
}
|
||||
|
||||
func (a *AppleMDM) runPostManualEnrollment(ctx context.Context, args appleMDMArgs) error {
|
||||
if err := a.installFleetd(ctx, args.HostUUID); err != nil {
|
||||
if _, err := a.installFleetd(ctx, args.HostUUID); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "installing post-enrollment packages")
|
||||
}
|
||||
|
||||
|
|
@ -81,13 +91,21 @@ func (a *AppleMDM) runPostManualEnrollment(ctx context.Context, args appleMDMArg
|
|||
}
|
||||
|
||||
func (a *AppleMDM) runPostDEPEnrollment(ctx context.Context, args appleMDMArgs) error {
|
||||
if err := a.installFleetd(ctx, args.HostUUID); err != nil {
|
||||
var awaitCmdUUIDs []string
|
||||
|
||||
fleetdCmdUUID, err := a.installFleetd(ctx, args.HostUUID)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "installing post-enrollment packages")
|
||||
}
|
||||
awaitCmdUUIDs = append(awaitCmdUUIDs, fleetdCmdUUID)
|
||||
|
||||
if err := a.installBootstrapPackage(ctx, args.HostUUID, args.TeamID); err != nil {
|
||||
bootstrapCmdUUID, err := a.installBootstrapPackage(ctx, args.HostUUID, args.TeamID)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "installing post-enrollment packages")
|
||||
}
|
||||
if bootstrapCmdUUID != "" {
|
||||
awaitCmdUUIDs = append(awaitCmdUUIDs, bootstrapCmdUUID)
|
||||
}
|
||||
|
||||
if ref := args.EnrollReference; ref != "" {
|
||||
a.Log.Log("info", "got an enroll_reference", "host_uuid", args.HostUUID, "ref", ref)
|
||||
|
|
@ -112,30 +130,143 @@ func (a *AppleMDM) runPostDEPEnrollment(ctx context.Context, args appleMDMArgs)
|
|||
|
||||
if ssoEnabled {
|
||||
a.Log.Log("info", "setting username and fullname", "host_uuid", args.HostUUID)
|
||||
cmdUUID := uuid.New().String()
|
||||
if err := a.Commander.AccountConfiguration(
|
||||
ctx,
|
||||
[]string{args.HostUUID},
|
||||
uuid.New().String(),
|
||||
cmdUUID,
|
||||
acct.Fullname,
|
||||
acct.Username,
|
||||
); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "sending AccountConfiguration command")
|
||||
}
|
||||
awaitCmdUUIDs = append(awaitCmdUUIDs, cmdUUID)
|
||||
}
|
||||
}
|
||||
|
||||
var manualRelease bool
|
||||
if args.TeamID == nil {
|
||||
ac, err := a.Datastore.AppConfig(ctx)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "get AppConfig to read enable_release_device_manually")
|
||||
}
|
||||
manualRelease = ac.MDM.MacOSSetup.EnableReleaseDeviceManually.Value
|
||||
} else {
|
||||
tm, err := a.Datastore.Team(ctx, *args.TeamID)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "get Team to read enable_release_device_manually")
|
||||
}
|
||||
manualRelease = tm.Config.MDM.MacOSSetup.EnableReleaseDeviceManually.Value
|
||||
}
|
||||
|
||||
if !manualRelease {
|
||||
// send all command uuids for the commands sent here during post-DEP
|
||||
// enrollment and enqueue a job to look for the status of those commands to
|
||||
// be final and same for MDM profiles of that host; it means the DEP
|
||||
// enrollment process is done and the device can be released.
|
||||
if err := QueueAppleMDMJob(ctx, a.Datastore, a.Log, AppleMDMPostDEPReleaseDeviceTask,
|
||||
args.HostUUID, args.TeamID, args.EnrollReference, awaitCmdUUIDs...); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "queue Apple Post-DEP release device job")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *AppleMDM) installFleetd(ctx context.Context, hostUUID string) error {
|
||||
func (a *AppleMDM) runPostDEPReleaseDevice(ctx context.Context, args appleMDMArgs) error {
|
||||
// Edge cases:
|
||||
// - if the device goes offline for a long time, should we go ahead and
|
||||
// release after a while?
|
||||
// - if some commands/profiles failed (a final state), should we go ahead
|
||||
// and release?
|
||||
// - if the device keeps moving team, or profiles keep being added/removed
|
||||
// from its team, it's possible that its profiles will never settle and
|
||||
// always have pending statuses. Same as going offline, should we release
|
||||
// after a while?
|
||||
//
|
||||
// We opted "yes" to all those, and we want to release after a few minutes,
|
||||
// not hours, so we'll allow only a couple retries.
|
||||
|
||||
level.Debug(a.Log).Log(
|
||||
"task", "runPostDEPReleaseDevice",
|
||||
"msg", fmt.Sprintf("awaiting commands %v and profiles to settle for host %s", args.EnrollmentCommands, args.HostUUID),
|
||||
)
|
||||
|
||||
if retryNum, _ := ctx.Value(retryNumberCtxKey).(int); retryNum > 2 {
|
||||
// give up and release the device
|
||||
a.Log.Log("info", "releasing device after too many attempts", "host_uuid", args.HostUUID, "retries", retryNum)
|
||||
if err := a.Commander.DeviceConfigured(ctx, args.HostUUID, uuid.NewString()); err != nil {
|
||||
return ctxerr.Wrapf(ctx, err, "failed to enqueue DeviceConfigured command after %d retries", retryNum)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, cmdUUID := range args.EnrollmentCommands {
|
||||
if cmdUUID == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
res, err := a.Datastore.GetMDMAppleCommandResults(ctx, cmdUUID)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "failed to get MDM command results")
|
||||
}
|
||||
|
||||
var completed bool
|
||||
for _, r := range res {
|
||||
// succeeded or failed, it is done (final state)
|
||||
if r.Status == fleet.MDMAppleStatusAcknowledged || r.Status == fleet.MDMAppleStatusError ||
|
||||
r.Status == fleet.MDMAppleStatusCommandFormatError {
|
||||
completed = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !completed {
|
||||
// DEP enrollment commands are not done being delivered to that device,
|
||||
// cannot release it now.
|
||||
return fmt.Errorf("device not ready for release, still awaiting result for command %s, will retry", cmdUUID)
|
||||
}
|
||||
level.Debug(a.Log).Log(
|
||||
"task", "runPostDEPReleaseDevice",
|
||||
"msg", fmt.Sprintf("command %s has completed", cmdUUID),
|
||||
)
|
||||
}
|
||||
|
||||
// all DEP-enrollment commands are done, check the host's profiles
|
||||
profs, err := a.Datastore.GetHostMDMAppleProfiles(ctx, args.HostUUID)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "failed to get host MDM profiles")
|
||||
}
|
||||
for _, prof := range profs {
|
||||
// if it has any pending profiles, then its profiles are not done being
|
||||
// delivered (installed or removed).
|
||||
if prof.Status == nil || *prof.Status == fleet.MDMDeliveryPending {
|
||||
return fmt.Errorf("device not ready for release, profile %s is still pending, will retry", prof.Identifier)
|
||||
}
|
||||
level.Debug(a.Log).Log(
|
||||
"task", "runPostDEPReleaseDevice",
|
||||
"msg", fmt.Sprintf("profile %s has been deployed", prof.Identifier),
|
||||
)
|
||||
}
|
||||
|
||||
// release the device
|
||||
a.Log.Log("info", "releasing device, all DEP enrollment commands and profiles have completed", "host_uuid", args.HostUUID)
|
||||
if err := a.Commander.DeviceConfigured(ctx, args.HostUUID, uuid.NewString()); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "failed to enqueue DeviceConfigured command")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *AppleMDM) installFleetd(ctx context.Context, hostUUID string) (string, error) {
|
||||
cmdUUID := uuid.New().String()
|
||||
if err := a.Commander.InstallEnterpriseApplication(ctx, []string{hostUUID}, cmdUUID, apple_mdm.FleetdPublicManifestURL); err != nil {
|
||||
return err
|
||||
return "", err
|
||||
}
|
||||
a.Log.Log("info", "sent command to install fleetd", "host_uuid", hostUUID)
|
||||
return nil
|
||||
return cmdUUID, nil
|
||||
}
|
||||
|
||||
func (a *AppleMDM) installBootstrapPackage(ctx context.Context, hostUUID string, teamID *uint) error {
|
||||
func (a *AppleMDM) installBootstrapPackage(ctx context.Context, hostUUID string, teamID *uint) (string, error) {
|
||||
// GetMDMAppleBootstrapPackageMeta expects team id 0 for no team
|
||||
var tmID uint
|
||||
if teamID != nil {
|
||||
|
|
@ -146,34 +277,34 @@ func (a *AppleMDM) installBootstrapPackage(ctx context.Context, hostUUID string,
|
|||
var nfe fleet.NotFoundError
|
||||
if errors.As(err, &nfe) {
|
||||
a.Log.Log("info", "unable to find a bootstrap package for DEP enrolled device, skipping installation", "host_uuid", hostUUID)
|
||||
return nil
|
||||
return "", nil
|
||||
}
|
||||
|
||||
return err
|
||||
return "", err
|
||||
}
|
||||
|
||||
appCfg, err := a.Datastore.AppConfig(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
return "", err
|
||||
}
|
||||
|
||||
url, err := meta.URL(appCfg.ServerSettings.ServerURL)
|
||||
if err != nil {
|
||||
return err
|
||||
return "", err
|
||||
}
|
||||
|
||||
manifest := appmanifest.NewFromSha(meta.Sha256, url)
|
||||
cmdUUID := uuid.New().String()
|
||||
err = a.Commander.InstallEnterpriseApplicationWithEmbeddedManifest(ctx, []string{hostUUID}, cmdUUID, manifest)
|
||||
if err != nil {
|
||||
return err
|
||||
return "", err
|
||||
}
|
||||
err = a.Datastore.RecordHostBootstrapPackage(ctx, cmdUUID, hostUUID)
|
||||
if err != nil {
|
||||
return err
|
||||
return "", err
|
||||
}
|
||||
a.Log.Log("info", "sent command to install bootstrap package", "host_uuid", hostUUID)
|
||||
return nil
|
||||
return cmdUUID, nil
|
||||
}
|
||||
|
||||
// QueueAppleMDMJob queues a apple_mdm job for one of the supported tasks, to
|
||||
|
|
@ -186,6 +317,7 @@ func QueueAppleMDMJob(
|
|||
hostUUID string,
|
||||
teamID *uint,
|
||||
enrollReference string,
|
||||
enrollmentCommandUUIDs ...string,
|
||||
) error {
|
||||
attrs := []interface{}{
|
||||
"enabled", "true",
|
||||
|
|
@ -196,15 +328,25 @@ func QueueAppleMDMJob(
|
|||
if teamID != nil {
|
||||
attrs = append(attrs, "team_id", *teamID)
|
||||
}
|
||||
if len(enrollmentCommandUUIDs) > 0 {
|
||||
attrs = append(attrs, "enrollment_commands", enrollmentCommandUUIDs)
|
||||
}
|
||||
level.Info(logger).Log(attrs...)
|
||||
|
||||
args := &appleMDMArgs{
|
||||
Task: task,
|
||||
HostUUID: hostUUID,
|
||||
TeamID: teamID,
|
||||
EnrollReference: enrollReference,
|
||||
Task: task,
|
||||
HostUUID: hostUUID,
|
||||
TeamID: teamID,
|
||||
EnrollReference: enrollReference,
|
||||
EnrollmentCommands: enrollmentCommandUUIDs,
|
||||
}
|
||||
job, err := QueueJob(ctx, ds, appleMDMJobName, args)
|
||||
|
||||
// the release device task is always added with a delay
|
||||
var delay time.Duration
|
||||
if task == AppleMDMPostDEPReleaseDeviceTask {
|
||||
delay = 30 * time.Second
|
||||
}
|
||||
job, err := QueueJobWithDelay(ctx, ds, appleMDMJobName, args, delay)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "queueing job")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import (
|
|||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/fleetdm/fleet/v4/pkg/optjson"
|
||||
"github.com/fleetdm/fleet/v4/server/datastore/mysql"
|
||||
"github.com/fleetdm/fleet/v4/server/fleet"
|
||||
apple_mdm "github.com/fleetdm/fleet/v4/server/mdm/apple"
|
||||
|
|
@ -40,6 +41,8 @@ func TestAppleMDM(t *testing.T) {
|
|||
// specific internals (sequence and number of calls, etc.). The MDM storage
|
||||
// and pusher are mocks.
|
||||
ds := mysql.CreateMySQLDS(t)
|
||||
// call TruncateTables immediately as a DB migation may have created jobs
|
||||
mysql.TruncateTables(t, ds)
|
||||
|
||||
mdmStorage, err := ds.NewMDMAppleMDMStorage([]byte("test"), []byte("test"))
|
||||
require.NoError(t, err)
|
||||
|
|
@ -92,6 +95,32 @@ func TestAppleMDM(t *testing.T) {
|
|||
return commands
|
||||
}
|
||||
|
||||
enableManualRelease := func(t *testing.T, teamID *uint) {
|
||||
if teamID == nil {
|
||||
enableAppCfg := func(enable bool) {
|
||||
ac, err := ds.AppConfig(ctx)
|
||||
require.NoError(t, err)
|
||||
ac.MDM.MacOSSetup.EnableReleaseDeviceManually = optjson.SetBool(enable)
|
||||
err = ds.SaveAppConfig(ctx, ac)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
enableAppCfg(true)
|
||||
t.Cleanup(func() { enableAppCfg(false) })
|
||||
} else {
|
||||
enableTm := func(enable bool) {
|
||||
tm, err := ds.Team(ctx, *teamID)
|
||||
require.NoError(t, err)
|
||||
tm.Config.MDM.MacOSSetup.EnableReleaseDeviceManually = optjson.SetBool(enable)
|
||||
_, err = ds.SaveTeam(ctx, tm)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
enableTm(true)
|
||||
t.Cleanup(func() { enableTm(false) })
|
||||
}
|
||||
}
|
||||
|
||||
t.Run("no-op with nil commander", func(t *testing.T) {
|
||||
defer mysql.TruncateTables(t, ds)
|
||||
|
||||
|
|
@ -115,7 +144,7 @@ func TestAppleMDM(t *testing.T) {
|
|||
// again
|
||||
time.Sleep(time.Second)
|
||||
|
||||
jobs, err := ds.GetQueuedJobs(ctx, 1)
|
||||
jobs, err := ds.GetQueuedJobs(ctx, 1, time.Now().UTC().Add(time.Minute)) // look in the future to catch any delayed job
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, jobs)
|
||||
})
|
||||
|
|
@ -143,7 +172,7 @@ func TestAppleMDM(t *testing.T) {
|
|||
// ensure the job's not_before allows it to be returned
|
||||
time.Sleep(time.Second)
|
||||
|
||||
jobs, err := ds.GetQueuedJobs(ctx, 1)
|
||||
jobs, err := ds.GetQueuedJobs(ctx, 1, time.Time{})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, jobs, 1)
|
||||
require.Contains(t, jobs[0].Error, "unknown task: no-such-task")
|
||||
|
|
@ -175,9 +204,49 @@ func TestAppleMDM(t *testing.T) {
|
|||
// again
|
||||
time.Sleep(time.Second)
|
||||
|
||||
jobs, err := ds.GetQueuedJobs(ctx, 1)
|
||||
jobs, err := ds.GetQueuedJobs(ctx, 1, time.Now().UTC().Add(time.Minute)) // look in the future to catch any delayed job
|
||||
require.NoError(t, err)
|
||||
|
||||
// the post-DEP release device job is pending
|
||||
require.Len(t, jobs, 1)
|
||||
require.Equal(t, appleMDMJobName, jobs[0].Name)
|
||||
require.Contains(t, string(*jobs[0].Args), AppleMDMPostDEPReleaseDeviceTask)
|
||||
require.Equal(t, 0, jobs[0].Retries) // hasn't run yet
|
||||
|
||||
require.ElementsMatch(t, []string{"InstallEnterpriseApplication"}, getEnqueuedCommandTypes(t))
|
||||
})
|
||||
|
||||
t.Run("installs default manifest, manual release", func(t *testing.T) {
|
||||
t.Cleanup(func() { mysql.TruncateTables(t, ds) })
|
||||
|
||||
h := createEnrolledHost(t, 1, nil, true)
|
||||
enableManualRelease(t, nil)
|
||||
|
||||
mdmWorker := &AppleMDM{
|
||||
Datastore: ds,
|
||||
Log: nopLog,
|
||||
Commander: apple_mdm.NewMDMAppleCommander(mdmStorage, mockPusher{}),
|
||||
}
|
||||
w := NewWorker(ds, nopLog)
|
||||
w.Register(mdmWorker)
|
||||
|
||||
err = QueueAppleMDMJob(ctx, ds, nopLog, AppleMDMPostDEPEnrollmentTask, h.UUID, nil, "")
|
||||
require.NoError(t, err)
|
||||
|
||||
// run the worker, should succeed
|
||||
err = w.ProcessJobs(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
// ensure the job's not_before allows it to be returned if it were to run
|
||||
// again
|
||||
time.Sleep(time.Second)
|
||||
|
||||
jobs, err := ds.GetQueuedJobs(ctx, 1, time.Now().UTC().Add(time.Minute)) // look in the future to catch any delayed job
|
||||
require.NoError(t, err)
|
||||
|
||||
// there is no post-DEP release device job pending
|
||||
require.Empty(t, jobs)
|
||||
|
||||
require.ElementsMatch(t, []string{"InstallEnterpriseApplication"}, getEnqueuedCommandTypes(t))
|
||||
})
|
||||
|
||||
|
|
@ -213,9 +282,15 @@ func TestAppleMDM(t *testing.T) {
|
|||
// again
|
||||
time.Sleep(time.Second)
|
||||
|
||||
jobs, err := ds.GetQueuedJobs(ctx, 1)
|
||||
jobs, err := ds.GetQueuedJobs(ctx, 1, time.Now().UTC().Add(time.Minute)) // look in the future to catch any delayed job
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, jobs)
|
||||
|
||||
// the post-DEP release device job is pending
|
||||
require.Len(t, jobs, 1)
|
||||
require.Equal(t, appleMDMJobName, jobs[0].Name)
|
||||
require.Contains(t, string(*jobs[0].Args), AppleMDMPostDEPReleaseDeviceTask)
|
||||
require.Equal(t, 0, jobs[0].Retries) // hasn't run yet
|
||||
|
||||
require.ElementsMatch(t, []string{"InstallEnterpriseApplication", "InstallEnterpriseApplication"}, getEnqueuedCommandTypes(t))
|
||||
|
||||
ms, err := ds.GetHostMDMMacOSSetup(ctx, h.ID)
|
||||
|
|
@ -258,9 +333,64 @@ func TestAppleMDM(t *testing.T) {
|
|||
// again
|
||||
time.Sleep(time.Second)
|
||||
|
||||
jobs, err := ds.GetQueuedJobs(ctx, 1)
|
||||
jobs, err := ds.GetQueuedJobs(ctx, 1, time.Now().UTC().Add(time.Minute)) // look in the future to catch any delayed job
|
||||
require.NoError(t, err)
|
||||
|
||||
// the post-DEP release device job is pending
|
||||
require.Len(t, jobs, 1)
|
||||
require.Equal(t, appleMDMJobName, jobs[0].Name)
|
||||
require.Contains(t, string(*jobs[0].Args), AppleMDMPostDEPReleaseDeviceTask)
|
||||
require.Equal(t, 0, jobs[0].Retries) // hasn't run yet
|
||||
|
||||
require.ElementsMatch(t, []string{"InstallEnterpriseApplication", "InstallEnterpriseApplication"}, getEnqueuedCommandTypes(t))
|
||||
|
||||
ms, err := ds.GetHostMDMMacOSSetup(ctx, h.ID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "custom-team-bootstrap", ms.BootstrapPackageName)
|
||||
})
|
||||
|
||||
t.Run("installs custom bootstrap manifest of a team, manual release", func(t *testing.T) {
|
||||
t.Cleanup(func() { mysql.TruncateTables(t, ds) })
|
||||
|
||||
tm, err := ds.NewTeam(ctx, &fleet.Team{Name: "test"})
|
||||
require.NoError(t, err)
|
||||
enableManualRelease(t, &tm.ID)
|
||||
|
||||
h := createEnrolledHost(t, 1, &tm.ID, true)
|
||||
err = ds.InsertMDMAppleBootstrapPackage(ctx, &fleet.MDMAppleBootstrapPackage{
|
||||
Name: "custom-team-bootstrap",
|
||||
TeamID: tm.ID,
|
||||
Bytes: []byte("test"),
|
||||
Sha256: []byte("test"),
|
||||
Token: "token",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
mdmWorker := &AppleMDM{
|
||||
Datastore: ds,
|
||||
Log: nopLog,
|
||||
Commander: apple_mdm.NewMDMAppleCommander(mdmStorage, mockPusher{}),
|
||||
}
|
||||
w := NewWorker(ds, nopLog)
|
||||
w.Register(mdmWorker)
|
||||
|
||||
err = QueueAppleMDMJob(ctx, ds, nopLog, AppleMDMPostDEPEnrollmentTask, h.UUID, &tm.ID, "")
|
||||
require.NoError(t, err)
|
||||
|
||||
// run the worker, should succeed
|
||||
err = w.ProcessJobs(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
// ensure the job's not_before allows it to be returned if it were to run
|
||||
// again
|
||||
time.Sleep(time.Second)
|
||||
|
||||
jobs, err := ds.GetQueuedJobs(ctx, 1, time.Now().UTC().Add(time.Minute)) // look in the future to catch any delayed job
|
||||
require.NoError(t, err)
|
||||
|
||||
// there is no post-DEP release device job pending
|
||||
require.Empty(t, jobs)
|
||||
|
||||
require.ElementsMatch(t, []string{"InstallEnterpriseApplication", "InstallEnterpriseApplication"}, getEnqueuedCommandTypes(t))
|
||||
|
||||
ms, err := ds.GetHostMDMMacOSSetup(ctx, h.ID)
|
||||
|
|
@ -292,7 +422,7 @@ func TestAppleMDM(t *testing.T) {
|
|||
// again
|
||||
time.Sleep(time.Second)
|
||||
|
||||
jobs, err := ds.GetQueuedJobs(ctx, 1)
|
||||
jobs, err := ds.GetQueuedJobs(ctx, 1, time.Time{})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, jobs, 1)
|
||||
require.Contains(t, jobs[0].Error, "MDMIdPAccount with uuid abcd was not found")
|
||||
|
|
@ -334,9 +464,15 @@ func TestAppleMDM(t *testing.T) {
|
|||
// again
|
||||
time.Sleep(time.Second)
|
||||
|
||||
jobs, err := ds.GetQueuedJobs(ctx, 1)
|
||||
jobs, err := ds.GetQueuedJobs(ctx, 1, time.Now().UTC().Add(time.Minute)) // look in the future to catch any delayed job
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, jobs)
|
||||
|
||||
// the post-DEP release device job is pending, having failed its first attempt
|
||||
require.Len(t, jobs, 1)
|
||||
require.Equal(t, appleMDMJobName, jobs[0].Name)
|
||||
require.Contains(t, string(*jobs[0].Args), AppleMDMPostDEPReleaseDeviceTask)
|
||||
require.Equal(t, 0, jobs[0].Retries) // hasn't run yet
|
||||
|
||||
// confirm that AccountConfiguration command was not enqueued
|
||||
require.ElementsMatch(t, []string{"InstallEnterpriseApplication"}, getEnqueuedCommandTypes(t))
|
||||
})
|
||||
|
|
@ -383,9 +519,15 @@ func TestAppleMDM(t *testing.T) {
|
|||
// again
|
||||
time.Sleep(time.Second)
|
||||
|
||||
jobs, err := ds.GetQueuedJobs(ctx, 1)
|
||||
jobs, err := ds.GetQueuedJobs(ctx, 1, time.Now().UTC().Add(time.Minute)) // look in the future to catch any delayed job
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, jobs)
|
||||
|
||||
// the post-DEP release device job is pending
|
||||
require.Len(t, jobs, 1)
|
||||
require.Equal(t, appleMDMJobName, jobs[0].Name)
|
||||
require.Contains(t, string(*jobs[0].Args), AppleMDMPostDEPReleaseDeviceTask)
|
||||
require.Equal(t, 0, jobs[0].Retries) // hasn't run yet
|
||||
|
||||
require.ElementsMatch(t, []string{"InstallEnterpriseApplication", "AccountConfiguration"}, getEnqueuedCommandTypes(t))
|
||||
})
|
||||
|
||||
|
|
@ -413,7 +555,7 @@ func TestAppleMDM(t *testing.T) {
|
|||
// again
|
||||
time.Sleep(time.Second)
|
||||
|
||||
jobs, err := ds.GetQueuedJobs(ctx, 1)
|
||||
jobs, err := ds.GetQueuedJobs(ctx, 1, time.Now().UTC().Add(time.Minute)) // look in the future to catch any delayed job
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, jobs)
|
||||
require.ElementsMatch(t, []string{"InstallEnterpriseApplication"}, getEnqueuedCommandTypes(t))
|
||||
|
|
|
|||
|
|
@ -25,6 +25,8 @@ import (
|
|||
func TestMacosSetupAssistant(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
ds := mysql.CreateMySQLDS(t)
|
||||
// call TruncateTables immediately as some DB migrations may create jobs
|
||||
mysql.TruncateTables(t, ds)
|
||||
|
||||
// create a couple hosts for no team, team 1 and team 2 (none for team 3)
|
||||
hosts := make([]*fleet.Host, 6)
|
||||
|
|
@ -140,7 +142,7 @@ func TestMacosSetupAssistant(t *testing.T) {
|
|||
err = w.ProcessJobs(ctx)
|
||||
require.NoError(t, err)
|
||||
// no remaining jobs to process
|
||||
pending, err := ds.GetQueuedJobs(ctx, 10)
|
||||
pending, err := ds.GetQueuedJobs(ctx, 10, time.Time{})
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, pending)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,11 +12,17 @@ import (
|
|||
"github.com/go-kit/kit/log/level"
|
||||
)
|
||||
|
||||
type ctxKey int
|
||||
|
||||
const (
|
||||
maxRetries = 5
|
||||
// nvdCVEURL is the base link to a CVE on the NVD website, only the CVE code
|
||||
// needs to be appended to make it a valid link.
|
||||
nvdCVEURL = "https://nvd.nist.gov/vuln/detail/"
|
||||
|
||||
// context key for the retry number of a job, made available via the context
|
||||
// to the job processor.
|
||||
retryNumberCtxKey = ctxKey(0)
|
||||
)
|
||||
|
||||
const (
|
||||
|
|
@ -89,14 +95,26 @@ func (w *Worker) Register(jobs ...Job) {
|
|||
// identified by the name (e.g. "jira"). The args value is marshaled as JSON
|
||||
// and provided to the job processor when the job is executed.
|
||||
func QueueJob(ctx context.Context, ds fleet.Datastore, name string, args interface{}) (*fleet.Job, error) {
|
||||
return QueueJobWithDelay(ctx, ds, name, args, 0)
|
||||
}
|
||||
|
||||
// QueueJobWithDelay is like QueueJob but does not make the job available
|
||||
// before a specified delay (or no delay if delay is <= 0).
|
||||
func QueueJobWithDelay(ctx context.Context, ds fleet.Datastore, name string, args interface{}, delay time.Duration) (*fleet.Job, error) {
|
||||
argsJSON, err := json.Marshal(args)
|
||||
if err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "marshal args")
|
||||
}
|
||||
|
||||
var notBefore time.Time
|
||||
if delay > 0 {
|
||||
notBefore = time.Now().UTC().Add(delay)
|
||||
}
|
||||
job := &fleet.Job{
|
||||
Name: name,
|
||||
Args: (*json.RawMessage)(&argsJSON),
|
||||
State: fleet.JobStateQueued,
|
||||
Name: name,
|
||||
Args: (*json.RawMessage)(&argsJSON),
|
||||
State: fleet.JobStateQueued,
|
||||
NotBefore: notBefore,
|
||||
}
|
||||
|
||||
return ds.NewJob(ctx, job)
|
||||
|
|
@ -122,7 +140,7 @@ func (w *Worker) ProcessJobs(ctx context.Context) error {
|
|||
// process jobs until there are none left or the context is cancelled
|
||||
seen := make(map[uint]struct{})
|
||||
for {
|
||||
jobs, err := w.ds.GetQueuedJobs(ctx, maxNumJobs)
|
||||
jobs, err := w.ds.GetQueuedJobs(ctx, maxNumJobs, time.Time{})
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "get queued jobs")
|
||||
}
|
||||
|
|
@ -191,6 +209,7 @@ func (w *Worker) processJob(ctx context.Context, job *fleet.Job) error {
|
|||
args = *job.Args
|
||||
}
|
||||
|
||||
ctx = context.WithValue(ctx, retryNumberCtxKey, job.Retries)
|
||||
return j.Run(ctx, args)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ func TestWorker(t *testing.T) {
|
|||
|
||||
// set up mocks
|
||||
getQueuedJobsCalled := 0
|
||||
ds.GetQueuedJobsFunc = func(ctx context.Context, maxNumJobs int) ([]*fleet.Job, error) {
|
||||
ds.GetQueuedJobsFunc = func(ctx context.Context, maxNumJobs int, now time.Time) ([]*fleet.Job, error) {
|
||||
if getQueuedJobsCalled > 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
|
@ -93,7 +93,7 @@ func TestWorkerRetries(t *testing.T) {
|
|||
State: fleet.JobStateQueued,
|
||||
Retries: 0,
|
||||
}
|
||||
ds.GetQueuedJobsFunc = func(ctx context.Context, maxNumJobs int) ([]*fleet.Job, error) {
|
||||
ds.GetQueuedJobsFunc = func(ctx context.Context, maxNumJobs int, now time.Time) ([]*fleet.Job, error) {
|
||||
if theJob.State == fleet.JobStateQueued {
|
||||
return []*fleet.Job{theJob}, nil
|
||||
}
|
||||
|
|
@ -173,7 +173,7 @@ func TestWorkerMiddleJobFails(t *testing.T) {
|
|||
Retries: 0,
|
||||
},
|
||||
}
|
||||
ds.GetQueuedJobsFunc = func(ctx context.Context, maxNumJobs int) ([]*fleet.Job, error) {
|
||||
ds.GetQueuedJobsFunc = func(ctx context.Context, maxNumJobs int, now time.Time) ([]*fleet.Job, error) {
|
||||
var queued []*fleet.Job
|
||||
for _, j := range jobs {
|
||||
if j.State == fleet.JobStateQueued {
|
||||
|
|
@ -241,6 +241,8 @@ func TestWorkerMiddleJobFails(t *testing.T) {
|
|||
func TestWorkerWithRealDatastore(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
ds := mysql.CreateMySQLDS(t)
|
||||
// call TruncateTables immediately, because a DB migration may create jobs
|
||||
mysql.TruncateTables(t, ds)
|
||||
|
||||
oldDelayPerRetry := delayPerRetry
|
||||
delayPerRetry = []time.Duration{
|
||||
|
|
@ -295,7 +297,7 @@ func TestWorkerWithRealDatastore(t *testing.T) {
|
|||
// timestamp in mysql vs the one set in ProcessJobs (time.Now().Add(...)).
|
||||
time.Sleep(time.Second)
|
||||
|
||||
jobs, err := ds.GetQueuedJobs(ctx, 10)
|
||||
jobs, err := ds.GetQueuedJobs(ctx, 10, time.Time{})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, jobs, 1)
|
||||
require.Equal(t, j2.ID, jobs[0].ID)
|
||||
|
|
@ -311,7 +313,7 @@ func TestWorkerWithRealDatastore(t *testing.T) {
|
|||
// timestamp in mysql vs the one set in ProcessJobs (time.Now().Add(...)).
|
||||
time.Sleep(time.Second)
|
||||
|
||||
jobs, err = ds.GetQueuedJobs(ctx, 10)
|
||||
jobs, err = ds.GetQueuedJobs(ctx, 10, time.Time{})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, jobs, 1)
|
||||
require.Equal(t, j2.ID, jobs[0].ID)
|
||||
|
|
@ -326,7 +328,7 @@ func TestWorkerWithRealDatastore(t *testing.T) {
|
|||
|
||||
time.Sleep(time.Second)
|
||||
|
||||
jobs, err = ds.GetQueuedJobs(ctx, 10)
|
||||
jobs, err = ds.GetQueuedJobs(ctx, 10, time.Time{})
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, jobs)
|
||||
|
||||
|
|
|
|||
|
|
@ -113,6 +113,10 @@ github.com/fleetdm/fleet/v4/server/fleet/MDM MacOSSetup fleet.MacOSSetup
|
|||
github.com/fleetdm/fleet/v4/server/fleet/MacOSSetup BootstrapPackage optjson.String
|
||||
github.com/fleetdm/fleet/v4/server/fleet/MacOSSetup EnableEndUserAuthentication bool
|
||||
github.com/fleetdm/fleet/v4/server/fleet/MacOSSetup MacOSSetupAssistant optjson.String
|
||||
github.com/fleetdm/fleet/v4/server/fleet/MacOSSetup EnableReleaseDeviceManually optjson.Bool
|
||||
github.com/fleetdm/fleet/v4/pkg/optjson/Bool Set bool
|
||||
github.com/fleetdm/fleet/v4/pkg/optjson/Bool Valid bool
|
||||
github.com/fleetdm/fleet/v4/pkg/optjson/Bool Value bool
|
||||
github.com/fleetdm/fleet/v4/server/fleet/MDM MacOSMigration fleet.MacOSMigration
|
||||
github.com/fleetdm/fleet/v4/server/fleet/MacOSMigration Enable bool
|
||||
github.com/fleetdm/fleet/v4/server/fleet/MacOSMigration Mode fleet.MacOSMigrationMode string
|
||||
|
|
@ -121,9 +125,6 @@ github.com/fleetdm/fleet/v4/server/fleet/MDM EndUserAuthentication fleet.MDMEndU
|
|||
github.com/fleetdm/fleet/v4/server/fleet/MDMEndUserAuthentication SSOProviderSettings fleet.SSOProviderSettings
|
||||
github.com/fleetdm/fleet/v4/server/fleet/MDM WindowsEnabledAndConfigured bool
|
||||
github.com/fleetdm/fleet/v4/server/fleet/MDM EnableDiskEncryption optjson.Bool
|
||||
github.com/fleetdm/fleet/v4/pkg/optjson/Bool Set bool
|
||||
github.com/fleetdm/fleet/v4/pkg/optjson/Bool Valid bool
|
||||
github.com/fleetdm/fleet/v4/pkg/optjson/Bool Value bool
|
||||
github.com/fleetdm/fleet/v4/server/fleet/MDM WindowsSettings fleet.WindowsSettings
|
||||
github.com/fleetdm/fleet/v4/server/fleet/WindowsSettings CustomSettings optjson.Slice[github.com/fleetdm/fleet/v4/server/fleet.MDMProfileSpec]
|
||||
github.com/fleetdm/fleet/v4/pkg/optjson/Slice[github.com/fleetdm/fleet/v4/server/fleet.MDMProfileSpec] Set bool
|
||||
|
|
|
|||
|
|
@ -20,6 +20,10 @@ github.com/fleetdm/fleet/v4/server/fleet/TeamMDM MacOSSetup fleet.MacOSSetup
|
|||
github.com/fleetdm/fleet/v4/server/fleet/MacOSSetup BootstrapPackage optjson.String
|
||||
github.com/fleetdm/fleet/v4/server/fleet/MacOSSetup EnableEndUserAuthentication bool
|
||||
github.com/fleetdm/fleet/v4/server/fleet/MacOSSetup MacOSSetupAssistant optjson.String
|
||||
github.com/fleetdm/fleet/v4/server/fleet/MacOSSetup EnableReleaseDeviceManually optjson.Bool
|
||||
github.com/fleetdm/fleet/v4/pkg/optjson/Bool Set bool
|
||||
github.com/fleetdm/fleet/v4/pkg/optjson/Bool Valid bool
|
||||
github.com/fleetdm/fleet/v4/pkg/optjson/Bool Value bool
|
||||
github.com/fleetdm/fleet/v4/server/fleet/TeamMDM WindowsSettings fleet.WindowsSettings
|
||||
github.com/fleetdm/fleet/v4/server/fleet/WindowsSettings CustomSettings optjson.Slice[github.com/fleetdm/fleet/v4/server/fleet.MDMProfileSpec]
|
||||
github.com/fleetdm/fleet/v4/pkg/optjson/Slice[github.com/fleetdm/fleet/v4/server/fleet.MDMProfileSpec] Set bool
|
||||
|
|
|
|||
Loading…
Reference in a new issue