From 7964a818285702abfb5978f1efb588afce9dd013 Mon Sep 17 00:00:00 2001 From: Martin Angers Date: Mon, 8 Apr 2024 14:48:11 -0400 Subject: [PATCH] Add tests for declarations --- server/datastore/mysql/apple_mdm.go | 7 +- server/datastore/mysql/microsoft_mdm.go | 6 + server/mdm/mdm.go | 14 +- server/service/integration_mdm_test.go | 222 ++++++++++++++++++++++++ 4 files changed, 241 insertions(+), 8 deletions(-) diff --git a/server/datastore/mysql/apple_mdm.go b/server/datastore/mysql/apple_mdm.go index 0fdc2bb2c2..5bb3cdfa16 100644 --- a/server/datastore/mysql/apple_mdm.go +++ b/server/datastore/mysql/apple_mdm.go @@ -12,6 +12,7 @@ import ( "github.com/fleetdm/fleet/v4/server/contexts/ctxerr" "github.com/fleetdm/fleet/v4/server/fleet" + fleetmdm "github.com/fleetdm/fleet/v4/server/mdm" apple_mdm "github.com/fleetdm/fleet/v4/server/mdm/apple" "github.com/fleetdm/fleet/v4/server/mdm/apple/mobileconfig" "github.com/fleetdm/fleet/v4/server/mdm/nanodep/godep" @@ -3393,8 +3394,6 @@ WHERE h.uuid = ? } func (ds *Datastore) batchSetMDMAppleDeclarations(ctx context.Context, tx sqlx.ExtContext, tmID *uint, incomingDeclarations []*fleet.MDMAppleDeclaration) ([]*fleet.MDMAppleDeclaration, error) { - // TODO(mna): batch-set should not delete the reserved OS updates DDM. - const insertStmt = ` INSERT INTO mdm_apple_declarations ( declaration_uuid, @@ -3471,7 +3470,7 @@ WHERE } // figure out if we need to delete any declarations - keepIdents := make([]any, 0, len(incomingIdents)) + keepIdents := make([]string, 0, len(incomingIdents)) for _, p := range existingDecls { if newP := incomingDecls[p.Identifier]; newP != nil { keepIdents = append(keepIdents, p.Identifier) @@ -3486,7 +3485,7 @@ WHERE delArgs = []any{declTeamID} } else { // delete the obsolete declarations (all those that are not in keepIdents) - stmt, args, err := sqlx.In(fmt.Sprintf(fmtDeleteStmt, andIdentNotInList), declTeamID, keepIdents) + stmt, args, err := sqlx.In(fmt.Sprintf(fmtDeleteStmt, andIdentNotInList), declTeamID, append(keepIdents, fleetmdm.ListFleetReservedMacOSDeclarationNames()...)) // if err != nil || strings.HasPrefix(ds.testBatchSetMDMAppleProfilesErr, "inselect") { // TODO(JVE): do we need to create similar errors for testing decls? // if err == nil { // err = errors.New(ds.testBatchSetMDMAppleProfilesErr) diff --git a/server/datastore/mysql/microsoft_mdm.go b/server/datastore/mysql/microsoft_mdm.go index 3380815902..2da15a65e3 100644 --- a/server/datastore/mysql/microsoft_mdm.go +++ b/server/datastore/mysql/microsoft_mdm.go @@ -1693,6 +1693,12 @@ ON DUPLICATE KEY UPDATE keepNames = append(keepNames, p.Name) } } + for n := range mdm.FleetReservedProfileNames() { + if _, ok := incomingProfs[n]; !ok { + // always keep reserved profiles even if they're not incoming + keepNames = append(keepNames, n) + } + } var ( stmt string diff --git a/server/mdm/mdm.go b/server/mdm/mdm.go index 534c2333c3..8f5a2c5345 100644 --- a/server/mdm/mdm.go +++ b/server/mdm/mdm.go @@ -116,10 +116,16 @@ func ListFleetReservedWindowsProfileNames() []string { return []string{FleetWindowsOSUpdatesProfileName} } -// ListFleetReservedAppleDDMProfileNames returns a list of profile names that -// are reserved by Fleet for Apple DDM declarations. -func ListFleetReservedAppleDDMProfileNames() []string { +// ListFleetReservedMacOSProfileNames returns a list of PayloadDisplayName strings +// that are reserved by Fleet for macOS. +func ListFleetReservedMacOSProfileNames() []string { + return []string{FleetFileVaultProfileName, FleetdConfigProfileName} +} + +// ListFleetReservedMacOSDeclarationNames returns a list of declaration names +// that are reserved by Fleet for Apple DDM declarations. +func ListFleetReservedMacOSDeclarationNames() []string { return []string{FleetMacOSUpdatesProfileName} // TODO(mna): use this to filter-out those reserved profiles from status - // summaries/filters. + // summaries/filters. Reconcile with the previous func... } diff --git a/server/service/integration_mdm_test.go b/server/service/integration_mdm_test.go index 838135ebec..df741aaf7e 100644 --- a/server/service/integration_mdm_test.go +++ b/server/service/integration_mdm_test.go @@ -6230,6 +6230,54 @@ func (s *integrationMDMTestSuite) assertConfigProfilesByIdentifier(teamID *uint, return profile } +func (s *integrationMDMTestSuite) assertMacOSConfigProfilesByName(teamID *uint, profileName string, exists bool) { + t := s.T() + if teamID == nil { + teamID = ptr.Uint(0) + } + var cfgProfs []*fleet.MDMAppleConfigProfile + mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error { + return sqlx.SelectContext(context.Background(), q, &cfgProfs, `SELECT name FROM mdm_apple_configuration_profiles WHERE team_id = ?`, teamID) + }) + + label := "exist" + if !exists { + label = "not exist" + } + require.Condition(t, func() bool { + for _, p := range cfgProfs { + if p.Name == profileName { + return exists // success if we want it to exist, failure if we don't + } + } + return !exists + }, "a config profile must %s with name: %s", label, profileName) +} + +func (s *integrationMDMTestSuite) assertMacOSDeclarationsByName(teamID *uint, declarationName string, exists bool) { + t := s.T() + if teamID == nil { + teamID = ptr.Uint(0) + } + var cfgProfs []*fleet.MDMAppleConfigProfile + mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error { + return sqlx.SelectContext(context.Background(), q, &cfgProfs, `SELECT name FROM mdm_apple_declarations WHERE team_id = ?`, teamID) + }) + + label := "exist" + if !exists { + label = "not exist" + } + require.Condition(t, func() bool { + for _, p := range cfgProfs { + if p.Name == declarationName { + return exists // success if we want it to exist, failure if we don't + } + } + return !exists + }, "a config profile must %s with name: %s", label, declarationName) +} + func (s *integrationMDMTestSuite) assertWindowsConfigProfilesByName(teamID *uint, profileName string, exists bool) { t := s.T() if teamID == nil { @@ -7423,6 +7471,7 @@ func (s *integrationMDMTestSuite) TestOrbitConfigNudgeSettings() { Description: "desc team1_" + t.Name(), }) require.NoError(t, err) + s.assertMacOSDeclarationsByName(&team.ID, servermdm.FleetMacOSUpdatesProfileName, false) // add the host to the team err = s.ds.AddHostsToTeam(context.Background(), &team.ID, []uint{h.ID}) @@ -7444,6 +7493,7 @@ func (s *integrationMDMTestSuite) TestOrbitConfigNudgeSettings() { }, }, }, http.StatusOK, &tmResp) + s.assertMacOSDeclarationsByName(&team.ID, servermdm.FleetMacOSUpdatesProfileName, true) resp = orbitGetConfigResponse{} s.DoJSON("POST", "/api/fleet/orbit/config", json.RawMessage(fmt.Sprintf(`{"orbit_node_key": %q}`, *h.OrbitNodeKey)), http.StatusOK, &resp) @@ -12219,3 +12269,175 @@ func (s *integrationMDMTestSuite) TestIsServerBitlockerStatus() { require.NotNil(t, hr.Host.MDM.OSSettings.DiskEncryption.Status) require.Equal(t, fleet.DiskEncryptionEnforcing, *hr.Host.MDM.OSSettings.DiskEncryption.Status) } + +func (s *integrationMDMTestSuite) TestMDMBatchSetProfilesKeepsReservedNames() { + t := s.T() + ctx := context.Background() + + checkMacProfs := func(teamID *uint, names ...string) { + var count int + mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error { + var tid uint + if teamID != nil { + tid = *teamID + } + return sqlx.GetContext(ctx, q, &count, `SELECT COUNT(*) FROM mdm_apple_configuration_profiles WHERE team_id = ?`, tid) + }) + require.Equal(t, len(names), count) + for _, n := range names { + s.assertMacOSConfigProfilesByName(teamID, n, true) + } + } + + checkMacDecls := func(teamID *uint, names ...string) { + var count int + mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error { + var tid uint + if teamID != nil { + tid = *teamID + } + return sqlx.GetContext(ctx, q, &count, `SELECT COUNT(*) FROM mdm_apple_declarations WHERE team_id = ?`, tid) + }) + require.Equal(t, len(names), count) + for _, n := range names { + s.assertMacOSDeclarationsByName(teamID, n, true) + } + } + + checkWinProfs := func(teamID *uint, names ...string) { + var count int + mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error { + var tid uint + if teamID != nil { + tid = *teamID + } + return sqlx.GetContext(ctx, q, &count, `SELECT COUNT(*) FROM mdm_windows_configuration_profiles WHERE team_id = ?`, tid) + }) + for _, n := range names { + s.assertWindowsConfigProfilesByName(teamID, n, true) + } + } + + acResp := appConfigResponse{} + s.DoJSON("GET", "/api/latest/fleet/config", nil, http.StatusOK, &acResp) + require.True(t, acResp.MDM.EnabledAndConfigured) + require.True(t, acResp.MDM.WindowsEnabledAndConfigured) + + // ensures that the fleetd profile is created + secrets, err := s.ds.GetEnrollSecrets(ctx, nil) + require.NoError(t, err) + if len(secrets) == 0 { + require.NoError(t, s.ds.ApplyEnrollSecrets(ctx, nil, []*fleet.EnrollSecret{{Secret: t.Name()}})) + } + require.NoError(t, ReconcileAppleProfiles(ctx, s.ds, s.mdmCommander, s.logger)) + + // turn on disk encryption and os updates + s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(`{ + "mdm": { + "enable_disk_encryption": true, + "windows_updates": { + "deadline_days": 3, + "grace_period_days": 1 + }, + "macos_updates": { + "deadline": "2023-12-31", + "minimum_version": "13.3.7" + } + } + }`), http.StatusOK, &acResp) + checkMacProfs(nil, servermdm.ListFleetReservedMacOSProfileNames()...) + checkMacDecls(nil, servermdm.ListFleetReservedMacOSDeclarationNames()...) + checkWinProfs(nil, servermdm.ListFleetReservedWindowsProfileNames()...) + + // batch set only windows profiles doesn't remove the reserved names + newWinProfile := syncml.ForTestWithData(map[string]string{"l1": "d1"}) + var testProfiles []fleet.MDMProfileBatchPayload + testProfiles = append(testProfiles, fleet.MDMProfileBatchPayload{ + Name: "n1", + Contents: newWinProfile, + }) + s.Do("POST", "/api/v1/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: testProfiles}, http.StatusNoContent) + checkMacProfs(nil, servermdm.ListFleetReservedMacOSProfileNames()...) + checkWinProfs(nil, append(servermdm.ListFleetReservedWindowsProfileNames(), "n1")...) + + // batch set windows and mac profiles doesn't remove the reserved names + newMacProfile := mcBytesForTest("n2", "i2", uuid.NewString()) + testProfiles = append(testProfiles, fleet.MDMProfileBatchPayload{ + Name: "n2", + Contents: newMacProfile, + }) + s.Do("POST", "/api/v1/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: testProfiles}, http.StatusNoContent) + checkMacProfs(nil, append(servermdm.ListFleetReservedMacOSProfileNames(), "n2")...) + checkWinProfs(nil, append(servermdm.ListFleetReservedWindowsProfileNames(), "n1")...) + + // batch set only mac profiles doesn't remove the reserved names + testProfiles = []fleet.MDMProfileBatchPayload{{ + Name: "n2", + Contents: newMacProfile, + }} + s.Do("POST", "/api/v1/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: testProfiles}, http.StatusNoContent) + checkMacProfs(nil, append(servermdm.ListFleetReservedMacOSProfileNames(), "n2")...) + checkWinProfs(nil, servermdm.ListFleetReservedWindowsProfileNames()...) + + // create a team + var tmResp teamResponse + s.DoJSON("POST", "/api/v1/fleet/teams", map[string]string{"Name": t.Name()}, http.StatusOK, &tmResp) + + // edit team mdm config to turn on disk encryption and os updates + s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/teams/%d", tmResp.Team.ID), modifyTeamRequest{ + TeamPayload: fleet.TeamPayload{ + Name: ptr.String(t.Name()), + MDM: &fleet.TeamPayloadMDM{ + EnableDiskEncryption: optjson.SetBool(true), + WindowsUpdates: &fleet.WindowsUpdates{ + DeadlineDays: optjson.SetInt(4), + GracePeriodDays: optjson.SetInt(1), + }, + MacOSUpdates: &fleet.MacOSUpdates{ + Deadline: optjson.SetString("2023-12-31"), + MinimumVersion: optjson.SetString("13.3.8"), + }, + }, + }, + }, http.StatusOK, &teamResponse{}) + + s.DoJSON("GET", fmt.Sprintf("/api/v1/fleet/teams/%d", tmResp.Team.ID), nil, http.StatusOK, &tmResp) + require.True(t, tmResp.Team.Config.MDM.EnableDiskEncryption) + require.Equal(t, 4, tmResp.Team.Config.MDM.WindowsUpdates.DeadlineDays.Value) + require.Equal(t, 1, tmResp.Team.Config.MDM.WindowsUpdates.GracePeriodDays.Value) + require.Equal(t, "2023-12-31", tmResp.Team.Config.MDM.MacOSUpdates.Deadline.Value) + require.Equal(t, "13.3.8", tmResp.Team.Config.MDM.MacOSUpdates.MinimumVersion.Value) + + require.NoError(t, ReconcileAppleProfiles(ctx, s.ds, s.mdmCommander, s.logger)) + + checkMacProfs(&tmResp.Team.ID, servermdm.ListFleetReservedMacOSProfileNames()...) + checkWinProfs(&tmResp.Team.ID, servermdm.ListFleetReservedWindowsProfileNames()...) + + // batch set only windows profiles doesn't remove the reserved names + var testTeamProfiles []fleet.MDMProfileBatchPayload + testTeamProfiles = append(testTeamProfiles, fleet.MDMProfileBatchPayload{ + Name: "n1", + Contents: newWinProfile, + }) + s.Do("POST", "/api/v1/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: testTeamProfiles}, http.StatusNoContent, "team_id", strconv.Itoa(int(tmResp.Team.ID))) + checkMacProfs(&tmResp.Team.ID, servermdm.ListFleetReservedMacOSProfileNames()...) + checkWinProfs(&tmResp.Team.ID, append(servermdm.ListFleetReservedWindowsProfileNames(), "n1")...) + + // batch set windows and mac profiles doesn't remove the reserved names + testTeamProfiles = append(testTeamProfiles, fleet.MDMProfileBatchPayload{ + Name: "n2", + Contents: newMacProfile, + }) + s.Do("POST", "/api/v1/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: testTeamProfiles}, http.StatusNoContent, "team_id", strconv.Itoa(int(tmResp.Team.ID))) + checkMacProfs(&tmResp.Team.ID, append(servermdm.ListFleetReservedMacOSProfileNames(), "n2")...) + checkWinProfs(&tmResp.Team.ID, append(servermdm.ListFleetReservedWindowsProfileNames(), "n1")...) + + // batch set only mac profiles doesn't remove the reserved names + testTeamProfiles = []fleet.MDMProfileBatchPayload{{ + Name: "n2", + Contents: newMacProfile, + }} + s.Do("POST", "/api/v1/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: testTeamProfiles}, http.StatusNoContent, "team_id", strconv.Itoa(int(tmResp.Team.ID))) + checkMacProfs(&tmResp.Team.ID, append(servermdm.ListFleetReservedMacOSProfileNames(), "n2")...) + checkWinProfs(&tmResp.Team.ID, servermdm.ListFleetReservedWindowsProfileNames()...) +}