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:
Roberto Dip
2023-11-10 11:05:10 -03:00
committed by GitHub
parent 36421bd505
commit 8478171256
15 changed files with 1167 additions and 108 deletions
+6 -3
View File
@@ -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
View File
@@ -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,
+6 -12
View File
@@ -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() {
+12 -12
View File
@@ -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)
}
+6
View File
@@ -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
+237
View File
@@ -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
View File
@@ -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
}
+25
View File
@@ -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
+20
View File
@@ -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
}
+60
View File
@@ -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
+1 -1
View File
@@ -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,
+4 -4
View File
@@ -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)
+199 -75
View File
@@ -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)
}
+106
View File
@@ -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
}