Match pre-assigned profiles to a team (or create one) and assign host to team (#12127)

This commit is contained in:
Martin Angers
2023-06-05 15:08:21 -04:00
committed by GitHub
parent 166b83a461
commit 9f064acd2e
13 changed files with 620 additions and 22 deletions
+2 -1
View File
@@ -1 +1,2 @@
* Added the `POST /fleet/mdm/apple/preassign` endpoint to store profiles to be assigned to a host, for subsequent matching with an existing (or new) team.
* Added the `POST /fleet/mdm/apple/profiles/preassign` endpoint to store profiles to be assigned to a host, for subsequent matching with an existing (or new) team.
* Added the `POST /fleet/mdm/apple/profiles/match` endpoint to match pre-assigned profiles to an existing team or create one if needed, and assign the host to that team.
+116 -2
View File
@@ -7,10 +7,13 @@ import (
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"sort"
"strings"
"time"
"github.com/fleetdm/fleet/v4/pkg/file"
"github.com/fleetdm/fleet/v4/server/authz"
@@ -742,7 +745,118 @@ func (svc *Service) MDMAppleMatchPreassignment(ctx context.Context, externalHost
return ctxerr.Wrap(ctx, err)
}
// TODO(mna): TBD the exact signature, must return the list of profiles for matching
//profs, err := svc.profileMatcher.RetrieveProfiles(ctx, externalHostIdentifier)
profs, err := svc.profileMatcher.RetrieveProfiles(ctx, externalHostIdentifier)
if err != nil {
return err
}
if len(profs.Profiles) == 0 || profs.HostUUID == "" {
return nil // nothing to do
}
// load the host and ensure it is enrolled in Fleet MDM
host, err := svc.ds.HostByIdentifier(ctx, profs.HostUUID)
if err != nil {
return err // will return a not found error if host does not exist
}
hostMDM, err := svc.ds.GetHostMDM(ctx, host.ID)
if err != nil || !hostMDM.IsFleetEnrolled() {
if err == nil || fleet.IsNotFound(err) {
err = errors.New("host is not enrolled in Fleet MDM")
return ctxerr.Wrap(ctx, &fleet.BadRequestError{
Message: err.Error(),
InternalErr: err,
})
}
return err
}
// Collect the profiles' hashes and look for a team with exactly that set.
// Also collect the profiles' groups in case we need to create a new team,
// and the list of raw profiles bytes.
hashes, groups, rawProfiles := make([]string, 0, len(profs.Profiles)),
make([]string, 0, len(profs.Profiles)),
make([][]byte, 0, len(profs.Profiles))
for _, prof := range profs.Profiles {
hashes = append(hashes, prof.HexMD5Hash)
rawProfiles = append(rawProfiles, prof.Profile)
if prof.Group != "" {
groups = append(groups, prof.Group)
}
}
// find a team with exactly that set of profiles
teamIDs, err := svc.ds.MatchMDMAppleConfigProfiles(ctx, hashes)
if err != nil {
return err
}
var targetTeamID uint
if len(teamIDs) > 0 {
// if the host is already in one of those valid teams, nothing to do.
if host.TeamID != nil {
for _, tmID := range teamIDs {
if *host.TeamID == tmID {
return nil
}
}
}
// else assign the host to the first valid team
targetTeamID = teamIDs[0]
} else {
// Create a new team with this set of profiles. Creating via the service
// call so that it properly assigns the agent options and creates audit
// activities, etc.
teamName := teamNameFromPreassignGroups(groups)
tm, err := svc.NewTeam(ctx, fleet.TeamPayload{Name: &teamName})
if err != nil {
return err
}
// create profiles for that team via the service call, so that uniqueness
// of profile identifier/name is verified, activity created, etc.
// NOTE: this will use the read replica to load the team, which was just
// created above, could lead to not found issues with slow replication.
if err := svc.BatchSetMDMAppleProfiles(ctx, &tm.ID, nil, rawProfiles, false); err != nil {
return err
}
targetTeamID = tm.ID
}
// assign host to that team via the service call, which will trigger
// deployment of the profiles.
if err := svc.AddHostsToTeam(ctx, &targetTeamID, []uint{host.ID}); err != nil {
return err
}
return nil
}
// teamNameFromPreassignGroups returns the team name to use for a new team
// created to match the set of profiles preassigned to a host. The team name is
// derived from the "group" field provided with each request to pre-assign a
// profile to a host (in fleet.MDMApplePreassignProfilePayload). That field is
// optional, and empty groups are ignored. The current timestamp is appended to
// the team's name to help avoid duplicates.
func teamNameFromPreassignGroups(groups []string) string {
const defaultName = "default"
dedupeGroups := make(map[string]struct{}, len(groups))
for _, group := range groups {
if group != "" {
dedupeGroups[group] = struct{}{}
}
}
groups = groups[:0]
for group := range dedupeGroups {
groups = append(groups, group)
}
sort.Strings(groups)
if len(groups) == 0 {
groups = []string{defaultName}
}
return fmt.Sprintf("%s (%s)", strings.Join(groups, " - "), time.Now().UTC().Format("2006-01-02:15:04:05"))
}
+39
View File
@@ -107,6 +107,45 @@ ORDER BY name`
return res, nil
}
func (ds *Datastore) MatchMDMAppleConfigProfiles(ctx context.Context, hexMD5Hashes []string) ([]uint, error) {
// as a special-case, should never be called without at least one hash but if
// so, never matches anything.
if len(hexMD5Hashes) == 0 {
return nil, nil
}
stmt := `
SELECT
p1.team_id
FROM
mdm_apple_configuration_profiles p1
WHERE
NOT EXISTS (
SELECT
1
FROM
mdm_apple_configuration_profiles p2
WHERE
p1.team_id = p2.team_id AND
HEX(p2.checksum) NOT IN (?)
)
GROUP BY
p1.team_id
HAVING
COUNT(*) = ?`
stmt, args, err := sqlx.In(stmt, hexMD5Hashes, len(hexMD5Hashes))
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "prepare query arguments")
}
var teamIDs []uint
if err := sqlx.SelectContext(ctx, ds.reader, &teamIDs, stmt, args...); err != nil {
return nil, ctxerr.Wrap(ctx, err, "execute query")
}
return teamIDs, nil
}
func (ds *Datastore) GetMDMAppleConfigProfile(ctx context.Context, profileID uint) (*fleet.MDMAppleConfigProfile, error) {
stmt := `
SELECT
+84
View File
@@ -61,6 +61,7 @@ func TestMDMApple(t *testing.T) {
{"TestMDMAppleDefaultSetupAssistant", testMDMAppleDefaultSetupAssistant},
{"TestSetVerifiedMacOSProfiles", testSetVerifiedMacOSProfiles},
{"TestMDMAppleConfigProfileHash", testMDMAppleConfigProfileHash},
{"TestMatchMDMAppleConfigProfiles", testMatchMDMAppleConfigProfiles},
}
for _, c := range cases {
@@ -4240,3 +4241,86 @@ func testMDMAppleConfigProfileHash(t *testing.T, ds *Datastore) {
})
}
}
func testMatchMDMAppleConfigProfiles(t *testing.T, ds *Datastore) {
ctx := context.Background()
// create some teams with different sets of profiles
tmNoProf, err := ds.NewTeam(ctx, &fleet.Team{Name: "no-prof"})
require.NoError(t, err)
require.NotNil(t, tmNoProf)
tmProfA, err := ds.NewTeam(ctx, &fleet.Team{Name: "prof-a"})
require.NoError(t, err)
tmProfAB, err := ds.NewTeam(ctx, &fleet.Team{Name: "prof-ab"})
require.NoError(t, err)
tmProfBC, err := ds.NewTeam(ctx, &fleet.Team{Name: "prof-bc"})
require.NoError(t, err)
tmProfABC, err := ds.NewTeam(ctx, &fleet.Team{Name: "prof-abc"})
require.NoError(t, err)
// create another team with profile A
tmProfA2, err := ds.NewTeam(ctx, &fleet.Team{Name: "prof-a2"})
require.NoError(t, err)
profA := configProfileForTest(t, "A", "A", "A")
profB := configProfileForTest(t, "B", "B", "B")
profC := configProfileForTest(t, "C", "C", "C")
profA.TeamID = &tmProfA.ID
_, err = ds.NewMDMAppleConfigProfile(ctx, *profA)
require.NoError(t, err)
profA.TeamID = &tmProfA2.ID
_, err = ds.NewMDMAppleConfigProfile(ctx, *profA)
require.NoError(t, err)
profA.TeamID = &tmProfAB.ID
_, err = ds.NewMDMAppleConfigProfile(ctx, *profA)
require.NoError(t, err)
profA.TeamID = &tmProfABC.ID
_, err = ds.NewMDMAppleConfigProfile(ctx, *profA)
require.NoError(t, err)
profB.TeamID = &tmProfAB.ID
_, err = ds.NewMDMAppleConfigProfile(ctx, *profB)
require.NoError(t, err)
profB.TeamID = &tmProfBC.ID
_, err = ds.NewMDMAppleConfigProfile(ctx, *profB)
require.NoError(t, err)
profB.TeamID = &tmProfABC.ID
_, err = ds.NewMDMAppleConfigProfile(ctx, *profB)
require.NoError(t, err)
profC.TeamID = &tmProfBC.ID
_, err = ds.NewMDMAppleConfigProfile(ctx, *profC)
require.NoError(t, err)
profC.TeamID = &tmProfABC.ID
_, err = ds.NewMDMAppleConfigProfile(ctx, *profC)
require.NoError(t, err)
// get the hashes for each profile, the same way the matching logic would
profAHash := (fleet.MDMApplePreassignProfilePayload{Profile: profA.Mobileconfig}).HexMD5Hash()
profBHash := (fleet.MDMApplePreassignProfilePayload{Profile: profB.Mobileconfig}).HexMD5Hash()
profCHash := (fleet.MDMApplePreassignProfilePayload{Profile: profC.Mobileconfig}).HexMD5Hash()
cases := []struct {
hashes []string
teamIDs []uint
}{
{nil, nil},
{[]string{profAHash}, []uint{tmProfA.ID, tmProfA2.ID}},
{[]string{profBHash}, nil},
{[]string{profCHash}, nil},
{[]string{profAHash, profBHash}, []uint{tmProfAB.ID}},
{[]string{profAHash, profBHash, profCHash}, []uint{tmProfABC.ID}},
{[]string{profBHash, profCHash}, []uint{tmProfBC.ID}},
{[]string{profAHash, profCHash}, nil},
}
for _, c := range cases {
t.Run(fmt.Sprintf("%v", c.hashes), func(t *testing.T) {
matches, err := ds.MatchMDMAppleConfigProfiles(ctx, c.hashes)
require.NoError(t, err)
require.ElementsMatch(t, c.teamIDs, matches)
})
}
}
+18 -2
View File
@@ -435,7 +435,23 @@ type MDMApplePreassignProfilePayload struct {
// an option: https://dev.mysql.com/doc/refman/5.7/en/encryption-functions.html#function_sha2).
func (p MDMApplePreassignProfilePayload) HexMD5Hash() string {
sum := md5.Sum(p.Profile) //nolint: gosec
return hex.EncodeToString(sum[:])
// mysql's HEX function returns uppercase
return strings.ToUpper(hex.EncodeToString(sum[:]))
}
// MDMApplePreassignHostProfiles represents the set of profiles that were
// pre-assigned to a given host identified by its UUID.
type MDMApplePreassignHostProfiles struct {
HostUUID string
Profiles []MDMApplePreassignProfile
}
// MDMApplePreassignProfile represents a single profile pre-assigned to a host.
type MDMApplePreassignProfile struct {
Profile []byte
Group string
HexMD5Hash string
}
// MDMAppleSettingsPayload describes the payload accepted by the endpoint to
@@ -548,5 +564,5 @@ func (a MDMAppleSetupAssistant) AuthzType() string {
// implementation is used in production.
type ProfileMatcher interface {
PreassignProfile(ctx context.Context, payload MDMApplePreassignProfilePayload) error
RetrieveProfiles(ctx context.Context, externalHostIdentifier string) error
RetrieveProfiles(ctx context.Context, externalHostIdentifier string) (MDMApplePreassignHostProfiles, error)
}
+5
View File
@@ -764,6 +764,11 @@ type Datastore interface {
// For global config profiles, specify nil as the team id.
ListMDMAppleConfigProfiles(ctx context.Context, teamID *uint) ([]*MDMAppleConfigProfile, error)
// MatchMDMAppleConfigProfiles returns the list of team ids that have the
// exact set of configuration profiles as those specified by their
// hex-encoded md5 hashes.
MatchMDMAppleConfigProfiles(ctx context.Context, hexMD5Hashes []string) ([]uint, error)
// DeleteMDMAppleConfigProfile deletes the mdm config profile corresponding
// to the specified profile id.
DeleteMDMAppleConfigProfile(ctx context.Context, profileID uint) error
+7
View File
@@ -803,6 +803,13 @@ func (h *HostMDM) IsManualFleetEnrolled() bool {
h.Name == WellKnownMDMFleet
}
// IsFleetEnrolled returns true if the host's MDM information indicates that
// it is in enrolled state for Fleet MDM, regardless of automatic or manual
// enrollment method.
func (h *HostMDM) IsFleetEnrolled() bool {
return h.IsDEPFleetEnrolled() || h.IsManualFleetEnrolled()
}
// HostMunkiIssue represents a single munki issue for a host.
type HostMunkiIssue struct {
MunkiIssueID uint `db:"munki_issue_id" json:"id"`
+43 -4
View File
@@ -2,6 +2,8 @@ package apple_mdm
import (
"context"
"encoding/hex"
"strings"
"time"
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
@@ -97,15 +99,52 @@ func (p *profileMatcher) PreassignProfile(ctx context.Context, payload fleet.MDM
// RetrieveProfiles retrieves the profiles preassigned to this host for
// matching with a team and assignment.
func (p *profileMatcher) RetrieveProfiles(ctx context.Context, externalHostIdentifier string) error {
func (p *profileMatcher) RetrieveProfiles(ctx context.Context, externalHostIdentifier string) (fleet.MDMApplePreassignHostProfiles, error) {
var hostProfs fleet.MDMApplePreassignHostProfiles
// Note that we do not configure the Redis connection to read from a replica
// here as it may be called very soon after the PreassignProfile call and
// could miss some unreplicated profiles.
conn := redis.ConfigureDoer(p.pool, p.pool.Get())
defer conn.Close()
// TODO: find all profiles matching the host identifier
// TODO: cleanup all retrieved profiles?
return nil
profs, err := redigo.StringMap(conn.Do("HGETALL", keyForExternalHostIdentifier(externalHostIdentifier)))
if err != nil {
return hostProfs, ctxerr.Wrap(ctx, err, "execute redis HGETALL")
}
if _, err := conn.Do("UNLINK", keyForExternalHostIdentifier(externalHostIdentifier)); err != nil {
return hostProfs, ctxerr.Wrap(ctx, err, "execute redis UNLINK")
}
_ = conn.Close() // release connection to the pool immediately as we're done with redis
if hostProfs.HostUUID = profs["host_uuid"]; hostProfs.HostUUID == "" {
// unknown host/no profiles to assign, not an error but nothing to do
return hostProfs, nil
}
delete(profs, "host_uuid")
for k, v := range profs {
if strings.HasSuffix(k, "_group") || v == "" {
// only look for profiles' hex hashes, the group information will be
// retrieved only when a profile is found. Ignore empty values (e.g.
// empty profile).
continue
}
// if the key is not the group, then it has to be a profile hash, ensure
// that it is a valid hex-encoded value.
if _, err := hex.DecodeString(k); err != nil {
// ignore unknown/invalid fields
continue
}
hostProfs.Profiles = append(hostProfs.Profiles, fleet.MDMApplePreassignProfile{
Profile: []byte(v),
Group: profs[k+"_group"],
HexMD5Hash: k,
})
}
return hostProfs, nil
}
func keyForExternalHostIdentifier(externalHostIdentifier string) string {
+79
View File
@@ -137,6 +137,85 @@ func TestPreassignProfile(t *testing.T) {
})
}
func TestRetrieveProfiles(t *testing.T) {
runTest := func(t *testing.T, pool fleet.RedisPool) {
ctx := context.Background()
matcher := NewProfileMatcher(pool)
// preassign a profile with a group
p1 := fleet.MDMApplePreassignProfilePayload{
ExternalHostIdentifier: "abcd",
HostUUID: "1234",
Profile: generateProfile("p1", "p1", "Configuration", "p1"),
Group: "g1",
}
err := matcher.PreassignProfile(ctx, p1)
require.NoError(t, err)
// preassign a profile without a group
p2 := fleet.MDMApplePreassignProfilePayload{
ExternalHostIdentifier: "abcd",
HostUUID: "1234",
Profile: generateProfile("p2", "p2", "Configuration", "p2"),
}
err = matcher.PreassignProfile(ctx, p2)
require.NoError(t, err)
// retrieve from unknown external host identifier
profs, err := matcher.RetrieveProfiles(ctx, "efgh")
require.NoError(t, err)
require.Empty(t, profs.HostUUID)
require.Empty(t, profs.Profiles)
// retrieve from valid external host identifier
profs, err = matcher.RetrieveProfiles(ctx, "abcd")
require.NoError(t, err)
require.Equal(t, "1234", profs.HostUUID)
require.ElementsMatch(t, []fleet.MDMApplePreassignProfile{
{Profile: p1.Profile, Group: p1.Group, HexMD5Hash: p1.HexMD5Hash()},
{Profile: p2.Profile, Group: "", HexMD5Hash: p2.HexMD5Hash()},
}, profs.Profiles)
// after retrieval, the key is deleted
profs, err = matcher.RetrieveProfiles(ctx, "abcd")
require.NoError(t, err)
require.Empty(t, profs.HostUUID)
require.Empty(t, profs.Profiles)
// preassign to a host and generate invalid data
p3 := fleet.MDMApplePreassignProfilePayload{
ExternalHostIdentifier: "xyz",
HostUUID: "5678",
Profile: generateProfile("p3", "p3", "Configuration", "p3"),
}
err = matcher.PreassignProfile(ctx, p3)
require.NoError(t, err)
conn := redis.ConfigureDoer(pool, pool.Get())
defer conn.Close()
_, err = conn.Do("HSET", keyForExternalHostIdentifier("xyz"), "123ABC", "", "not-hex", "foo")
require.NoError(t, err)
// retrieves only the valid data for that host
profs, err = matcher.RetrieveProfiles(ctx, "xyz")
require.NoError(t, err)
require.Equal(t, "5678", profs.HostUUID)
require.ElementsMatch(t, []fleet.MDMApplePreassignProfile{
{Profile: p3.Profile, Group: "", HexMD5Hash: p3.HexMD5Hash()},
}, profs.Profiles)
}
t.Run("standalone", func(t *testing.T) {
pool := redistest.SetupRedis(t, preassignKeyPrefix, false, false, false)
runTest(t, pool)
})
t.Run("cluster", func(t *testing.T) {
pool := redistest.SetupRedis(t, preassignKeyPrefix, true, true, false)
runTest(t, pool)
})
}
func TestPreassignProfileValidation(t *testing.T) {
ctx := context.Background()
pool := redistest.SetupRedis(t, preassignKeyPrefix, false, false, false)
+12
View File
@@ -528,6 +528,8 @@ type GetMDMAppleConfigProfileFunc func(ctx context.Context, profileID uint) (*fl
type ListMDMAppleConfigProfilesFunc func(ctx context.Context, teamID *uint) ([]*fleet.MDMAppleConfigProfile, error)
type MatchMDMAppleConfigProfilesFunc func(ctx context.Context, hexMD5Hashes []string) ([]uint, error)
type DeleteMDMAppleConfigProfileFunc func(ctx context.Context, profileID uint) error
type DeleteMDMAppleConfigProfileByTeamAndIdentifierFunc func(ctx context.Context, teamID *uint, profileIdentifier string) error
@@ -1405,6 +1407,9 @@ type DataStore struct {
ListMDMAppleConfigProfilesFunc ListMDMAppleConfigProfilesFunc
ListMDMAppleConfigProfilesFuncInvoked bool
MatchMDMAppleConfigProfilesFunc MatchMDMAppleConfigProfilesFunc
MatchMDMAppleConfigProfilesFuncInvoked bool
DeleteMDMAppleConfigProfileFunc DeleteMDMAppleConfigProfileFunc
DeleteMDMAppleConfigProfileFuncInvoked bool
@@ -3362,6 +3367,13 @@ func (s *DataStore) ListMDMAppleConfigProfiles(ctx context.Context, teamID *uint
return s.ListMDMAppleConfigProfilesFunc(ctx, teamID)
}
func (s *DataStore) MatchMDMAppleConfigProfiles(ctx context.Context, hexMD5Hashes []string) ([]uint, error) {
s.mu.Lock()
s.MatchMDMAppleConfigProfilesFuncInvoked = true
s.mu.Unlock()
return s.MatchMDMAppleConfigProfilesFunc(ctx, hexMD5Hashes)
}
func (s *DataStore) DeleteMDMAppleConfigProfile(ctx context.Context, profileID uint) error {
s.mu.Lock()
s.DeleteMDMAppleConfigProfileFuncInvoked = true
+66 -5
View File
@@ -35,6 +35,16 @@ import (
"github.com/stretchr/testify/require"
)
type nopProfileMatcher struct{}
func (nopProfileMatcher) PreassignProfile(ctx context.Context, pld fleet.MDMApplePreassignProfilePayload) error {
return nil
}
func (nopProfileMatcher) RetrieveProfiles(ctx context.Context, extHostID string) (fleet.MDMApplePreassignHostProfiles, error) {
return fleet.MDMApplePreassignHostProfiles{}, nil
}
func setupAppleMDMService(t *testing.T, license *fleet.LicenseInfo) (fleet.Service, context.Context, *mock.Store) {
ds := new(mock.Store)
cfg := config.TestConfig()
@@ -62,11 +72,12 @@ func setupAppleMDMService(t *testing.T, license *fleet.LicenseInfo) (fleet.Servi
)
opts := &TestServerOpts{
FleetConfig: &cfg,
MDMStorage: mdmStorage,
DEPStorage: depStorage,
MDMPusher: pusher,
License: license,
FleetConfig: &cfg,
MDMStorage: mdmStorage,
DEPStorage: depStorage,
MDMPusher: pusher,
License: license,
ProfileMatcher: nopProfileMatcher{},
}
svc, ctx := newTestServiceWithConfig(t, ds, cfg, nil, nil, opts)
@@ -2458,6 +2469,56 @@ func TestMDMAppleSetupAssistant(t *testing.T) {
}
}
func TestMDMApplePreassignEndpoints(t *testing.T) {
svc, ctx, _ := setupAppleMDMService(t, &fleet.LicenseInfo{Tier: fleet.TierPremium})
checkAuthErr := func(t *testing.T, err error, shouldFailWithAuth bool) {
t.Helper()
if shouldFailWithAuth {
require.Error(t, err)
require.Contains(t, err.Error(), authz.ForbiddenErrorMessage)
} else {
require.NoError(t, err)
}
}
testCases := []struct {
name string
user *fleet.User
shouldFail bool
}{
{"no role", test.UserNoRoles, true},
{"global admin", test.UserAdmin, false},
{"global maintainer", test.UserMaintainer, true},
{"global observer", test.UserObserver, true},
{"global observer+", test.UserObserverPlus, true},
{"global gitops", test.UserGitOps, false},
{"team admin", test.UserTeamAdminTeam1, true},
{"team maintainer", test.UserTeamMaintainerTeam1, true},
{"team observer", test.UserTeamObserverTeam1, true},
{"team observer+", test.UserTeamObserverPlusTeam1, true},
{"team gitops", test.UserTeamGitOpsTeam1, true},
}
for _, tt := range testCases {
t.Run(tt.name, func(t *testing.T) {
// prepare the context with the user
ctx := viewer.NewContext(ctx, viewer.Viewer{User: tt.user})
err := svc.MDMApplePreassignProfile(ctx, fleet.MDMApplePreassignProfilePayload{
ExternalHostIdentifier: "test",
HostUUID: "test",
Profile: mobileconfigForTest("N1", "I1"),
})
checkAuthErr(t, err, tt.shouldFail)
err = svc.MDMAppleMatchPreassignment(ctx, "test")
checkAuthErr(t, err, tt.shouldFail)
})
}
}
func mobileconfigForTest(name, identifier string) []byte {
return []byte(fmt.Sprintf(`<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+145 -8
View File
@@ -539,11 +539,12 @@ func (s *integrationMDMTestSuite) TestProfileManagement() {
require.Equal(t, uint(0), noTeamSummaryResp.Verified)
}
func (s *integrationMDMTestSuite) TestPuppetPreassignProfiles() {
func (s *integrationMDMTestSuite) TestPuppetMatchPreassignProfiles() {
ctx := context.Background()
t := s.T()
// create a host enrolled in fleet
mdmHost, mdmDevice := createHostThenEnrollMDM(s.ds, s.server.URL, t)
mdmHost, _ := createHostThenEnrollMDM(s.ds, s.server.URL, t)
s.runWorker()
// create a host that's not enrolled into MDM
@@ -557,19 +558,155 @@ func (s *integrationMDMTestSuite) TestPuppetPreassignProfiles() {
require.NoError(t, err)
// preassign an empty profile, fails
s.Do("POST", "/api/latest/fleet/mdm/apple/profiles/preassign", preassignMDMAppleProfileRequest{MDMApplePreassignProfilePayload: fleet.MDMApplePreassignProfilePayload{ExternalHostIdentifier: "a", HostUUID: nonMDMHost.UUID, Profile: nil}}, http.StatusUnprocessableEntity)
s.Do("POST", "/api/latest/fleet/mdm/apple/profiles/preassign", preassignMDMAppleProfileRequest{MDMApplePreassignProfilePayload: fleet.MDMApplePreassignProfilePayload{ExternalHostIdentifier: "empty", HostUUID: nonMDMHost.UUID, Profile: nil}}, http.StatusUnprocessableEntity)
// preassign a valid profile to the MDM host
s.Do("POST", "/api/latest/fleet/mdm/apple/profiles/preassign", preassignMDMAppleProfileRequest{MDMApplePreassignProfilePayload: fleet.MDMApplePreassignProfilePayload{ExternalHostIdentifier: "a", HostUUID: mdmHost.UUID, Profile: mobileconfigForTest("n1", "i1")}}, http.StatusNoContent)
prof1 := mobileconfigForTest("n1", "i1")
s.Do("POST", "/api/latest/fleet/mdm/apple/profiles/preassign", preassignMDMAppleProfileRequest{MDMApplePreassignProfilePayload: fleet.MDMApplePreassignProfilePayload{ExternalHostIdentifier: "mdm1", HostUUID: mdmHost.UUID, Profile: prof1}}, http.StatusNoContent)
// preassign another valid profile to the MDM host
s.Do("POST", "/api/latest/fleet/mdm/apple/profiles/preassign", preassignMDMAppleProfileRequest{MDMApplePreassignProfilePayload: fleet.MDMApplePreassignProfilePayload{ExternalHostIdentifier: "b", HostUUID: mdmHost.UUID, Profile: mobileconfigForTest("n2", "i2"), Group: "g1"}}, http.StatusNoContent)
prof2 := mobileconfigForTest("n2", "i2")
s.Do("POST", "/api/latest/fleet/mdm/apple/profiles/preassign", preassignMDMAppleProfileRequest{MDMApplePreassignProfilePayload: fleet.MDMApplePreassignProfilePayload{ExternalHostIdentifier: "mdm1", HostUUID: mdmHost.UUID, Profile: prof2, Group: "g1"}}, http.StatusNoContent)
// preassign a valid profile to the non-MDM host, still works as the host is not validated in this call
s.Do("POST", "/api/latest/fleet/mdm/apple/profiles/preassign", preassignMDMAppleProfileRequest{MDMApplePreassignProfilePayload: fleet.MDMApplePreassignProfilePayload{ExternalHostIdentifier: "c", HostUUID: nonMDMHost.UUID, Profile: mobileconfigForTest("n3", "i3"), Group: "g2"}}, http.StatusNoContent)
prof3 := mobileconfigForTest("n3", "i3")
s.Do("POST", "/api/latest/fleet/mdm/apple/profiles/preassign", preassignMDMAppleProfileRequest{MDMApplePreassignProfilePayload: fleet.MDMApplePreassignProfilePayload{ExternalHostIdentifier: "non-mdm", HostUUID: nonMDMHost.UUID, Profile: prof3, Group: "g2"}}, http.StatusNoContent)
// TODO(mna): when matching is implemented, add test cases to check team creation and host assignment
_ = mdmDevice
// match with an invalid external host id, succeeds as it is the same as if
// there was no matching to do (no preassignment was done)
s.Do("POST", "/api/latest/fleet/mdm/apple/profiles/match", matchMDMApplePreassignmentRequest{ExternalHostIdentifier: "no-such-id"}, http.StatusNoContent)
// match with the non-mdm host fails
res := s.Do("POST", "/api/latest/fleet/mdm/apple/profiles/match", matchMDMApplePreassignmentRequest{ExternalHostIdentifier: "non-mdm"}, http.StatusBadRequest)
errMsg := extractServerErrorText(res.Body)
require.Contains(t, errMsg, "host is not enrolled in Fleet MDM")
// match with the mdm host succeeds and creates a team based on the group labels
s.Do("POST", "/api/latest/fleet/mdm/apple/profiles/match", matchMDMApplePreassignmentRequest{ExternalHostIdentifier: "mdm1"}, http.StatusNoContent)
// the host is now part of that team
h, err := s.ds.Host(ctx, mdmHost.ID)
require.NoError(t, err)
require.NotNil(t, h.TeamID)
tm1, err := s.ds.Team(ctx, *h.TeamID)
require.NoError(t, err)
require.Regexp(t, `^g1 \(\d+-\d+-\d+:\d+:\d+:\d+\)$`, tm1.Name)
// and the team has the expected profiles
profs, err := s.ds.ListMDMAppleConfigProfiles(ctx, &tm1.ID)
require.NoError(t, err)
require.Len(t, profs, 2)
// order is guaranteed by profile name
require.Equal(t, prof1, []byte(profs[0].Mobileconfig))
require.Equal(t, prof2, []byte(profs[1].Mobileconfig))
// create a team and set profiles to it
tm2, err := s.ds.NewTeam(context.Background(), &fleet.Team{
Name: "team2_" + t.Name(),
})
require.NoError(t, err)
prof4 := mobileconfigForTest("n4", "i4")
s.Do("POST", "/api/v1/fleet/mdm/apple/profiles/batch", batchSetMDMAppleProfilesRequest{Profiles: [][]byte{
prof1, prof4,
}}, http.StatusNoContent, "team_id", fmt.Sprint(tm2.ID))
// create another team with a superset of profiles
tm3, err := s.ds.NewTeam(context.Background(), &fleet.Team{
Name: "team3_" + t.Name(),
})
require.NoError(t, err)
s.Do("POST", "/api/v1/fleet/mdm/apple/profiles/batch", batchSetMDMAppleProfilesRequest{Profiles: [][]byte{
prof1, prof2, prof4,
}}, http.StatusNoContent, "team_id", fmt.Sprint(tm3.ID))
// and yet another team with the same profiles as tm3
tm4, err := s.ds.NewTeam(context.Background(), &fleet.Team{
Name: "team4_" + t.Name(),
})
require.NoError(t, err)
s.Do("POST", "/api/v1/fleet/mdm/apple/profiles/batch", batchSetMDMAppleProfilesRequest{Profiles: [][]byte{
prof1, prof2, prof4,
}}, http.StatusNoContent, "team_id", fmt.Sprint(tm4.ID))
// preassign the MDM host to prof1 and prof4, should match existing team tm2
s.Do("POST", "/api/latest/fleet/mdm/apple/profiles/preassign", preassignMDMAppleProfileRequest{MDMApplePreassignProfilePayload: fleet.MDMApplePreassignProfilePayload{ExternalHostIdentifier: "mdm1", HostUUID: mdmHost.UUID, Profile: prof1, Group: "g1"}}, http.StatusNoContent)
s.Do("POST", "/api/latest/fleet/mdm/apple/profiles/preassign", preassignMDMAppleProfileRequest{MDMApplePreassignProfilePayload: fleet.MDMApplePreassignProfilePayload{ExternalHostIdentifier: "mdm1", HostUUID: mdmHost.UUID, Profile: prof4, Group: "g4"}}, http.StatusNoContent)
// match with the mdm host succeeds and assigns it to tm2
s.Do("POST", "/api/latest/fleet/mdm/apple/profiles/match", matchMDMApplePreassignmentRequest{ExternalHostIdentifier: "mdm1"}, http.StatusNoContent)
// the host is now part of that team
h, err = s.ds.Host(ctx, mdmHost.ID)
require.NoError(t, err)
require.NotNil(t, h.TeamID)
require.Equal(t, tm2.ID, *h.TeamID)
// the host's profiles are the same as the team's and are pending, and prof2 is pending removal
hostProfs, err := s.ds.GetHostMDMProfiles(ctx, mdmHost.UUID)
require.NoError(t, err)
require.Len(t, hostProfs, 3)
sort.Slice(hostProfs, func(i, j int) bool {
l, r := hostProfs[i], hostProfs[j]
return l.Name < r.Name
})
require.Equal(t, "n1", hostProfs[0].Name)
require.NotNil(t, hostProfs[0].Status)
require.Equal(t, fleet.MDMAppleDeliveryPending, *hostProfs[0].Status)
require.Equal(t, fleet.MDMAppleOperationTypeInstall, hostProfs[0].OperationType)
require.Equal(t, "n2", hostProfs[1].Name)
require.NotNil(t, hostProfs[1].Status)
require.Equal(t, fleet.MDMAppleDeliveryPending, *hostProfs[1].Status)
require.Equal(t, fleet.MDMAppleOperationTypeRemove, hostProfs[1].OperationType)
require.Equal(t, "n4", hostProfs[2].Name)
require.NotNil(t, hostProfs[2].Status)
require.Equal(t, fleet.MDMAppleDeliveryPending, *hostProfs[2].Status)
require.Equal(t, fleet.MDMAppleOperationTypeInstall, hostProfs[2].OperationType)
// create a new mdm host enrolled in fleet
mdmHost2, _ := createHostThenEnrollMDM(s.ds, s.server.URL, t)
s.runWorker()
// make it part of team 4
s.Do("POST", "/api/v1/fleet/hosts/transfer",
addHostsToTeamRequest{TeamID: &tm4.ID, HostIDs: []uint{mdmHost2.ID}}, http.StatusOK)
// simulate having its profiles installed
mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
_, err := q.ExecContext(ctx, `UPDATE host_mdm_apple_profiles SET status = ? WHERE host_uuid = ?`, fleet.MacOSSettingsVerifying, mdmHost2.UUID)
return err
})
// preassign the MDM host to prof1, prof2 and prof4, should match existing
// team tm3 and tm4, and nothing be done since the host is already in tm4
s.Do("POST", "/api/latest/fleet/mdm/apple/profiles/preassign", preassignMDMAppleProfileRequest{MDMApplePreassignProfilePayload: fleet.MDMApplePreassignProfilePayload{ExternalHostIdentifier: "mdm2", HostUUID: mdmHost2.UUID, Profile: prof1, Group: "g1"}}, http.StatusNoContent)
s.Do("POST", "/api/latest/fleet/mdm/apple/profiles/preassign", preassignMDMAppleProfileRequest{MDMApplePreassignProfilePayload: fleet.MDMApplePreassignProfilePayload{ExternalHostIdentifier: "mdm2", HostUUID: mdmHost2.UUID, Profile: prof2, Group: "g2"}}, http.StatusNoContent)
s.Do("POST", "/api/latest/fleet/mdm/apple/profiles/preassign", preassignMDMAppleProfileRequest{MDMApplePreassignProfilePayload: fleet.MDMApplePreassignProfilePayload{ExternalHostIdentifier: "mdm2", HostUUID: mdmHost2.UUID, Profile: prof4, Group: "g4"}}, http.StatusNoContent)
s.Do("POST", "/api/latest/fleet/mdm/apple/profiles/match", matchMDMApplePreassignmentRequest{ExternalHostIdentifier: "mdm2"}, http.StatusNoContent)
// the host is still part of tm4
h, err = s.ds.Host(ctx, mdmHost2.ID)
require.NoError(t, err)
require.NotNil(t, h.TeamID)
require.Equal(t, tm4.ID, *h.TeamID)
// and its profiles have been left untouched
hostProfs, err = s.ds.GetHostMDMProfiles(ctx, mdmHost2.UUID)
require.NoError(t, err)
require.Len(t, hostProfs, 3)
sort.Slice(hostProfs, func(i, j int) bool {
l, r := hostProfs[i], hostProfs[j]
return l.Name < r.Name
})
require.Equal(t, "n1", hostProfs[0].Name)
require.NotNil(t, hostProfs[0].Status)
require.Equal(t, fleet.MDMAppleDeliveryVerifying, *hostProfs[0].Status)
require.Equal(t, "n2", hostProfs[1].Name)
require.NotNil(t, hostProfs[1].Status)
require.Equal(t, fleet.MDMAppleDeliveryVerifying, *hostProfs[1].Status)
require.Equal(t, "n4", hostProfs[2].Name)
require.NotNil(t, hostProfs[2].Status)
require.Equal(t, fleet.MDMAppleDeliveryVerifying, *hostProfs[2].Status)
}
func createHostThenEnrollMDM(ds fleet.Datastore, fleetServerURL string, t *testing.T) (*fleet.Host, *mdmtest.TestMDMClient) {
+4
View File
@@ -92,6 +92,9 @@ func newTestServiceWithConfig(t *testing.T, ds fleet.Datastore, fleetConfig conf
ssoStore = sso.NewSessionStore(opts[0].Pool)
profMatcher = apple_mdm.NewProfileMatcher(opts[0].Pool)
}
if opts[0].ProfileMatcher != nil {
profMatcher = opts[0].ProfileMatcher
}
if opts[0].FailingPolicySet != nil {
failingPolicySet = opts[0].FailingPolicySet
}
@@ -271,6 +274,7 @@ type TestServerOpts struct {
StartCronSchedules []TestNewScheduleFunc
UseMailService bool
APNSTopic string
ProfileMatcher fleet.ProfileMatcher
}
func RunServerForTestsWithDS(t *testing.T, ds fleet.Datastore, opts ...*TestServerOpts) (map[string]fleet.User, *httptest.Server) {