add a cron job to deliver windows MDM profiles (#15065)
for #14364 , this implements the delivery mechanism for windows hosts. I will follow up in another PR with logic to update the profile status when we get responses from the device.
This commit is contained in:
+6
-3
@@ -948,7 +948,7 @@ func newAppleMDMDEPProfileAssigner(
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func newMDMAppleProfileManager(
|
||||
func newMDMProfileManager(
|
||||
ctx context.Context,
|
||||
instanceID string,
|
||||
ds fleet.Datastore,
|
||||
@@ -967,8 +967,11 @@ func newMDMAppleProfileManager(
|
||||
s := schedule.New(
|
||||
ctx, name, instanceID, defaultInterval, ds, ds,
|
||||
schedule.WithLogger(logger),
|
||||
schedule.WithJob("manage_profiles", func(ctx context.Context) error {
|
||||
return service.ReconcileProfiles(ctx, ds, commander, logger)
|
||||
schedule.WithJob("manage_apple_profiles", func(ctx context.Context) error {
|
||||
return service.ReconcileAppleProfiles(ctx, ds, commander, logger)
|
||||
}),
|
||||
schedule.WithJob("manage_windows_profiles", func(ctx context.Context) error {
|
||||
return service.ReconcileWindowsProfiles(ctx, ds, logger)
|
||||
}),
|
||||
)
|
||||
|
||||
|
||||
+1
-1
@@ -728,7 +728,7 @@ the way that the Fleet server works.
|
||||
|
||||
if appCfg.MDM.EnabledAndConfigured {
|
||||
if err := cronSchedules.StartCronSchedule(func() (fleet.CronSchedule, error) {
|
||||
return newMDMAppleProfileManager(
|
||||
return newMDMProfileManager(
|
||||
ctx,
|
||||
instanceID,
|
||||
ds,
|
||||
|
||||
@@ -1188,9 +1188,6 @@ func (ds *Datastore) BulkDeleteMDMAppleHostsConfigProfiles(ctx context.Context,
|
||||
})
|
||||
}
|
||||
|
||||
// for tests, set to override the default batch size.
|
||||
var testDeleteMDMAppleProfilesBatchSize int
|
||||
|
||||
func bulkDeleteMDMAppleHostsConfigProfilesDB(ctx context.Context, tx sqlx.ExtContext, profs []*fleet.MDMAppleProfilePayload) error {
|
||||
if len(profs) == 0 {
|
||||
return nil
|
||||
@@ -1212,8 +1209,8 @@ func bulkDeleteMDMAppleHostsConfigProfilesDB(ctx context.Context, tx sqlx.ExtCon
|
||||
|
||||
const defaultBatchSize = 1000 // results in this times 2 placeholders
|
||||
batchSize := defaultBatchSize
|
||||
if testDeleteMDMAppleProfilesBatchSize > 0 {
|
||||
batchSize = testDeleteMDMAppleProfilesBatchSize
|
||||
if testDeleteMDMProfilesBatchSize > 0 {
|
||||
batchSize = testDeleteMDMProfilesBatchSize
|
||||
}
|
||||
|
||||
resetBatch := func() {
|
||||
@@ -1243,9 +1240,6 @@ func bulkDeleteMDMAppleHostsConfigProfilesDB(ctx context.Context, tx sqlx.ExtCon
|
||||
return nil
|
||||
}
|
||||
|
||||
// for tests, set to override the default batch size.
|
||||
var testUpsertMDMAppleDesiredProfilesBatchSize int
|
||||
|
||||
// Note that team ID 0 is used for profiles that apply to hosts in no team
|
||||
// (i.e. pass 0 in that case as part of the teamIDs slice). Only one of the
|
||||
// slice arguments can have values.
|
||||
@@ -1485,8 +1479,8 @@ WHERE
|
||||
|
||||
const defaultBatchSize = 1000 // results in this times 9 placeholders
|
||||
batchSize := defaultBatchSize
|
||||
if testUpsertMDMAppleDesiredProfilesBatchSize > 0 {
|
||||
batchSize = testUpsertMDMAppleDesiredProfilesBatchSize
|
||||
if testUpsertMDMDesiredProfilesBatchSize > 0 {
|
||||
batchSize = testUpsertMDMDesiredProfilesBatchSize
|
||||
}
|
||||
|
||||
resetBatch := func() {
|
||||
@@ -1740,8 +1734,8 @@ func (ds *Datastore) BulkUpsertMDMAppleHostProfiles(ctx context.Context, payload
|
||||
|
||||
const defaultBatchSize = 1000 // results in this times 9 placeholders
|
||||
batchSize := defaultBatchSize
|
||||
if testUpsertMDMAppleDesiredProfilesBatchSize > 0 {
|
||||
batchSize = testUpsertMDMAppleDesiredProfilesBatchSize
|
||||
if testUpsertMDMDesiredProfilesBatchSize > 0 {
|
||||
batchSize = testUpsertMDMDesiredProfilesBatchSize
|
||||
}
|
||||
|
||||
resetBatch := func() {
|
||||
|
||||
@@ -991,17 +991,17 @@ func teamConfigProfileForTest(t *testing.T, name, identifier, uuid string, teamI
|
||||
}
|
||||
|
||||
func testMDMAppleProfileManagementBatch2(t *testing.T, ds *Datastore) {
|
||||
testUpsertMDMAppleDesiredProfilesBatchSize = 2
|
||||
testUpsertMDMDesiredProfilesBatchSize = 2
|
||||
t.Cleanup(func() {
|
||||
testUpsertMDMAppleDesiredProfilesBatchSize = 0
|
||||
testUpsertMDMDesiredProfilesBatchSize = 0
|
||||
})
|
||||
testMDMAppleProfileManagement(t, ds)
|
||||
}
|
||||
|
||||
func testMDMAppleProfileManagementBatch3(t *testing.T, ds *Datastore) {
|
||||
testUpsertMDMAppleDesiredProfilesBatchSize = 3
|
||||
testUpsertMDMDesiredProfilesBatchSize = 3
|
||||
t.Cleanup(func() {
|
||||
testUpsertMDMAppleDesiredProfilesBatchSize = 0
|
||||
testUpsertMDMDesiredProfilesBatchSize = 0
|
||||
})
|
||||
testMDMAppleProfileManagement(t, ds)
|
||||
}
|
||||
@@ -2472,21 +2472,21 @@ func TestMDMAppleFileVaultSummary(t *testing.T) {
|
||||
}
|
||||
|
||||
func testBulkSetPendingMDMAppleHostProfilesBatch2(t *testing.T, ds *Datastore) {
|
||||
testUpsertMDMAppleDesiredProfilesBatchSize = 2
|
||||
testDeleteMDMAppleProfilesBatchSize = 2
|
||||
testUpsertMDMDesiredProfilesBatchSize = 2
|
||||
testDeleteMDMProfilesBatchSize = 2
|
||||
t.Cleanup(func() {
|
||||
testUpsertMDMAppleDesiredProfilesBatchSize = 0
|
||||
testDeleteMDMAppleProfilesBatchSize = 0
|
||||
testUpsertMDMDesiredProfilesBatchSize = 0
|
||||
testDeleteMDMProfilesBatchSize = 0
|
||||
})
|
||||
testBulkSetPendingMDMAppleHostProfiles(t, ds)
|
||||
}
|
||||
|
||||
func testBulkSetPendingMDMAppleHostProfilesBatch3(t *testing.T, ds *Datastore) {
|
||||
testUpsertMDMAppleDesiredProfilesBatchSize = 3
|
||||
testDeleteMDMAppleProfilesBatchSize = 3
|
||||
testUpsertMDMDesiredProfilesBatchSize = 3
|
||||
testDeleteMDMProfilesBatchSize = 3
|
||||
t.Cleanup(func() {
|
||||
testUpsertMDMAppleDesiredProfilesBatchSize = 0
|
||||
testDeleteMDMAppleProfilesBatchSize = 0
|
||||
testUpsertMDMDesiredProfilesBatchSize = 0
|
||||
testDeleteMDMProfilesBatchSize = 0
|
||||
})
|
||||
testBulkSetPendingMDMAppleHostProfiles(t, ds)
|
||||
}
|
||||
|
||||
@@ -9,6 +9,12 @@ import (
|
||||
"github.com/jmoiron/sqlx"
|
||||
)
|
||||
|
||||
// for tests, set to override the default batch size.
|
||||
var testDeleteMDMProfilesBatchSize int
|
||||
|
||||
// for tests, set to override the default batch size.
|
||||
var testUpsertMDMDesiredProfilesBatchSize int
|
||||
|
||||
func (ds *Datastore) GetMDMCommandPlatform(ctx context.Context, commandUUID string) (string, error) {
|
||||
stmt := `
|
||||
SELECT CASE
|
||||
|
||||
@@ -615,3 +615,240 @@ func (ds *Datastore) DeleteMDMWindowsConfigProfile(ctx context.Context, profileU
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ds *Datastore) ListMDMWindowsProfilesToInstall(ctx context.Context) ([]*fleet.MDMWindowsProfilePayload, error) {
|
||||
// The query below is a set difference between:
|
||||
//
|
||||
// - Set A (ds), the desired state, can be obtained from a JOIN between
|
||||
// mdm_windows_configuration_profiles and hosts.
|
||||
// - Set B, the current state given by host_mdm_windows_profiles.
|
||||
//
|
||||
// A - B gives us the profiles that need to be installed:
|
||||
//
|
||||
// - profiles that are in A but not in B
|
||||
//
|
||||
// - profiles that are in A and in B, with an operation type of "install"
|
||||
// and a NULL status. Other statuses mean that the operation is already in
|
||||
// flight (pending), the operation has been completed but is still subject
|
||||
// to independent verification by Fleet (verifying), or has reached a terminal
|
||||
// state (failed or verified). If the profile's content is edited, all relevant hosts will
|
||||
// be marked as status NULL so that it gets re-installed.
|
||||
query := `
|
||||
SELECT
|
||||
ds.profile_uuid,
|
||||
ds.host_uuid
|
||||
FROM (
|
||||
SELECT mwcp.profile_uuid, h.uuid as host_uuid
|
||||
FROM mdm_windows_configuration_profiles mwcp
|
||||
JOIN hosts h ON h.team_id = mwcp.team_id OR (h.team_id IS NULL AND mwcp.team_id = 0)
|
||||
JOIN mdm_windows_enrollments mwe ON mwe.host_uuid = h.uuid
|
||||
WHERE h.platform = 'windows'
|
||||
) as ds
|
||||
LEFT JOIN host_mdm_windows_profiles hmwp
|
||||
ON hmwp.profile_uuid = ds.profile_uuid AND hmwp.host_uuid = ds.host_uuid
|
||||
WHERE
|
||||
-- profiles in A but not in B
|
||||
( hmwp.profile_uuid IS NULL AND hmwp.host_uuid IS NULL ) OR
|
||||
-- profiles in A and B with operation type "install" and NULL status
|
||||
( hmwp.host_uuid IS NOT NULL AND hmwp.operation_type = ? AND hmwp.status IS NULL )
|
||||
`
|
||||
|
||||
var profiles []*fleet.MDMWindowsProfilePayload
|
||||
err := sqlx.SelectContext(ctx, ds.reader(ctx), &profiles, query, fleet.MDMOperationTypeInstall)
|
||||
return profiles, err
|
||||
}
|
||||
|
||||
func (ds *Datastore) ListMDMWindowsProfilesToRemove(ctx context.Context) ([]*fleet.MDMWindowsProfilePayload, error) {
|
||||
// The query below is a set difference between:
|
||||
//
|
||||
// - Set A (ds), the desired state, can be obtained from a JOIN between
|
||||
// mdm_windows_configuration_profiles and hosts.
|
||||
// - Set B, the current state given by host_mdm_windows_profiles.
|
||||
//
|
||||
// B - A gives us the profiles that need to be removed
|
||||
//
|
||||
// Any other case are profiles that are in both B and A, and as such are
|
||||
// processed by the ListMDMWindowsProfilesToInstall method (since they are in
|
||||
// both, their desired state is necessarily to be installed).
|
||||
query := `
|
||||
SELECT
|
||||
hmwp.profile_uuid,
|
||||
hmwp.host_uuid,
|
||||
hmwp.operation_type,
|
||||
COALESCE(hmwp.detail, '') as detail,
|
||||
hmwp.status,
|
||||
hmwp.command_uuid
|
||||
FROM (
|
||||
SELECT h.uuid, mwcp.profile_uuid
|
||||
FROM mdm_windows_configuration_profiles mwcp
|
||||
JOIN hosts h ON h.team_id = mwcp.team_id OR (h.team_id IS NULL AND mwcp.team_id = 0)
|
||||
JOIN mdm_windows_enrollments mwe ON mwe.host_uuid = h.uuid
|
||||
WHERE h.platform = 'windows'
|
||||
) as ds
|
||||
RIGHT JOIN host_mdm_windows_profiles hmwp
|
||||
ON hmwp.profile_uuid = ds.profile_uuid AND hmwp.host_uuid = ds.uuid
|
||||
-- profiles that are in B but not in A
|
||||
WHERE ds.profile_uuid IS NULL AND ds.uuid IS NULL
|
||||
`
|
||||
|
||||
var profiles []*fleet.MDMWindowsProfilePayload
|
||||
err := sqlx.SelectContext(ctx, ds.reader(ctx), &profiles, query)
|
||||
return profiles, err
|
||||
}
|
||||
|
||||
func (ds *Datastore) BulkUpsertMDMWindowsHostProfiles(ctx context.Context, payload []*fleet.MDMWindowsBulkUpsertHostProfilePayload) error {
|
||||
if len(payload) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
executeUpsertBatch := func(valuePart string, args []any) error {
|
||||
stmt := fmt.Sprintf(`
|
||||
INSERT INTO host_mdm_windows_profiles (
|
||||
profile_uuid,
|
||||
host_uuid,
|
||||
status,
|
||||
operation_type,
|
||||
detail,
|
||||
command_uuid,
|
||||
profile_name
|
||||
)
|
||||
VALUES %s
|
||||
ON DUPLICATE KEY UPDATE
|
||||
status = VALUES(status),
|
||||
operation_type = VALUES(operation_type),
|
||||
detail = VALUES(detail),
|
||||
profile_name = VALUES(profile_name),
|
||||
command_uuid = VALUES(command_uuid)`,
|
||||
strings.TrimSuffix(valuePart, ","),
|
||||
)
|
||||
|
||||
_, err := ds.writer(ctx).ExecContext(ctx, stmt, args...)
|
||||
return err
|
||||
}
|
||||
|
||||
var (
|
||||
args []any
|
||||
sb strings.Builder
|
||||
batchCount int
|
||||
)
|
||||
|
||||
const defaultBatchSize = 1000 // results in this times 9 placeholders
|
||||
batchSize := defaultBatchSize
|
||||
if testUpsertMDMDesiredProfilesBatchSize > 0 {
|
||||
batchSize = testUpsertMDMDesiredProfilesBatchSize
|
||||
}
|
||||
|
||||
resetBatch := func() {
|
||||
batchCount = 0
|
||||
args = args[:0]
|
||||
sb.Reset()
|
||||
}
|
||||
|
||||
for _, p := range payload {
|
||||
args = append(args, p.ProfileUUID, p.HostUUID, p.Status, p.OperationType, p.Detail, p.CommandUUID, p.ProfileName)
|
||||
sb.WriteString("(?, ?, ?, ?, ?, ?, ?),")
|
||||
batchCount++
|
||||
|
||||
if batchCount >= batchSize {
|
||||
if err := executeUpsertBatch(sb.String(), args); err != nil {
|
||||
return err
|
||||
}
|
||||
resetBatch()
|
||||
}
|
||||
}
|
||||
|
||||
if batchCount > 0 {
|
||||
if err := executeUpsertBatch(sb.String(), args); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ds *Datastore) GetMDMWindowsProfilesContents(ctx context.Context, uuids []string) (map[string][]byte, error) {
|
||||
if len(uuids) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
stmt := `
|
||||
SELECT profile_uuid, syncml
|
||||
FROM mdm_windows_configuration_profiles WHERE profile_uuid IN (?)
|
||||
`
|
||||
query, args, err := sqlx.In(stmt, uuids)
|
||||
if err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "building in statement")
|
||||
}
|
||||
|
||||
var profs []struct {
|
||||
ProfileUUID string `db:"profile_uuid"`
|
||||
SyncML []byte `db:"syncml"`
|
||||
}
|
||||
if err := sqlx.SelectContext(ctx, ds.reader(ctx), &profs, query, args...); err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "running query")
|
||||
}
|
||||
|
||||
results := make(map[string][]byte)
|
||||
for _, p := range profs {
|
||||
results[p.ProfileUUID] = p.SyncML
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func (ds *Datastore) BulkDeleteMDMWindowsHostsConfigProfiles(ctx context.Context, profs []*fleet.MDMWindowsProfilePayload) error {
|
||||
return ds.withTx(ctx, func(tx sqlx.ExtContext) error {
|
||||
return bulkDeleteMDMWindowsHostsConfigProfilesDB(ctx, tx, profs)
|
||||
})
|
||||
}
|
||||
|
||||
func bulkDeleteMDMWindowsHostsConfigProfilesDB(ctx context.Context, tx sqlx.ExtContext, profs []*fleet.MDMWindowsProfilePayload) error {
|
||||
if len(profs) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
executeDeleteBatch := func(valuePart string, args []any) error {
|
||||
stmt := fmt.Sprintf(`DELETE FROM host_mdm_windows_profiles WHERE (profile_uuid, host_uuid) IN (%s)`, strings.TrimSuffix(valuePart, ","))
|
||||
if _, err := tx.ExecContext(ctx, stmt, args...); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "error deleting host_mdm_windows_profiles")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var (
|
||||
args []any
|
||||
sb strings.Builder
|
||||
batchCount int
|
||||
)
|
||||
|
||||
const defaultBatchSize = 1000 // results in this times 2 placeholders
|
||||
batchSize := defaultBatchSize
|
||||
if testDeleteMDMProfilesBatchSize > 0 {
|
||||
batchSize = testDeleteMDMProfilesBatchSize
|
||||
}
|
||||
|
||||
resetBatch := func() {
|
||||
batchCount = 0
|
||||
args = args[:0]
|
||||
sb.Reset()
|
||||
}
|
||||
|
||||
for _, p := range profs {
|
||||
args = append(args, p.ProfileUUID, p.HostUUID)
|
||||
sb.WriteString("(?, ?),")
|
||||
batchCount++
|
||||
|
||||
if batchCount >= batchSize {
|
||||
if err := executeDeleteBatch(sb.String(), args); err != nil {
|
||||
return err
|
||||
}
|
||||
resetBatch()
|
||||
}
|
||||
}
|
||||
|
||||
if batchCount > 0 {
|
||||
if err := executeDeleteBatch(sb.String(), args); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -26,6 +26,11 @@ func TestMDMWindows(t *testing.T) {
|
||||
{"TestMDMWindowsInsertCommandForHosts", testMDMWindowsInsertCommandForHosts},
|
||||
{"TestMDMWindowsGetPendingCommands", testMDMWindowsGetPendingCommands},
|
||||
{"TestMDMWindowsCommandResults", testMDMWindowsCommandResults},
|
||||
{"TestMDMWindowsProfileManagement", testMDMWindowsProfileManagement},
|
||||
{"TestBulkOperationsMDMWindowsHostProfiles", testBulkOperationsMDMWindowsHostProfiles},
|
||||
{"TestBulkOperationsMDMWindowsHostProfilesBatch2", testBulkOperationsMDMWindowsHostProfilesBatch2},
|
||||
{"TestBulkOperationsMDMWindowsHostProfilesBatch3", testBulkOperationsMDMWindowsHostProfilesBatch3},
|
||||
{"TestGetMDMWindowsProfilesContents", testGetMDMWindowsProfilesContents},
|
||||
{"TestMDMWindowsConfigProfiles", testMDMWindowsConfigProfiles},
|
||||
}
|
||||
|
||||
@@ -668,6 +673,468 @@ func testMDMWindowsCommandResults(t *testing.T, ds *Datastore) {
|
||||
require.Empty(t, results)
|
||||
}
|
||||
|
||||
func windowsEnroll(t *testing.T, ds fleet.Datastore, h *fleet.Host) {
|
||||
ctx := context.Background()
|
||||
d1 := &fleet.MDMWindowsEnrolledDevice{
|
||||
MDMDeviceID: uuid.New().String(),
|
||||
MDMHardwareID: uuid.New().String() + uuid.New().String(),
|
||||
MDMDeviceState: uuid.New().String(),
|
||||
MDMDeviceType: "CIMClient_Windows",
|
||||
MDMDeviceName: "DESKTOP-1C3ARC1",
|
||||
MDMEnrollType: "ProgrammaticEnrollment",
|
||||
MDMEnrollUserID: "",
|
||||
MDMEnrollProtoVersion: "5.0",
|
||||
MDMEnrollClientVersion: "10.0.19045.2965",
|
||||
MDMNotInOOBE: false,
|
||||
HostUUID: h.UUID,
|
||||
}
|
||||
err := ds.MDMWindowsInsertEnrolledDevice(ctx, d1)
|
||||
require.NoError(t, err)
|
||||
err = ds.UpdateMDMWindowsEnrollmentsHostUUID(ctx, d1.HostUUID, d1.MDMDeviceID)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
func testMDMWindowsProfileManagement(t *testing.T, ds *Datastore) {
|
||||
ctx := context.Background()
|
||||
|
||||
globalProfiles := []string{
|
||||
InsertWindowsProfileForTest(t, ds, 0),
|
||||
InsertWindowsProfileForTest(t, ds, 0),
|
||||
InsertWindowsProfileForTest(t, ds, 0),
|
||||
}
|
||||
|
||||
// if there are no hosts, then no profiles need to be installed
|
||||
profiles, err := ds.ListMDMWindowsProfilesToInstall(ctx)
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, profiles)
|
||||
|
||||
host1, err := ds.NewHost(ctx, &fleet.Host{
|
||||
Hostname: "test-host1-name",
|
||||
OsqueryHostID: ptr.String("1337"),
|
||||
NodeKey: ptr.String("1337"),
|
||||
UUID: "test-uuid-1",
|
||||
TeamID: nil,
|
||||
Platform: "windows",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
windowsEnroll(t, ds, host1)
|
||||
|
||||
// non-Windows hosts shouldn't modify any of the results below
|
||||
_, err = ds.NewHost(ctx, &fleet.Host{
|
||||
Hostname: "test-macos-host",
|
||||
OsqueryHostID: ptr.String("4824"),
|
||||
NodeKey: ptr.String("4824"),
|
||||
UUID: "test-macos-host",
|
||||
TeamID: nil,
|
||||
Platform: "macos",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// a windows host that's not MDM enrolled into Fleet shouldn't
|
||||
// modify any of the results below
|
||||
_, err = ds.NewHost(ctx, &fleet.Host{
|
||||
Hostname: "test-non-mdm-host",
|
||||
OsqueryHostID: ptr.String("4825"),
|
||||
NodeKey: ptr.String("4825"),
|
||||
UUID: "test-non-mdm-host",
|
||||
TeamID: nil,
|
||||
Platform: "windows",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
profilesMatch := func(t *testing.T, want []string, profs []*fleet.MDMWindowsProfilePayload) {
|
||||
got := []string{}
|
||||
for _, prof := range profs {
|
||||
got = append(got, prof.ProfileUUID)
|
||||
}
|
||||
require.ElementsMatch(t, want, got)
|
||||
}
|
||||
|
||||
// global profiles to install on the newly added host
|
||||
profiles, err = ds.ListMDMWindowsProfilesToInstall(ctx)
|
||||
require.NoError(t, err)
|
||||
profilesMatch(t, globalProfiles, profiles)
|
||||
|
||||
// add another host, it belongs to a team
|
||||
team, err := ds.NewTeam(ctx, &fleet.Team{Name: "test team"})
|
||||
require.NoError(t, err)
|
||||
host2, err := ds.NewHost(ctx, &fleet.Host{
|
||||
Hostname: "test-host2-name",
|
||||
OsqueryHostID: ptr.String("1338"),
|
||||
NodeKey: ptr.String("1338"),
|
||||
UUID: "test-uuid-2",
|
||||
TeamID: &team.ID,
|
||||
Platform: "windows",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
windowsEnroll(t, ds, host2)
|
||||
|
||||
// still the same profiles to assign as there are no profiles for team 1
|
||||
profiles, err = ds.ListMDMWindowsProfilesToInstall(ctx)
|
||||
require.NoError(t, err)
|
||||
profilesMatch(t, globalProfiles, profiles)
|
||||
|
||||
// assign profiles to team 1
|
||||
teamProfiles := []string{
|
||||
InsertWindowsProfileForTest(t, ds, team.ID),
|
||||
InsertWindowsProfileForTest(t, ds, team.ID),
|
||||
}
|
||||
|
||||
// new profiles, this time for the new host belonging to team 1
|
||||
profiles, err = ds.ListMDMWindowsProfilesToInstall(ctx)
|
||||
require.NoError(t, err)
|
||||
profilesMatch(t, append(globalProfiles, teamProfiles...), profiles)
|
||||
|
||||
// add another global host
|
||||
host3, err := ds.NewHost(ctx, &fleet.Host{
|
||||
Hostname: "test-host3-name",
|
||||
OsqueryHostID: ptr.String("1339"),
|
||||
NodeKey: ptr.String("1339"),
|
||||
UUID: "test-uuid-3",
|
||||
TeamID: nil,
|
||||
Platform: "windows",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
windowsEnroll(t, ds, host3)
|
||||
|
||||
// more profiles, this time for both global hosts and the team
|
||||
profiles, err = ds.ListMDMWindowsProfilesToInstall(ctx)
|
||||
require.NoError(t, err)
|
||||
profilesMatch(t, append(globalProfiles, append(globalProfiles, teamProfiles...)...), profiles)
|
||||
|
||||
// cron runs and updates the status
|
||||
err = ds.BulkUpsertMDMWindowsHostProfiles(
|
||||
ctx, []*fleet.MDMWindowsBulkUpsertHostProfilePayload{
|
||||
{
|
||||
ProfileUUID: globalProfiles[0],
|
||||
ProfileName: "foo",
|
||||
HostUUID: "test-uuid-1",
|
||||
Status: &fleet.MDMDeliveryVerifying,
|
||||
OperationType: fleet.MDMOperationTypeInstall,
|
||||
CommandUUID: "command-uuid",
|
||||
},
|
||||
{
|
||||
ProfileUUID: globalProfiles[0],
|
||||
ProfileName: "foo",
|
||||
HostUUID: "test-uuid-3",
|
||||
Status: &fleet.MDMDeliveryVerifying,
|
||||
OperationType: fleet.MDMOperationTypeInstall,
|
||||
CommandUUID: "command-uuid",
|
||||
},
|
||||
{
|
||||
ProfileUUID: globalProfiles[1],
|
||||
ProfileName: "foo",
|
||||
HostUUID: "test-uuid-1",
|
||||
Status: &fleet.MDMDeliveryVerifying,
|
||||
OperationType: fleet.MDMOperationTypeInstall,
|
||||
CommandUUID: "command-uuid",
|
||||
},
|
||||
{
|
||||
ProfileUUID: globalProfiles[1],
|
||||
ProfileName: "foo",
|
||||
HostUUID: "test-uuid-3",
|
||||
Status: &fleet.MDMDeliveryVerifying,
|
||||
OperationType: fleet.MDMOperationTypeInstall,
|
||||
CommandUUID: "command-uuid",
|
||||
},
|
||||
{
|
||||
ProfileUUID: globalProfiles[2],
|
||||
ProfileName: "foo",
|
||||
HostUUID: "test-uuid-1",
|
||||
Status: &fleet.MDMDeliveryVerifying,
|
||||
OperationType: fleet.MDMOperationTypeInstall,
|
||||
CommandUUID: "command-uuid",
|
||||
},
|
||||
{
|
||||
ProfileUUID: globalProfiles[2],
|
||||
ProfileName: "foo",
|
||||
HostUUID: "test-uuid-3",
|
||||
Status: &fleet.MDMDeliveryVerifying,
|
||||
OperationType: fleet.MDMOperationTypeInstall,
|
||||
CommandUUID: "command-uuid",
|
||||
},
|
||||
{
|
||||
ProfileUUID: teamProfiles[0],
|
||||
ProfileName: "foo",
|
||||
HostUUID: "test-uuid-2",
|
||||
Status: &fleet.MDMDeliveryVerifying,
|
||||
OperationType: fleet.MDMOperationTypeInstall,
|
||||
CommandUUID: "command-uuid",
|
||||
},
|
||||
{
|
||||
ProfileUUID: teamProfiles[1],
|
||||
ProfileName: "foo",
|
||||
HostUUID: "test-uuid-2",
|
||||
Status: &fleet.MDMDeliveryVerifying,
|
||||
OperationType: fleet.MDMOperationTypeInstall,
|
||||
CommandUUID: "command-uuid",
|
||||
},
|
||||
},
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
// no profiles left to install
|
||||
profiles, err = ds.ListMDMWindowsProfilesToInstall(ctx)
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, profiles)
|
||||
|
||||
// no profiles to remove yet
|
||||
toRemove, err := ds.ListMDMWindowsProfilesToRemove(ctx)
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, toRemove)
|
||||
|
||||
// add host1 to team
|
||||
err = ds.AddHostsToTeam(ctx, &team.ID, []uint{host1.ID})
|
||||
require.NoError(t, err)
|
||||
|
||||
// profiles to be added for host1 are now related to the team
|
||||
profiles, err = ds.ListMDMWindowsProfilesToInstall(ctx)
|
||||
require.NoError(t, err)
|
||||
profilesMatch(t, teamProfiles, profiles)
|
||||
|
||||
// profiles to be removed includes host1's old profiles
|
||||
toRemove, err = ds.ListMDMWindowsProfilesToRemove(ctx)
|
||||
require.NoError(t, err)
|
||||
profilesMatch(t, globalProfiles, toRemove)
|
||||
}
|
||||
|
||||
func testBulkOperationsMDMWindowsHostProfiles(t *testing.T, ds *Datastore) {
|
||||
ctx := context.Background()
|
||||
|
||||
profiles := []string{
|
||||
InsertWindowsProfileForTest(t, ds, 0),
|
||||
InsertWindowsProfileForTest(t, ds, 0),
|
||||
InsertWindowsProfileForTest(t, ds, 0),
|
||||
InsertWindowsProfileForTest(t, ds, 0),
|
||||
InsertWindowsProfileForTest(t, ds, 0),
|
||||
}
|
||||
|
||||
getAllHostProfiles := func() []*fleet.MDMWindowsProfilePayload {
|
||||
var hostProfiles []*fleet.MDMWindowsProfilePayload
|
||||
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
|
||||
stmt := `SELECT profile_uuid, status, operation_type FROM host_mdm_windows_profiles ORDER BY profile_name ASC`
|
||||
return sqlx.SelectContext(ctx, q, &hostProfiles, stmt)
|
||||
})
|
||||
return hostProfiles
|
||||
}
|
||||
|
||||
// empty payloads is a noop
|
||||
err := ds.BulkUpsertMDMWindowsHostProfiles(ctx, []*fleet.MDMWindowsBulkUpsertHostProfilePayload{})
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, getAllHostProfiles())
|
||||
|
||||
// valid payload inserts new records
|
||||
err = ds.BulkUpsertMDMWindowsHostProfiles(
|
||||
ctx, []*fleet.MDMWindowsBulkUpsertHostProfilePayload{
|
||||
{
|
||||
ProfileUUID: profiles[0],
|
||||
ProfileName: "A",
|
||||
HostUUID: "test-uuid-1",
|
||||
Status: &fleet.MDMDeliveryVerifying,
|
||||
OperationType: fleet.MDMOperationTypeInstall,
|
||||
CommandUUID: "command-uuid",
|
||||
},
|
||||
{
|
||||
ProfileUUID: profiles[1],
|
||||
ProfileName: "B",
|
||||
HostUUID: "test-uuid-3",
|
||||
Status: &fleet.MDMDeliveryVerifying,
|
||||
OperationType: fleet.MDMOperationTypeInstall,
|
||||
CommandUUID: "command-uuid",
|
||||
},
|
||||
{
|
||||
ProfileUUID: profiles[2],
|
||||
ProfileName: "C",
|
||||
HostUUID: "test-uuid-1",
|
||||
Status: &fleet.MDMDeliveryVerifying,
|
||||
OperationType: fleet.MDMOperationTypeInstall,
|
||||
CommandUUID: "command-uuid",
|
||||
},
|
||||
{
|
||||
ProfileUUID: profiles[3],
|
||||
ProfileName: "D",
|
||||
HostUUID: "test-uuid-1",
|
||||
Status: &fleet.MDMDeliveryVerifying,
|
||||
OperationType: fleet.MDMOperationTypeInstall,
|
||||
CommandUUID: "command-uuid",
|
||||
},
|
||||
{
|
||||
ProfileUUID: profiles[4],
|
||||
ProfileName: "E",
|
||||
HostUUID: "test-uuid-1",
|
||||
Status: &fleet.MDMDeliveryVerifying,
|
||||
OperationType: fleet.MDMOperationTypeInstall,
|
||||
CommandUUID: "command-uuid",
|
||||
},
|
||||
},
|
||||
)
|
||||
require.NoError(t, err)
|
||||
hostsProfs := getAllHostProfiles()
|
||||
require.Len(t, hostsProfs, 5)
|
||||
for i, p := range hostsProfs {
|
||||
require.Equal(t, profiles[i], p.ProfileUUID)
|
||||
require.Equal(t, fleet.MDMOperationTypeInstall, p.OperationType)
|
||||
require.Equal(t, &fleet.MDMDeliveryVerifying, p.Status)
|
||||
}
|
||||
|
||||
// valid payload updates existing records
|
||||
err = ds.BulkUpsertMDMWindowsHostProfiles(
|
||||
ctx, []*fleet.MDMWindowsBulkUpsertHostProfilePayload{
|
||||
{
|
||||
ProfileUUID: profiles[0],
|
||||
ProfileName: "A",
|
||||
HostUUID: "test-uuid-1",
|
||||
Status: &fleet.MDMDeliveryVerified,
|
||||
OperationType: fleet.MDMOperationTypeInstall,
|
||||
CommandUUID: "command-uuid",
|
||||
},
|
||||
{
|
||||
ProfileUUID: profiles[1],
|
||||
ProfileName: "B",
|
||||
HostUUID: "test-uuid-3",
|
||||
Status: &fleet.MDMDeliveryVerified,
|
||||
OperationType: fleet.MDMOperationTypeInstall,
|
||||
CommandUUID: "command-uuid",
|
||||
},
|
||||
{
|
||||
ProfileUUID: profiles[2],
|
||||
ProfileName: "C",
|
||||
HostUUID: "test-uuid-1",
|
||||
Status: &fleet.MDMDeliveryVerified,
|
||||
OperationType: fleet.MDMOperationTypeInstall,
|
||||
CommandUUID: "command-uuid",
|
||||
},
|
||||
{
|
||||
ProfileUUID: profiles[3],
|
||||
ProfileName: "D",
|
||||
HostUUID: "test-uuid-1",
|
||||
Status: &fleet.MDMDeliveryVerified,
|
||||
OperationType: fleet.MDMOperationTypeInstall,
|
||||
CommandUUID: "command-uuid",
|
||||
},
|
||||
{
|
||||
ProfileUUID: profiles[4],
|
||||
ProfileName: "E",
|
||||
HostUUID: "test-uuid-1",
|
||||
Status: &fleet.MDMDeliveryVerified,
|
||||
OperationType: fleet.MDMOperationTypeInstall,
|
||||
CommandUUID: "command-uuid",
|
||||
},
|
||||
},
|
||||
)
|
||||
require.NoError(t, err)
|
||||
hostsProfs = getAllHostProfiles()
|
||||
require.Len(t, hostsProfs, 5)
|
||||
for i, p := range hostsProfs {
|
||||
require.Equal(t, profiles[i], p.ProfileUUID)
|
||||
require.Equal(t, fleet.MDMOperationTypeInstall, p.OperationType)
|
||||
require.Equal(t, &fleet.MDMDeliveryVerified, p.Status)
|
||||
}
|
||||
|
||||
// empty payload
|
||||
err = ds.BulkDeleteMDMWindowsHostsConfigProfiles(ctx, []*fleet.MDMWindowsProfilePayload{})
|
||||
require.NoError(t, err)
|
||||
hostsProfs = getAllHostProfiles()
|
||||
require.Len(t, hostsProfs, 5)
|
||||
|
||||
// partial deletes
|
||||
err = ds.BulkDeleteMDMWindowsHostsConfigProfiles(ctx, []*fleet.MDMWindowsProfilePayload{
|
||||
{
|
||||
ProfileUUID: profiles[0],
|
||||
HostUUID: "test-uuid-1",
|
||||
},
|
||||
{
|
||||
ProfileUUID: profiles[1],
|
||||
HostUUID: "test-uuid-3",
|
||||
},
|
||||
{
|
||||
ProfileUUID: profiles[2],
|
||||
HostUUID: "test-uuid-1",
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
hostsProfs = getAllHostProfiles()
|
||||
require.Len(t, hostsProfs, 2)
|
||||
|
||||
// full deletes
|
||||
err = ds.BulkDeleteMDMWindowsHostsConfigProfiles(ctx, []*fleet.MDMWindowsProfilePayload{
|
||||
{
|
||||
ProfileUUID: profiles[0],
|
||||
HostUUID: "test-uuid-1",
|
||||
},
|
||||
{
|
||||
ProfileUUID: profiles[1],
|
||||
HostUUID: "test-uuid-3",
|
||||
},
|
||||
{
|
||||
ProfileUUID: profiles[2],
|
||||
HostUUID: "test-uuid-1",
|
||||
},
|
||||
{
|
||||
ProfileUUID: profiles[3],
|
||||
HostUUID: "test-uuid-1",
|
||||
},
|
||||
{
|
||||
ProfileUUID: profiles[4],
|
||||
HostUUID: "test-uuid-1",
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
hostsProfs = getAllHostProfiles()
|
||||
require.Len(t, hostsProfs, 0)
|
||||
}
|
||||
|
||||
func testBulkOperationsMDMWindowsHostProfilesBatch2(t *testing.T, ds *Datastore) {
|
||||
testUpsertMDMDesiredProfilesBatchSize = 2
|
||||
testDeleteMDMProfilesBatchSize = 2
|
||||
t.Cleanup(func() {
|
||||
testUpsertMDMDesiredProfilesBatchSize = 0
|
||||
testDeleteMDMProfilesBatchSize = 0
|
||||
})
|
||||
testBulkOperationsMDMWindowsHostProfiles(t, ds)
|
||||
}
|
||||
|
||||
func testBulkOperationsMDMWindowsHostProfilesBatch3(t *testing.T, ds *Datastore) {
|
||||
testUpsertMDMDesiredProfilesBatchSize = 3
|
||||
testDeleteMDMProfilesBatchSize = 3
|
||||
t.Cleanup(func() {
|
||||
testUpsertMDMDesiredProfilesBatchSize = 0
|
||||
testDeleteMDMProfilesBatchSize = 0
|
||||
})
|
||||
testBulkOperationsMDMWindowsHostProfiles(t, ds)
|
||||
}
|
||||
|
||||
func testGetMDMWindowsProfilesContents(t *testing.T, ds *Datastore) {
|
||||
ctx := context.Background()
|
||||
profileUUIDs := []string{
|
||||
InsertWindowsProfileForTest(t, ds, 0),
|
||||
InsertWindowsProfileForTest(t, ds, 0),
|
||||
InsertWindowsProfileForTest(t, ds, 0),
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
ids []string
|
||||
want map[string][]byte
|
||||
}{
|
||||
{[]string{}, nil},
|
||||
{nil, nil},
|
||||
{[]string{profileUUIDs[0]}, map[string][]byte{profileUUIDs[0]: generateDummyWindowsProfile(profileUUIDs[0])}},
|
||||
{
|
||||
[]string{profileUUIDs[0], profileUUIDs[1], profileUUIDs[2]},
|
||||
map[string][]byte{
|
||||
profileUUIDs[0]: generateDummyWindowsProfile(profileUUIDs[0]),
|
||||
profileUUIDs[1]: generateDummyWindowsProfile(profileUUIDs[1]),
|
||||
profileUUIDs[2]: generateDummyWindowsProfile(profileUUIDs[2]),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
out, err := ds.GetMDMWindowsProfilesContents(ctx, c.ids)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, c.want, out)
|
||||
}
|
||||
}
|
||||
|
||||
func testMDMWindowsConfigProfiles(t *testing.T, ds *Datastore) {
|
||||
ctx := context.Background()
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
"github.com/WatchBeam/clock"
|
||||
"github.com/fleetdm/fleet/v4/server/config"
|
||||
"github.com/go-kit/kit/log"
|
||||
"github.com/google/uuid"
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@@ -419,3 +420,19 @@ func DumpTable(t *testing.T, q sqlx.QueryerContext, tableName string) { //nolint
|
||||
require.NoError(t, rows.Err())
|
||||
t.Logf("<< dumping table %s completed", tableName)
|
||||
}
|
||||
|
||||
func generateDummyWindowsProfile(uuid string) []byte {
|
||||
return []byte(fmt.Sprintf(`<Replace><Target><LocUri>./Device/Foo/%s</LocUri></Target></Replace>`, uuid))
|
||||
}
|
||||
|
||||
// TODO(roberto): update when we have datastore functions and API methods for this
|
||||
func InsertWindowsProfileForTest(t *testing.T, ds *Datastore, teamID uint) string {
|
||||
profUUID := uuid.NewString()
|
||||
prof := generateDummyWindowsProfile(profUUID)
|
||||
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
|
||||
stmt := `INSERT INTO mdm_windows_configuration_profiles (profile_uuid, team_id, name, syncml) VALUES (?, ?, ?, ?);`
|
||||
_, err := q.ExecContext(context.Background(), stmt, profUUID, teamID, fmt.Sprintf("name-%s", uuid.NewString()), prof)
|
||||
return err
|
||||
})
|
||||
return profUUID
|
||||
}
|
||||
|
||||
@@ -1096,6 +1096,31 @@ type Datastore interface {
|
||||
// server or if disk encryption is disabled for the host's team (or no team, as applicable).
|
||||
GetMDMWindowsBitLockerStatus(ctx context.Context, host *Host) (*HostMDMDiskEncryption, error)
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// Windows MDM Profiles
|
||||
|
||||
// ListMDMWindowsProfilesToInstall returns all the profiles that should
|
||||
// be installed based on diffing the ideal state vs the state we have
|
||||
// registered in `host_mdm_windows_profiles`
|
||||
ListMDMWindowsProfilesToInstall(ctx context.Context) ([]*MDMWindowsProfilePayload, error)
|
||||
|
||||
// ListMDMWindowsProfilesToRemove returns all the profiles that should
|
||||
// be removed based on diffing the ideal state vs the state we have
|
||||
// registered in `host_mdm_apple_profiles`
|
||||
ListMDMWindowsProfilesToRemove(ctx context.Context) ([]*MDMWindowsProfilePayload, error)
|
||||
|
||||
// BulkUpsertMDMWindowsHostProfiles bulk-adds/updates records to track the
|
||||
// status of a profile in a host.
|
||||
BulkUpsertMDMWindowsHostProfiles(ctx context.Context, payload []*MDMWindowsBulkUpsertHostProfilePayload) error
|
||||
|
||||
// GetMDMWindowsProfilesContents retrieves the XML contents of the
|
||||
// profiles requested.
|
||||
GetMDMWindowsProfilesContents(ctx context.Context, profileUUIDs []string) (map[string][]byte, error)
|
||||
|
||||
// BulkDeleteMDMWindowsHostsConfigProfiles deletes entries from
|
||||
// host_mdm_windows_profiles that match the given payload.
|
||||
BulkDeleteMDMWindowsHostsConfigProfiles(ctx context.Context, payload []*MDMWindowsProfilePayload) error
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// Host Script Results
|
||||
|
||||
|
||||
@@ -28,3 +28,23 @@ type MDMWindowsConfigProfile struct {
|
||||
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
|
||||
}
|
||||
|
||||
type MDMWindowsProfilePayload struct {
|
||||
ProfileUUID string `db:"profile_uuid"`
|
||||
ProfileName string `db:"profile_name"`
|
||||
HostUUID string `db:"host_uuid"`
|
||||
Status *MDMDeliveryStatus `db:"status" json:"status"`
|
||||
OperationType MDMOperationType `db:"operation_type"`
|
||||
Detail string `db:"detail"`
|
||||
CommandUUID string `db:"command_uuid"`
|
||||
}
|
||||
|
||||
type MDMWindowsBulkUpsertHostProfilePayload struct {
|
||||
ProfileUUID string
|
||||
ProfileName string
|
||||
HostUUID string
|
||||
CommandUUID string
|
||||
OperationType MDMOperationType
|
||||
Status *MDMDeliveryStatus
|
||||
Detail string
|
||||
}
|
||||
|
||||
@@ -710,6 +710,16 @@ type GetMDMWindowsBitLockerSummaryFunc func(ctx context.Context, teamID *uint) (
|
||||
|
||||
type GetMDMWindowsBitLockerStatusFunc func(ctx context.Context, host *fleet.Host) (*fleet.HostMDMDiskEncryption, error)
|
||||
|
||||
type ListMDMWindowsProfilesToInstallFunc func(ctx context.Context) ([]*fleet.MDMWindowsProfilePayload, error)
|
||||
|
||||
type ListMDMWindowsProfilesToRemoveFunc func(ctx context.Context) ([]*fleet.MDMWindowsProfilePayload, error)
|
||||
|
||||
type BulkUpsertMDMWindowsHostProfilesFunc func(ctx context.Context, payload []*fleet.MDMWindowsBulkUpsertHostProfilePayload) error
|
||||
|
||||
type GetMDMWindowsProfilesContentsFunc func(ctx context.Context, profileUUIDs []string) (map[string][]byte, error)
|
||||
|
||||
type BulkDeleteMDMWindowsHostsConfigProfilesFunc func(ctx context.Context, payload []*fleet.MDMWindowsProfilePayload) error
|
||||
|
||||
type NewHostScriptExecutionRequestFunc func(ctx context.Context, request *fleet.HostScriptRequestPayload) (*fleet.HostScriptResult, error)
|
||||
|
||||
type SetHostScriptExecutionResultFunc func(ctx context.Context, result *fleet.HostScriptResultPayload) error
|
||||
@@ -1771,6 +1781,21 @@ type DataStore struct {
|
||||
GetMDMWindowsBitLockerStatusFunc GetMDMWindowsBitLockerStatusFunc
|
||||
GetMDMWindowsBitLockerStatusFuncInvoked bool
|
||||
|
||||
ListMDMWindowsProfilesToInstallFunc ListMDMWindowsProfilesToInstallFunc
|
||||
ListMDMWindowsProfilesToInstallFuncInvoked bool
|
||||
|
||||
ListMDMWindowsProfilesToRemoveFunc ListMDMWindowsProfilesToRemoveFunc
|
||||
ListMDMWindowsProfilesToRemoveFuncInvoked bool
|
||||
|
||||
BulkUpsertMDMWindowsHostProfilesFunc BulkUpsertMDMWindowsHostProfilesFunc
|
||||
BulkUpsertMDMWindowsHostProfilesFuncInvoked bool
|
||||
|
||||
GetMDMWindowsProfilesContentsFunc GetMDMWindowsProfilesContentsFunc
|
||||
GetMDMWindowsProfilesContentsFuncInvoked bool
|
||||
|
||||
BulkDeleteMDMWindowsHostsConfigProfilesFunc BulkDeleteMDMWindowsHostsConfigProfilesFunc
|
||||
BulkDeleteMDMWindowsHostsConfigProfilesFuncInvoked bool
|
||||
|
||||
NewHostScriptExecutionRequestFunc NewHostScriptExecutionRequestFunc
|
||||
NewHostScriptExecutionRequestFuncInvoked bool
|
||||
|
||||
@@ -4229,6 +4254,41 @@ func (s *DataStore) GetMDMWindowsBitLockerStatus(ctx context.Context, host *flee
|
||||
return s.GetMDMWindowsBitLockerStatusFunc(ctx, host)
|
||||
}
|
||||
|
||||
func (s *DataStore) ListMDMWindowsProfilesToInstall(ctx context.Context) ([]*fleet.MDMWindowsProfilePayload, error) {
|
||||
s.mu.Lock()
|
||||
s.ListMDMWindowsProfilesToInstallFuncInvoked = true
|
||||
s.mu.Unlock()
|
||||
return s.ListMDMWindowsProfilesToInstallFunc(ctx)
|
||||
}
|
||||
|
||||
func (s *DataStore) ListMDMWindowsProfilesToRemove(ctx context.Context) ([]*fleet.MDMWindowsProfilePayload, error) {
|
||||
s.mu.Lock()
|
||||
s.ListMDMWindowsProfilesToRemoveFuncInvoked = true
|
||||
s.mu.Unlock()
|
||||
return s.ListMDMWindowsProfilesToRemoveFunc(ctx)
|
||||
}
|
||||
|
||||
func (s *DataStore) BulkUpsertMDMWindowsHostProfiles(ctx context.Context, payload []*fleet.MDMWindowsBulkUpsertHostProfilePayload) error {
|
||||
s.mu.Lock()
|
||||
s.BulkUpsertMDMWindowsHostProfilesFuncInvoked = true
|
||||
s.mu.Unlock()
|
||||
return s.BulkUpsertMDMWindowsHostProfilesFunc(ctx, payload)
|
||||
}
|
||||
|
||||
func (s *DataStore) GetMDMWindowsProfilesContents(ctx context.Context, profileUUIDs []string) (map[string][]byte, error) {
|
||||
s.mu.Lock()
|
||||
s.GetMDMWindowsProfilesContentsFuncInvoked = true
|
||||
s.mu.Unlock()
|
||||
return s.GetMDMWindowsProfilesContentsFunc(ctx, profileUUIDs)
|
||||
}
|
||||
|
||||
func (s *DataStore) BulkDeleteMDMWindowsHostsConfigProfiles(ctx context.Context, payload []*fleet.MDMWindowsProfilePayload) error {
|
||||
s.mu.Lock()
|
||||
s.BulkDeleteMDMWindowsHostsConfigProfilesFuncInvoked = true
|
||||
s.mu.Unlock()
|
||||
return s.BulkDeleteMDMWindowsHostsConfigProfilesFunc(ctx, payload)
|
||||
}
|
||||
|
||||
func (s *DataStore) NewHostScriptExecutionRequest(ctx context.Context, request *fleet.HostScriptRequestPayload) (*fleet.HostScriptResult, error) {
|
||||
s.mu.Lock()
|
||||
s.NewHostScriptExecutionRequestFuncInvoked = true
|
||||
|
||||
@@ -2416,7 +2416,7 @@ func ensureFleetdConfig(ctx context.Context, ds fleet.Datastore, logger kitlog.L
|
||||
return nil
|
||||
}
|
||||
|
||||
func ReconcileProfiles(
|
||||
func ReconcileAppleProfiles(
|
||||
ctx context.Context,
|
||||
ds fleet.Datastore,
|
||||
commander *apple_mdm.MDMAppleCommander,
|
||||
|
||||
@@ -1990,7 +1990,7 @@ func TestMDMAppleCommander(t *testing.T) {
|
||||
mdmStorage.RetrievePushInfoFuncInvoked = false
|
||||
}
|
||||
|
||||
func TestMDMAppleReconcileProfiles(t *testing.T) {
|
||||
func TestMDMAppleReconcileAppleProfiles(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
mdmStorage := &nanomdm_mock.Storage{}
|
||||
ds := new(mock.Store)
|
||||
@@ -2210,7 +2210,7 @@ func TestMDMAppleReconcileProfiles(t *testing.T) {
|
||||
failedCount++
|
||||
require.Len(t, payload, 0)
|
||||
}
|
||||
err := ReconcileProfiles(ctx, ds, cmdr, kitlog.NewNopLogger())
|
||||
err := ReconcileAppleProfiles(ctx, ds, cmdr, kitlog.NewNopLogger())
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, failedCount)
|
||||
checkAndReset(t, true, &ds.ListMDMAppleProfilesToInstallFuncInvoked)
|
||||
@@ -2246,7 +2246,7 @@ func TestMDMAppleReconcileProfiles(t *testing.T) {
|
||||
}
|
||||
|
||||
enqueueFailForOp = fleet.MDMOperationTypeRemove
|
||||
err := ReconcileProfiles(ctx, ds, cmdr, kitlog.NewNopLogger())
|
||||
err := ReconcileAppleProfiles(ctx, ds, cmdr, kitlog.NewNopLogger())
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, failedCount)
|
||||
checkAndReset(t, true, &ds.ListMDMAppleProfilesToInstallFuncInvoked)
|
||||
@@ -2299,7 +2299,7 @@ func TestMDMAppleReconcileProfiles(t *testing.T) {
|
||||
}
|
||||
|
||||
enqueueFailForOp = fleet.MDMOperationTypeInstall
|
||||
err := ReconcileProfiles(ctx, ds, cmdr, kitlog.NewNopLogger())
|
||||
err := ReconcileAppleProfiles(ctx, ds, cmdr, kitlog.NewNopLogger())
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, failedCount)
|
||||
checkAndReset(t, true, &ds.ListMDMAppleProfilesToInstallFuncInvoked)
|
||||
|
||||
@@ -21,6 +21,7 @@ import (
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -71,16 +72,16 @@ func TestIntegrationsMDM(t *testing.T) {
|
||||
type integrationMDMTestSuite struct {
|
||||
suite.Suite
|
||||
withServer
|
||||
fleetCfg config.FleetConfig
|
||||
fleetDMNextCSRStatus atomic.Value
|
||||
pushProvider *mock.APNSPushProvider
|
||||
depStorage nanodep_storage.AllStorage
|
||||
depSchedule *schedule.Schedule
|
||||
profileSchedule *schedule.Schedule
|
||||
onProfileScheduleDone func() // function called when profileSchedule.Trigger() job completed
|
||||
onDEPScheduleDone func() // function called when depSchedule.Trigger() job completed
|
||||
mdmStorage *mysql.NanoMDMStorage
|
||||
worker *worker.Worker
|
||||
fleetCfg config.FleetConfig
|
||||
fleetDMNextCSRStatus atomic.Value
|
||||
pushProvider *mock.APNSPushProvider
|
||||
depStorage nanodep_storage.AllStorage
|
||||
depSchedule *schedule.Schedule
|
||||
profileSchedule *schedule.Schedule
|
||||
onProfileJobDone func() // function called when profileSchedule.Trigger() job completed
|
||||
onDEPScheduleDone func() // function called when depSchedule.Trigger() job completed
|
||||
mdmStorage *mysql.NanoMDMStorage
|
||||
worker *worker.Worker
|
||||
}
|
||||
|
||||
func (s *integrationMDMTestSuite) SetupSuite() {
|
||||
@@ -145,7 +146,9 @@ func (s *integrationMDMTestSuite) SetupSuite() {
|
||||
if s.onDEPScheduleDone != nil {
|
||||
defer s.onDEPScheduleDone()
|
||||
}
|
||||
return fleetSyncer.RunAssigner(ctx)
|
||||
err := fleetSyncer.RunAssigner(ctx)
|
||||
require.NoError(s.T(), err)
|
||||
return err
|
||||
}),
|
||||
)
|
||||
return depSchedule, nil
|
||||
@@ -158,11 +161,21 @@ func (s *integrationMDMTestSuite) SetupSuite() {
|
||||
profileSchedule = schedule.New(
|
||||
ctx, name, s.T().Name(), 1*time.Hour, ds, ds,
|
||||
schedule.WithLogger(logger),
|
||||
schedule.WithJob("manage_profiles", func(ctx context.Context) error {
|
||||
if s.onProfileScheduleDone != nil {
|
||||
defer s.onProfileScheduleDone()
|
||||
schedule.WithJob("manage_apple_profiles", func(ctx context.Context) error {
|
||||
if s.onProfileJobDone != nil {
|
||||
s.onProfileJobDone()
|
||||
}
|
||||
return ReconcileProfiles(ctx, ds, mdmCommander, logger)
|
||||
err := ReconcileAppleProfiles(ctx, ds, mdmCommander, logger)
|
||||
require.NoError(s.T(), err)
|
||||
return err
|
||||
}),
|
||||
schedule.WithJob("manage_windows_profiles", func(ctx context.Context) error {
|
||||
if s.onProfileJobDone != nil {
|
||||
defer s.onProfileJobDone()
|
||||
}
|
||||
err := ReconcileWindowsProfiles(ctx, ds, logger)
|
||||
require.NoError(s.T(), err)
|
||||
return err
|
||||
}),
|
||||
)
|
||||
return profileSchedule, nil
|
||||
@@ -277,15 +290,14 @@ func (s *integrationMDMTestSuite) mockDEPResponse(handler http.Handler) {
|
||||
})
|
||||
}
|
||||
|
||||
func (s *integrationMDMTestSuite) awaitTriggerProfileSchedule(t *testing.T, additionalWait time.Duration) {
|
||||
ch := make(chan struct{})
|
||||
s.onProfileScheduleDone = func() {
|
||||
close(ch)
|
||||
}
|
||||
func (s *integrationMDMTestSuite) awaitTriggerProfileSchedule(t *testing.T) {
|
||||
// two jobs running sequentially (macOS then Windows) on the same schedule
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(2)
|
||||
s.onProfileJobDone = wg.Done
|
||||
_, err := s.profileSchedule.Trigger()
|
||||
require.NoError(t, err)
|
||||
<-ch
|
||||
time.Sleep(additionalWait)
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func (s *integrationMDMTestSuite) TestGetBootstrapToken() {
|
||||
@@ -495,7 +507,7 @@ func (s *integrationMDMTestSuite) TestABMExpiredToken() {
|
||||
require.False(t, config.MDM.AppleBMTermsExpired)
|
||||
}
|
||||
|
||||
func (s *integrationMDMTestSuite) TestProfileManagement() {
|
||||
func (s *integrationMDMTestSuite) TestAppleProfileManagement() {
|
||||
t := s.T()
|
||||
ctx := context.Background()
|
||||
|
||||
@@ -556,7 +568,7 @@ func (s *integrationMDMTestSuite) TestProfileManagement() {
|
||||
setupPusher(s, t, mdmDevice)
|
||||
|
||||
// trigger a profile sync
|
||||
s.awaitTriggerProfileSchedule(t, 5*time.Second)
|
||||
s.awaitTriggerProfileSchedule(t)
|
||||
installs, removes := checkNextPayloads(t, mdmDevice, false)
|
||||
// verify that we received all profiles
|
||||
require.ElementsMatch(t, wantGlobalProfiles, installs)
|
||||
@@ -567,7 +579,7 @@ func (s *integrationMDMTestSuite) TestProfileManagement() {
|
||||
require.NoError(t, err)
|
||||
|
||||
// trigger a profile sync
|
||||
s.awaitTriggerProfileSchedule(t, 0)
|
||||
s.awaitTriggerProfileSchedule(t)
|
||||
installs, removes = checkNextPayloads(t, mdmDevice, false)
|
||||
// verify that we should install the team profile
|
||||
require.ElementsMatch(t, wantTeamProfiles, installs)
|
||||
@@ -583,7 +595,7 @@ func (s *integrationMDMTestSuite) TestProfileManagement() {
|
||||
s.Do("POST", "/api/v1/fleet/mdm/apple/profiles/batch", batchSetMDMAppleProfilesRequest{Profiles: teamProfiles}, http.StatusNoContent, "team_id", strconv.Itoa(int(tm.ID)))
|
||||
|
||||
// trigger a profile sync
|
||||
s.awaitTriggerProfileSchedule(t, 0)
|
||||
s.awaitTriggerProfileSchedule(t)
|
||||
installs, removes = checkNextPayloads(t, mdmDevice, false)
|
||||
// verify that we should install the team profiles
|
||||
require.ElementsMatch(t, wantTeamProfiles, installs)
|
||||
@@ -591,7 +603,7 @@ func (s *integrationMDMTestSuite) TestProfileManagement() {
|
||||
require.ElementsMatch(t, []string{"I3"}, removes)
|
||||
|
||||
// with no changes
|
||||
s.awaitTriggerProfileSchedule(t, 0)
|
||||
s.awaitTriggerProfileSchedule(t)
|
||||
installs, removes = checkNextPayloads(t, mdmDevice, false)
|
||||
require.Empty(t, installs)
|
||||
require.Empty(t, removes)
|
||||
@@ -708,7 +720,7 @@ func (s *integrationMDMTestSuite) TestProfileRetries() {
|
||||
setProfileUpdatedAt(t, time.Now().Add(-48*time.Hour), "I1", "I2", mobileconfig.FleetdConfigPayloadIdentifier)
|
||||
|
||||
// trigger initial profile sync and confirm that we received all profiles
|
||||
s.awaitTriggerProfileSchedule(t, 5*time.Second)
|
||||
s.awaitTriggerProfileSchedule(t)
|
||||
installs, removes := checkNextPayloads(t, mdmDevice, false)
|
||||
require.ElementsMatch(t, initialExpectedProfiles, installs)
|
||||
require.Empty(t, removes)
|
||||
@@ -726,7 +738,7 @@ func (s *integrationMDMTestSuite) TestProfileRetries() {
|
||||
checkRetryCounts(t)
|
||||
|
||||
// trigger a profile sync and confirm that the install profile command for I2 was resent
|
||||
s.awaitTriggerProfileSchedule(t, 0)
|
||||
s.awaitTriggerProfileSchedule(t)
|
||||
installs, removes = checkNextPayloads(t, mdmDevice, false)
|
||||
require.ElementsMatch(t, [][]byte{initialExpectedProfiles[1]}, installs)
|
||||
require.Empty(t, removes)
|
||||
@@ -738,7 +750,7 @@ func (s *integrationMDMTestSuite) TestProfileRetries() {
|
||||
checkRetryCounts(t) // unchanged
|
||||
|
||||
// trigger a profile sync and confirm that no profiles were sent
|
||||
s.awaitTriggerProfileSchedule(t, 0)
|
||||
s.awaitTriggerProfileSchedule(t)
|
||||
installs, removes = checkNextPayloads(t, mdmDevice, false)
|
||||
require.Empty(t, installs)
|
||||
require.Empty(t, removes)
|
||||
@@ -753,7 +765,7 @@ func (s *integrationMDMTestSuite) TestProfileRetries() {
|
||||
checkRetryCounts(t)
|
||||
|
||||
// trigger a profile sync and confirm that the install profile command for I1 was resent
|
||||
s.awaitTriggerProfileSchedule(t, 0)
|
||||
s.awaitTriggerProfileSchedule(t)
|
||||
installs, removes := checkNextPayloads(t, mdmDevice, false)
|
||||
require.ElementsMatch(t, [][]byte{initialExpectedProfiles[0]}, installs)
|
||||
require.Empty(t, removes)
|
||||
@@ -765,7 +777,7 @@ func (s *integrationMDMTestSuite) TestProfileRetries() {
|
||||
checkRetryCounts(t) // unchanged
|
||||
|
||||
// trigger a profile sync and confirm that the install profile command for I1 was not resent
|
||||
s.awaitTriggerProfileSchedule(t, 0)
|
||||
s.awaitTriggerProfileSchedule(t)
|
||||
installs, removes = checkNextPayloads(t, mdmDevice, false)
|
||||
require.Empty(t, installs)
|
||||
require.Empty(t, removes)
|
||||
@@ -780,7 +792,7 @@ func (s *integrationMDMTestSuite) TestProfileRetries() {
|
||||
|
||||
// trigger a profile sync and confirm that the install profile command for I3 was sent and
|
||||
// simulate a device error
|
||||
s.awaitTriggerProfileSchedule(t, 0)
|
||||
s.awaitTriggerProfileSchedule(t)
|
||||
installs, removes := checkNextPayloads(t, mdmDevice, true)
|
||||
require.ElementsMatch(t, [][]byte{newProfile}, installs)
|
||||
require.Empty(t, removes)
|
||||
@@ -791,7 +803,7 @@ func (s *integrationMDMTestSuite) TestProfileRetries() {
|
||||
|
||||
// trigger a profile sync and confirm that the install profile command for I3 was sent and
|
||||
// simulate a device ack
|
||||
s.awaitTriggerProfileSchedule(t, 0)
|
||||
s.awaitTriggerProfileSchedule(t)
|
||||
installs, removes = checkNextPayloads(t, mdmDevice, false)
|
||||
require.ElementsMatch(t, [][]byte{newProfile}, installs)
|
||||
require.Empty(t, removes)
|
||||
@@ -807,7 +819,7 @@ func (s *integrationMDMTestSuite) TestProfileRetries() {
|
||||
checkRetryCounts(t) // unchanged
|
||||
|
||||
// trigger a profile sync and confirm that the install profile command for I3 was not resent
|
||||
s.awaitTriggerProfileSchedule(t, 0)
|
||||
s.awaitTriggerProfileSchedule(t)
|
||||
installs, removes = checkNextPayloads(t, mdmDevice, false)
|
||||
require.Empty(t, installs)
|
||||
require.Empty(t, removes)
|
||||
@@ -822,7 +834,7 @@ func (s *integrationMDMTestSuite) TestProfileRetries() {
|
||||
|
||||
// trigger a profile sync and confirm that the install profile command for I3 was sent and
|
||||
// simulate a device error
|
||||
s.awaitTriggerProfileSchedule(t, 0)
|
||||
s.awaitTriggerProfileSchedule(t)
|
||||
installs, removes := checkNextPayloads(t, mdmDevice, true)
|
||||
require.ElementsMatch(t, [][]byte{newProfile}, installs)
|
||||
require.Empty(t, removes)
|
||||
@@ -833,7 +845,7 @@ func (s *integrationMDMTestSuite) TestProfileRetries() {
|
||||
|
||||
// trigger a profile sync and confirm that the install profile command for I4 was sent and
|
||||
// simulate a second device error
|
||||
s.awaitTriggerProfileSchedule(t, 0)
|
||||
s.awaitTriggerProfileSchedule(t)
|
||||
installs, removes = checkNextPayloads(t, mdmDevice, true)
|
||||
require.ElementsMatch(t, [][]byte{newProfile}, installs)
|
||||
require.Empty(t, removes)
|
||||
@@ -842,7 +854,7 @@ func (s *integrationMDMTestSuite) TestProfileRetries() {
|
||||
checkRetryCounts(t) // unchanged
|
||||
|
||||
// trigger a profile sync and confirm that the install profile command for I3 was not resent
|
||||
s.awaitTriggerProfileSchedule(t, 0)
|
||||
s.awaitTriggerProfileSchedule(t)
|
||||
installs, removes = checkNextPayloads(t, mdmDevice, false)
|
||||
require.Empty(t, installs)
|
||||
require.Empty(t, removes)
|
||||
@@ -858,7 +870,7 @@ func (s *integrationMDMTestSuite) TestProfileRetries() {
|
||||
|
||||
// trigger a profile sync and confirm that the install profile command for I3 was sent and
|
||||
// simulate a device error
|
||||
s.awaitTriggerProfileSchedule(t, 0)
|
||||
s.awaitTriggerProfileSchedule(t)
|
||||
installs, removes := checkNextPayloads(t, mdmDevice, true)
|
||||
require.ElementsMatch(t, [][]byte{newProfile}, installs)
|
||||
require.Empty(t, removes)
|
||||
@@ -869,7 +881,7 @@ func (s *integrationMDMTestSuite) TestProfileRetries() {
|
||||
|
||||
// trigger a profile sync and confirm that the install profile command for I5 was sent and
|
||||
// simulate a device ack
|
||||
s.awaitTriggerProfileSchedule(t, 0)
|
||||
s.awaitTriggerProfileSchedule(t)
|
||||
installs, removes = checkNextPayloads(t, mdmDevice, false)
|
||||
require.ElementsMatch(t, [][]byte{newProfile}, installs)
|
||||
require.Empty(t, removes)
|
||||
@@ -884,7 +896,7 @@ func (s *integrationMDMTestSuite) TestProfileRetries() {
|
||||
checkRetryCounts(t) // unchanged
|
||||
|
||||
// trigger a profile sync and confirm that the install profile command for I5 was not resent
|
||||
s.awaitTriggerProfileSchedule(t, 0)
|
||||
s.awaitTriggerProfileSchedule(t)
|
||||
installs, removes = checkNextPayloads(t, mdmDevice, false)
|
||||
require.Empty(t, installs)
|
||||
require.Empty(t, removes)
|
||||
@@ -897,7 +909,7 @@ func (s *integrationMDMTestSuite) TestProfileRetries() {
|
||||
checkRetryCounts(t) // unchanged
|
||||
|
||||
// trigger a profile sync and confirm that the install profile command for I5 was not resent
|
||||
s.awaitTriggerProfileSchedule(t, 0)
|
||||
s.awaitTriggerProfileSchedule(t)
|
||||
installs, removes = checkNextPayloads(t, mdmDevice, false)
|
||||
require.Empty(t, installs)
|
||||
require.Empty(t, removes)
|
||||
@@ -1103,7 +1115,7 @@ func (s *integrationMDMTestSuite) TestPuppetMatchPreassignProfiles() {
|
||||
}}, http.StatusNoContent, "team_id", fmt.Sprint(tm4.ID))
|
||||
|
||||
// trigger the schedule so profiles are set in their state
|
||||
s.awaitTriggerProfileSchedule(t, 1*time.Second)
|
||||
s.awaitTriggerProfileSchedule(t)
|
||||
|
||||
// preassign the MDM host to prof1 and prof4, should match existing team tm2
|
||||
//
|
||||
@@ -1126,7 +1138,7 @@ func (s *integrationMDMTestSuite) TestPuppetMatchPreassignProfiles() {
|
||||
// - the same as the team's and are pending
|
||||
// - prof2 + old filevault are pending removal
|
||||
// - fleetd config being reinstalled (to update the enroll secret)
|
||||
s.awaitTriggerProfileSchedule(t, 1*time.Second)
|
||||
s.awaitTriggerProfileSchedule(t)
|
||||
hostProfs, err := s.ds.GetHostMDMProfiles(ctx, mdmHost.UUID)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, hostProfs, 5)
|
||||
@@ -1182,7 +1194,7 @@ func (s *integrationMDMTestSuite) TestPuppetMatchPreassignProfiles() {
|
||||
require.Equal(t, tm2.ID, *h.TeamID)
|
||||
|
||||
// and its profiles have been left untouched
|
||||
s.awaitTriggerProfileSchedule(t, 1*time.Second)
|
||||
s.awaitTriggerProfileSchedule(t)
|
||||
hostProfs, err = s.ds.GetHostMDMProfiles(ctx, mdmHost2.UUID)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, hostProfs, 3)
|
||||
@@ -1597,6 +1609,16 @@ func createHostThenEnrollMDM(ds fleet.Datastore, fleetServerURL string, t *testi
|
||||
return fleetHost, mdmDevice
|
||||
}
|
||||
|
||||
func createWindowsHostThenEnrollMDM(ds fleet.Datastore, fleetServerURL string, t *testing.T) (*fleet.Host, *mdmtest.TestWindowsMDMClient) {
|
||||
host := createOrbitEnrolledHost(t, "windows", "h1", ds)
|
||||
mdmDevice := mdmtest.NewTestMDMClientWindowsProgramatic(fleetServerURL, *host.OrbitNodeKey)
|
||||
err := mdmDevice.Enroll()
|
||||
require.NoError(t, err)
|
||||
err = ds.UpdateMDMWindowsEnrollmentsHostUUID(context.Background(), host.UUID, mdmDevice.DeviceID)
|
||||
require.NoError(t, err)
|
||||
return host, mdmDevice
|
||||
}
|
||||
|
||||
func (s *integrationMDMTestSuite) TestDEPProfileAssignment() {
|
||||
t := s.T()
|
||||
|
||||
@@ -1622,11 +1644,7 @@ func (s *integrationMDMTestSuite) TestDEPProfileAssignment() {
|
||||
// run the worker to process the DEP enroll request
|
||||
s.runWorker()
|
||||
// run the worker to assign configuration profiles
|
||||
ch := make(chan bool)
|
||||
s.onProfileScheduleDone = func() { close(ch) }
|
||||
_, err := s.profileSchedule.Trigger()
|
||||
require.NoError(t, err)
|
||||
<-ch
|
||||
s.awaitTriggerProfileSchedule(t)
|
||||
|
||||
var fleetdCmd, installProfileCmd *micromdm.CommandPayload
|
||||
cmd, err := mdmDevice.Idle()
|
||||
@@ -2185,8 +2203,7 @@ func (s *integrationMDMTestSuite) TestMDMAppleUnenroll() {
|
||||
}}, http.StatusNoContent)
|
||||
|
||||
// trigger a sync and verify that there are profiles assigned to the host
|
||||
_, err = s.profileSchedule.Trigger()
|
||||
require.NoError(t, err)
|
||||
s.awaitTriggerProfileSchedule(t)
|
||||
|
||||
var hostResp getHostResponse
|
||||
s.DoJSON("GET", fmt.Sprintf("/api/v1/fleet/hosts/%d", h.ID), getHostRequest{}, http.StatusOK, &hostResp)
|
||||
@@ -3870,11 +3887,7 @@ func (s *integrationMDMTestSuite) TestHostMDMProfilesStatus() {
|
||||
}
|
||||
|
||||
triggerReconcileProfiles := func() {
|
||||
ch := make(chan bool)
|
||||
s.onProfileScheduleDone = func() { close(ch) }
|
||||
_, err := s.profileSchedule.Trigger()
|
||||
require.NoError(t, err)
|
||||
<-ch
|
||||
s.awaitTriggerProfileSchedule(t)
|
||||
// this will only mark them as "pending", as the response to confirm
|
||||
// profile deployment is asynchronous, so we simulate it here by
|
||||
// updating any "pending" (not NULL) profiles to "verifying"
|
||||
@@ -4299,14 +4312,6 @@ func (s *integrationMDMTestSuite) TestFleetdConfiguration() {
|
||||
t := s.T()
|
||||
s.assertConfigProfilesByIdentifier(nil, mobileconfig.FleetdConfigPayloadIdentifier, false)
|
||||
|
||||
triggerSchedule := func() {
|
||||
ch := make(chan bool)
|
||||
s.onProfileScheduleDone = func() { close(ch) }
|
||||
_, err := s.profileSchedule.Trigger()
|
||||
require.NoError(t, err)
|
||||
<-ch
|
||||
}
|
||||
|
||||
var applyResp applyEnrollSecretSpecResponse
|
||||
s.DoJSON("POST", "/api/latest/fleet/spec/enroll_secret", applyEnrollSecretSpecRequest{
|
||||
Spec: &fleet.EnrollSecretSpec{
|
||||
@@ -4315,7 +4320,7 @@ func (s *integrationMDMTestSuite) TestFleetdConfiguration() {
|
||||
}, http.StatusOK, &applyResp)
|
||||
|
||||
// a new fleetd configuration profile for "no team" is created
|
||||
triggerSchedule()
|
||||
s.awaitTriggerProfileSchedule(t)
|
||||
s.assertConfigProfilesByIdentifier(nil, mobileconfig.FleetdConfigPayloadIdentifier, true)
|
||||
|
||||
// create a new team
|
||||
@@ -4335,7 +4340,7 @@ func (s *integrationMDMTestSuite) TestFleetdConfiguration() {
|
||||
}`, tm.Name)), http.StatusOK, &acResp)
|
||||
|
||||
// the team doesn't have any enroll secrets yet, a profile is created using the global enroll secret
|
||||
triggerSchedule()
|
||||
s.awaitTriggerProfileSchedule(t)
|
||||
p := s.assertConfigProfilesByIdentifier(&tm.ID, mobileconfig.FleetdConfigPayloadIdentifier, true)
|
||||
require.Contains(t, string(p.Mobileconfig), t.Name())
|
||||
|
||||
@@ -4347,7 +4352,7 @@ func (s *integrationMDMTestSuite) TestFleetdConfiguration() {
|
||||
s.Do("POST", "/api/latest/fleet/spec/teams", teamSpecs, http.StatusOK)
|
||||
|
||||
// a new fleetd configuration profile for that team is created
|
||||
triggerSchedule()
|
||||
s.awaitTriggerProfileSchedule(t)
|
||||
p = s.assertConfigProfilesByIdentifier(&tm.ID, mobileconfig.FleetdConfigPayloadIdentifier, true)
|
||||
require.Contains(t, string(p.Mobileconfig), t.Name()+"team-secret")
|
||||
|
||||
@@ -7300,12 +7305,7 @@ func (s *integrationMDMTestSuite) TestValidGetTOC() {
|
||||
|
||||
func (s *integrationMDMTestSuite) TestWindowsMDM() {
|
||||
t := s.T()
|
||||
orbitHost := createOrbitEnrolledHost(t, "windows", "h1", s.ds)
|
||||
d := mdmtest.NewTestMDMClientWindowsProgramatic(s.server.URL, *orbitHost.OrbitNodeKey)
|
||||
err := d.Enroll()
|
||||
require.NoError(t, err)
|
||||
err = s.ds.UpdateMDMWindowsEnrollmentsHostUUID(context.Background(), orbitHost.UUID, d.DeviceID)
|
||||
require.NoError(t, err)
|
||||
orbitHost, d := createWindowsHostThenEnrollMDM(s.ds, s.server.URL, t)
|
||||
|
||||
cmdOneUUID := uuid.New().String()
|
||||
commandOne := &fleet.MDMWindowsCommand{
|
||||
@@ -7327,7 +7327,7 @@ func (s *integrationMDMTestSuite) TestWindowsMDM() {
|
||||
`, cmdOneUUID)),
|
||||
TargetLocURI: "./Device/Vendor/MSFT/Reboot/RebootNow",
|
||||
}
|
||||
err = s.ds.MDMWindowsInsertCommandForHosts(context.Background(), []string{orbitHost.UUID}, commandOne)
|
||||
err := s.ds.MDMWindowsInsertCommandForHosts(context.Background(), []string{orbitHost.UUID}, commandOne)
|
||||
require.NoError(t, err)
|
||||
|
||||
cmds, err := d.StartManagementSession()
|
||||
@@ -8958,3 +8958,127 @@ func (s *integrationMDMTestSuite) newSyncMLUnenrollMsg(deviceID string, manageme
|
||||
</SyncBody>
|
||||
</SyncML>`), nil
|
||||
}
|
||||
|
||||
func (s *integrationMDMTestSuite) TestWindowsProfileManagement() {
|
||||
t := s.T()
|
||||
ctx := context.Background()
|
||||
|
||||
err := s.ds.ApplyEnrollSecrets(ctx, nil, []*fleet.EnrollSecret{{Secret: t.Name()}})
|
||||
require.NoError(t, err)
|
||||
|
||||
globalProfiles := []string{
|
||||
mysql.InsertWindowsProfileForTest(t, s.ds, 0),
|
||||
mysql.InsertWindowsProfileForTest(t, s.ds, 0),
|
||||
mysql.InsertWindowsProfileForTest(t, s.ds, 0),
|
||||
}
|
||||
|
||||
// create a new team
|
||||
tm, err := s.ds.NewTeam(ctx, &fleet.Team{Name: "batch_set_mdm_profiles"})
|
||||
require.NoError(t, err)
|
||||
teamProfiles := []string{
|
||||
mysql.InsertWindowsProfileForTest(t, s.ds, tm.ID),
|
||||
mysql.InsertWindowsProfileForTest(t, s.ds, tm.ID),
|
||||
}
|
||||
|
||||
// create a non-Windows host
|
||||
_, err = s.ds.NewHost(context.Background(), &fleet.Host{
|
||||
ID: 1,
|
||||
OsqueryHostID: ptr.String("non-windows-host"),
|
||||
NodeKey: ptr.String("non-windows-host"),
|
||||
UUID: uuid.New().String(),
|
||||
Hostname: fmt.Sprintf("%sfoo.local.non.windows", t.Name()),
|
||||
Platform: "darwin",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// create a Windows host that's not enrolled into MDM
|
||||
_, err = s.ds.NewHost(context.Background(), &fleet.Host{
|
||||
ID: 2,
|
||||
OsqueryHostID: ptr.String("not-mdm-enrolled"),
|
||||
NodeKey: ptr.String("not-mdm-enrolled"),
|
||||
UUID: uuid.New().String(),
|
||||
Hostname: fmt.Sprintf("%sfoo.local.not.enrolled", t.Name()),
|
||||
Platform: "windows",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
verifyProfiles := func(device *mdmtest.TestWindowsMDMClient, n int) {
|
||||
s.awaitTriggerProfileSchedule(t)
|
||||
cmds, err := device.StartManagementSession()
|
||||
require.NoError(t, err)
|
||||
// 2 Status + n profiles
|
||||
require.Len(t, cmds, n+2)
|
||||
|
||||
var atomicCmds []fleet.ProtoCmdOperation
|
||||
msgID, err := device.GetCurrentMsgID()
|
||||
require.NoError(t, err)
|
||||
for _, c := range cmds {
|
||||
cmdID := c.Cmd.CmdID
|
||||
if c.Verb == "Atomic" {
|
||||
atomicCmds = append(atomicCmds, c)
|
||||
}
|
||||
device.AppendResponse(fleet.SyncMLCmd{
|
||||
XMLName: xml.Name{Local: mdm_types.CmdStatus},
|
||||
MsgRef: &msgID,
|
||||
CmdRef: &cmdID,
|
||||
Cmd: ptr.String("Exec"),
|
||||
Data: ptr.String("200"),
|
||||
Items: nil,
|
||||
CmdID: uuid.NewString(),
|
||||
})
|
||||
}
|
||||
// TODO: verify profile contents as well
|
||||
require.Len(t, atomicCmds, n)
|
||||
|
||||
cmds, err = device.SendResponse()
|
||||
require.NoError(t, err)
|
||||
// the ack of the message should be the only returned command
|
||||
require.Len(t, cmds, 1)
|
||||
}
|
||||
|
||||
checkHostsProfilesMatch := func(host *fleet.Host, wantUUIDs []string) {
|
||||
var gotUUIDs []string
|
||||
mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
|
||||
stmt := `SELECT profile_uuid FROM host_mdm_windows_profiles WHERE host_uuid = ?`
|
||||
return sqlx.SelectContext(context.Background(), q, &gotUUIDs, stmt, host.UUID)
|
||||
})
|
||||
require.ElementsMatch(t, wantUUIDs, gotUUIDs)
|
||||
}
|
||||
|
||||
// Create a host and then enroll to MDM.
|
||||
host, mdmDevice := createWindowsHostThenEnrollMDM(s.ds, s.server.URL, t)
|
||||
// trigger a profile sync
|
||||
verifyProfiles(mdmDevice, 3)
|
||||
checkHostsProfilesMatch(host, globalProfiles)
|
||||
|
||||
// another sync shouldn't return profiles
|
||||
verifyProfiles(mdmDevice, 0)
|
||||
|
||||
// add the host to a team
|
||||
err = s.ds.AddHostsToTeam(ctx, &tm.ID, []uint{host.ID})
|
||||
require.NoError(t, err)
|
||||
|
||||
// trigger a profile sync, device gets the team profile
|
||||
verifyProfiles(mdmDevice, 2)
|
||||
checkHostsProfilesMatch(host, teamProfiles)
|
||||
|
||||
// set new team profiles (delete + addition)
|
||||
mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
|
||||
stmt := `DELETE FROM mdm_windows_configuration_profiles WHERE profile_uuid = ?`
|
||||
_, err := q.ExecContext(context.Background(), stmt, teamProfiles[1])
|
||||
return err
|
||||
})
|
||||
teamProfiles = []string{
|
||||
teamProfiles[0],
|
||||
mysql.InsertWindowsProfileForTest(t, s.ds, tm.ID),
|
||||
}
|
||||
|
||||
// trigger a profile sync, device gets the team profile
|
||||
verifyProfiles(mdmDevice, 1)
|
||||
|
||||
// check that we deleted the old profile in the DB
|
||||
checkHostsProfilesMatch(host, teamProfiles)
|
||||
|
||||
// another sync shouldn't return profiles
|
||||
verifyProfiles(mdmDevice, 0)
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ import (
|
||||
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
|
||||
"github.com/fleetdm/fleet/v4/server/contexts/logging"
|
||||
"github.com/fleetdm/fleet/v4/server/fleet"
|
||||
kitlog "github.com/go-kit/kit/log"
|
||||
"github.com/go-kit/log/level"
|
||||
|
||||
mdm_types "github.com/fleetdm/fleet/v4/server/fleet"
|
||||
@@ -2049,3 +2050,108 @@ func NewSyncMLCmdStatus(msgRef string, cmdRef string, cmdOrig string, statusCode
|
||||
CmdID: uuid.NewString(),
|
||||
}
|
||||
}
|
||||
|
||||
func ReconcileWindowsProfiles(ctx context.Context, ds fleet.Datastore, logger kitlog.Logger) error {
|
||||
// retrieve the profiles to install/remove.
|
||||
toInstall, err := ds.ListMDMWindowsProfilesToInstall(ctx)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "getting profiles to install")
|
||||
}
|
||||
toRemove, err := ds.ListMDMWindowsProfilesToRemove(ctx)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "getting profiles to remove")
|
||||
}
|
||||
|
||||
// toGetContents contains the IDs of all the profiles from which we
|
||||
// need to retrieve contents. Since the previous query returns one row
|
||||
// per host, it would be too expensive to retrieve the profile contents
|
||||
// there, so we make another request. Using a map to deduplicate.
|
||||
toGetContents := make(map[string]bool)
|
||||
|
||||
// hostProfiles tracks each host_mdm_windows_profile we need to upsert
|
||||
// with the new status, operation_type, etc.
|
||||
hostProfiles := make([]*fleet.MDMWindowsBulkUpsertHostProfilePayload, 0, len(toInstall))
|
||||
|
||||
// install are maps from profileID -> command uuid and host
|
||||
// UUIDs as the underlying MDM services are optimized to send one command to
|
||||
// multiple hosts at the same time. Note that the same command uuid is used
|
||||
// for all hosts in a given install/remove target operation.
|
||||
type cmdTarget struct {
|
||||
cmdUUID string
|
||||
profID string
|
||||
hostUUIDs []string
|
||||
}
|
||||
installTargets := make(map[string]*cmdTarget)
|
||||
|
||||
for _, p := range toInstall {
|
||||
toGetContents[p.ProfileUUID] = true
|
||||
target := installTargets[p.ProfileUUID]
|
||||
if target == nil {
|
||||
target = &cmdTarget{
|
||||
cmdUUID: uuid.New().String(),
|
||||
profID: p.ProfileUUID,
|
||||
}
|
||||
installTargets[p.ProfileUUID] = target
|
||||
}
|
||||
target.hostUUIDs = append(target.hostUUIDs, p.HostUUID)
|
||||
|
||||
hostProfiles = append(hostProfiles, &fleet.MDMWindowsBulkUpsertHostProfilePayload{
|
||||
ProfileUUID: p.ProfileUUID,
|
||||
HostUUID: p.HostUUID,
|
||||
ProfileName: p.ProfileName,
|
||||
CommandUUID: target.cmdUUID,
|
||||
OperationType: fleet.MDMOperationTypeInstall,
|
||||
Status: &fleet.MDMDeliveryPending,
|
||||
})
|
||||
}
|
||||
|
||||
// Grab the contents of all the profiles we need to install
|
||||
profileUUIDs := make([]string, 0, len(toGetContents))
|
||||
for pid := range toGetContents {
|
||||
profileUUIDs = append(profileUUIDs, pid)
|
||||
}
|
||||
profileContents, err := ds.GetMDMWindowsProfilesContents(ctx, profileUUIDs)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "get profile contents")
|
||||
}
|
||||
|
||||
for profID, target := range installTargets {
|
||||
p, ok := profileContents[profID]
|
||||
if !ok {
|
||||
// this should never happen
|
||||
level.Info(logger).Log("warn", "missing profile contents", "profile_id", profID)
|
||||
continue
|
||||
}
|
||||
|
||||
// TODO(roberto): I think this should live separately in the
|
||||
// Windows equivalent of Apple's Commander struct, but I'd like
|
||||
// to keep it simpler for now until we understand more.
|
||||
command := &fleet.MDMWindowsCommand{
|
||||
CommandUUID: target.cmdUUID,
|
||||
RawCommand: []byte(fmt.Sprintf(`
|
||||
<Atomic>
|
||||
<CmdID>%s</CmdID>
|
||||
%s
|
||||
</Atomic>
|
||||
`, target.cmdUUID, p)),
|
||||
// Atomic commands don't have a Target element.
|
||||
TargetLocURI: "",
|
||||
}
|
||||
if err := ds.MDMWindowsInsertCommandForHosts(ctx, target.hostUUIDs, command); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "inserting commands for hosts")
|
||||
}
|
||||
}
|
||||
|
||||
// Windows profiles are just deleted from the DB, the notion of sending
|
||||
// a command to remove a profile doesn't exist.
|
||||
if err := ds.BulkDeleteMDMWindowsHostsConfigProfiles(ctx, toRemove); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "deleting profiles that didn't change")
|
||||
}
|
||||
|
||||
// Upsert the status of the host profiles we need to track.
|
||||
if err := ds.BulkUpsertMDMWindowsHostProfiles(ctx, hostProfiles); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "updating host profiles")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user