macOS OOBE Prefill and Lock Local Account: feature branch (#17854)

Feature branch for #9147 .
This commit is contained in:
Martin Angers 2024-03-27 10:12:01 -04:00 committed by GitHub
commit 79f9caf49d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
81 changed files with 3397 additions and 1339 deletions

View file

@ -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

View 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`.

View file

@ -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 cant 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 cant 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)
})

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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:

View file

@ -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

View file

@ -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:

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View 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

View file

@ -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", `Couldnt edit macos_setup_assistant. The automatic enrollment profile cant 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 {

View 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
)

View 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()
})
}

View file

@ -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, &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")
}
}
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, &notFoundError{})
}
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

View file

@ -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

View file

@ -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,

View file

@ -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,
},

View file

@ -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;
}

View file

@ -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);

View file

@ -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;

View file

@ -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: {

View file

@ -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;

View file

@ -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;

View file

@ -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 {

View file

@ -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;

View file

@ -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;
}
}
}

View file

@ -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&apos;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;

View file

@ -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;
}
}

View file

@ -0,0 +1 @@
export { default } from "./AdvancedOptionsForm";

View file

@ -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", "Couldnt 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;

View file

@ -0,0 +1 @@
export { default } from "./DeleteAutoEnrollmentProfile";

View file

@ -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;

View file

@ -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;
}
}

View file

@ -0,0 +1 @@
export { default } from "./SetupAssistantPreview";

View file

@ -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;

View file

@ -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.
}
}

View file

@ -0,0 +1 @@
export { default } from "./SetupAssistantProfileCard";

View file

@ -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;

View file

@ -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;
};

View file

@ -0,0 +1 @@
export { default } from "./SetupAssistantProfileUploader";

View file

@ -0,0 +1 @@
export { default } from "./SetupAssistant";

View file

@ -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`,

View file

@ -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

View file

@ -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("Couldnt 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;

View file

@ -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(

View 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");
});
});
});

View 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`;
};

View file

@ -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`,

View file

@ -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
}

View file

@ -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)

View file

@ -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
}

View file

@ -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

View file

@ -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"}}),

View file

@ -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)

View file

@ -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.

View file

@ -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)

View file

@ -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)
}

View file

@ -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)

View file

@ -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)

View file

@ -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.
//

View file

@ -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"`

View file

@ -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) {

View file

@ -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.`)

View file

@ -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{

View file

@ -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() {

View file

@ -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() {

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -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")
}

View file

@ -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))

View file

@ -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)
}

View file

@ -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)
}

View file

@ -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)

View file

@ -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

View file

@ -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