diff --git a/ee/server/service/certificate_authorities.go b/ee/server/service/certificate_authorities.go
index 973580b89c..508a20fae9 100644
--- a/ee/server/service/certificate_authorities.go
+++ b/ee/server/service/certificate_authorities.go
@@ -240,7 +240,7 @@ func validateDigicertCACN(cn string, errPrefix string) error {
return fleet.NewInvalidArgumentError("certificate_common_name", fmt.Sprintf("%sCA Common Name (CN) cannot be empty", errPrefix))
}
fleetVars := variables.Find(cn)
- for fleetVar := range fleetVars {
+ for _, fleetVar := range fleetVars {
switch fleetVar {
case string(fleet.FleetVarHostEndUserEmailIDP), string(fleet.FleetVarHostHardwareSerial):
// ok
@@ -262,7 +262,7 @@ func validateDigicertSeatID(seatID string, errPrefix string) error {
return fleet.NewInvalidArgumentError("certificate_seat_id", fmt.Sprintf("%sCA Seat ID cannot be empty", errPrefix))
}
fleetVars := variables.Find(seatID)
- for fleetVar := range fleetVars {
+ for _, fleetVar := range fleetVars {
switch fleetVar {
case string(fleet.FleetVarHostEndUserEmailIDP), string(fleet.FleetVarHostHardwareSerial):
// ok
@@ -286,7 +286,7 @@ func validateDigicertUserPrincipalNames(userPrincipalNames []string, errPrefix s
fmt.Sprintf("%sDigiCert certificate_user_principal_name cannot be empty if specified", errPrefix))
}
fleetVars := variables.Find(userPrincipalNames[0])
- for fleetVar := range fleetVars {
+ for _, fleetVar := range fleetVars {
switch fleetVar {
case string(fleet.FleetVarHostEndUserEmailIDP), string(fleet.FleetVarHostHardwareSerial):
// ok
diff --git a/server/fleet/hosts.go b/server/fleet/hosts.go
index 5e3b70f7e2..97b3a11606 100644
--- a/server/fleet/hosts.go
+++ b/server/fleet/hosts.go
@@ -1557,3 +1557,58 @@ func (params *AddHostsToTeamParams) WithBatchSize(batchSize uint) *AddHostsToTea
params.BatchSize = batchSize
return params
}
+
+func GetEndUsers(ctx context.Context, ds Datastore, hostID uint) ([]HostEndUser, error) {
+ scimUser, err := ds.ScimUserByHostID(ctx, hostID)
+ if err != nil && !IsNotFound(err) {
+ return nil, fmt.Errorf("get scim user by host id: %w", err)
+ }
+
+ var endUsers []HostEndUser
+ if scimUser != nil {
+ endUser := HostEndUser{
+ IdpUserName: scimUser.UserName,
+ IdpFullName: scimUser.DisplayName(),
+ IdpInfoUpdatedAt: ptr.Time(scimUser.UpdatedAt),
+ }
+
+ if scimUser.ExternalID != nil {
+ endUser.IdpID = *scimUser.ExternalID
+ }
+ for _, group := range scimUser.Groups {
+ endUser.IdpGroups = append(endUser.IdpGroups, group.DisplayName)
+ }
+ if scimUser.Department != nil {
+ endUser.Department = *scimUser.Department
+ }
+ endUsers = append(endUsers, endUser)
+ }
+
+ deviceMapping, err := ds.ListHostDeviceMapping(ctx, hostID)
+ if err != nil {
+ return nil, fmt.Errorf("get host device mapping: %w", err)
+ }
+
+ if len(deviceMapping) > 0 {
+ endUser := HostEndUser{}
+ for _, email := range deviceMapping {
+ switch {
+ case (email.Source == DeviceMappingMDMIdpAccounts || email.Source == DeviceMappingIDP) && len(endUsers) == 0:
+ // If SCIM data is missing, we still populate IdpUserName if present.
+ // For DeviceMappingIDP source, this is the user-provided IDP username.
+ // Note: Username and email is the same thing here until we split them with https://github.com/fleetdm/fleet/issues/27952
+ endUser.IdpUserName = email.Email
+ case email.Source != DeviceMappingMDMIdpAccounts && email.Source != DeviceMappingIDP:
+ // Only add to OtherEmails if it's not an IDP source
+ endUser.OtherEmails = append(endUser.OtherEmails, *email)
+ }
+ }
+ if len(endUsers) > 0 {
+ endUsers[0].OtherEmails = endUser.OtherEmails
+ } else {
+ endUsers = append(endUsers, endUser)
+ }
+ }
+
+ return endUsers, nil
+}
diff --git a/server/fleet/mdm.go b/server/fleet/mdm.go
index 3a8a7a2db3..239e6ef203 100644
--- a/server/fleet/mdm.go
+++ b/server/fleet/mdm.go
@@ -46,7 +46,7 @@ const (
// (not doing it now because of time constraints to finish the story for the release).
// Host variables
- FleetVarHostEndUserEmailIDP FleetVarName = "HOST_END_USER_EMAIL_IDP"
+ FleetVarHostEndUserEmailIDP FleetVarName = "HOST_END_USER_EMAIL_IDP" // legacy variable, avoid to use in new replacements
FleetVarHostHardwareSerial FleetVarName = "HOST_HARDWARE_SERIAL"
FleetVarHostEndUserIDPUsername FleetVarName = "HOST_END_USER_IDP_USERNAME"
FleetVarHostEndUserIDPUsernameLocalPart FleetVarName = "HOST_END_USER_IDP_USERNAME_LOCAL_PART"
@@ -73,7 +73,6 @@ const (
var (
// Fleet variable regexp patterns
- FleetVarHostEndUserEmailIDPRegexp = regexp.MustCompile(fmt.Sprintf(`(\$FLEET_VAR_%s)|(\${FLEET_VAR_%[1]s})`, FleetVarHostEndUserEmailIDP))
FleetVarHostHardwareSerialRegexp = regexp.MustCompile(fmt.Sprintf(`(\$FLEET_VAR_%s)|(\${FLEET_VAR_%[1]s})`, FleetVarHostHardwareSerial))
FleetVarHostEndUserIDPUsernameRegexp = regexp.MustCompile(fmt.Sprintf(`(\$FLEET_VAR_%s)|(\${FLEET_VAR_%[1]s})`, FleetVarHostEndUserIDPUsername))
FleetVarHostEndUserIDPDepartmentRegexp = regexp.MustCompile(fmt.Sprintf(`(\$FLEET_VAR_%s)|(\${FLEET_VAR_%[1]s})`, FleetVarHostEndUserIDPDepartment))
@@ -90,6 +89,14 @@ var (
HostEndUserEmailIDPVariableReplacementFailedError = fmt.Sprintf("There is no IdP email for this host. "+
"Fleet couldn't populate $FLEET_VAR_%s. "+
"[Learn more](https://fleetdm.com/learn-more-about/idp-email)", FleetVarHostEndUserEmailIDP)
+
+ IDPFleetVariables = []FleetVarName{
+ FleetVarHostEndUserIDPUsername,
+ FleetVarHostEndUserIDPUsernameLocalPart,
+ FleetVarHostEndUserIDPGroups,
+ FleetVarHostEndUserIDPDepartment,
+ FleetVarHostEndUserIDPFullname,
+ }
)
type AppleMDM struct {
@@ -1123,17 +1130,3 @@ type MDMCommandResults interface {
}
type MDMCommandResultsHandler func(ctx context.Context, results MDMCommandResults) error
-
-// Helper function for variable replacement in MDM profiles
-func GetFirstIDPEmail(ctx context.Context, ds Datastore, hostUUID string) (email *string, err error) {
- // TODO: Should we check on another type of device mapping? Instead of mdm_idp_accounts source.
- emails, err := ds.GetHostEmails(ctx, hostUUID, DeviceMappingMDMIdpAccounts)
- if err != nil {
- // This is a server error, so we exit.
- return nil, err
- }
- if len(emails) == 0 {
- return nil, nil
- }
- return &emails[0], nil
-}
diff --git a/server/mdm/microsoft/profile_verifier.go b/server/mdm/microsoft/profile_verifier.go
index bb67cb4f99..3f13ca0f40 100644
--- a/server/mdm/microsoft/profile_verifier.go
+++ b/server/mdm/microsoft/profile_verifier.go
@@ -40,6 +40,11 @@ func LoopOverExpectedHostProfiles(
if err != nil {
return fmt.Errorf("getting host profiles for verification: %w", err)
}
+
+ params := PreprocessingParameters{
+ HostIDForUUIDCache: make(map[string]uint),
+ }
+
for _, expectedProf := range profileMap {
expanded, err := ds.ExpandEmbeddedSecrets(ctx, string(expectedProf.RawProfile))
if err != nil {
@@ -48,7 +53,7 @@ func LoopOverExpectedHostProfiles(
// Process Fleet variables if present (similar to how it's done during profile deployment)
// This ensures we compare what was actually sent to the device
- processedContent := PreprocessWindowsProfileContentsForVerification(ctx, logger, ds, host.UUID, expectedProf.ProfileUUID, expanded)
+ processedContent := PreprocessWindowsProfileContentsForVerification(ctx, logger, ds, host.UUID, expectedProf.ProfileUUID, expanded, params)
expectedProf.RawProfile = []byte(processedContent)
var prof fleet.SyncMLCmd
@@ -280,13 +285,20 @@ func IsWin32OrDesktopBridgeADMXCSP(locURI string) bool {
return false
}
+// PreprocessingParameters holds parameters needed for preprocessing Windows profiles, for both verification and deployment only.
+// It should only contain helper stuff, and not core values such as hostUUID, profileUUID, etc.
+type PreprocessingParameters struct {
+ // a lookup map to avoid repeated datastore calls for hostID from hostUUID
+ HostIDForUUIDCache map[string]uint
+}
+
// PreprocessWindowsProfileContentsForVerification processes Windows configuration profiles to replace Fleet variables
// with the given host UUID for verification purposes.
//
// This function is similar to PreprocessWindowsProfileContentsForDeployment, but it does not require
// a datastore or logger since it only replaces certain fleet variables to avoid datastore unnecessary work.
-func PreprocessWindowsProfileContentsForVerification(ctx context.Context, logger kitlog.Logger, ds fleet.Datastore, hostUUID string, profileUUID string, profileContents string) string {
- replacedContents, _ := preprocessWindowsProfileContents(ctx, logger, ds, nil, true, hostUUID, "", profileUUID, nil, profileContents)
+func PreprocessWindowsProfileContentsForVerification(ctx context.Context, logger kitlog.Logger, ds fleet.Datastore, hostUUID string, profileUUID string, profileContents string, params PreprocessingParameters) string {
+ replacedContents, _ := preprocessWindowsProfileContents(ctx, logger, ds, nil, true, hostUUID, "", profileUUID, nil, profileContents, params)
// ^ We ignore the error here, and rely on the fact that the function will return the original contents if no replacements were made.
// So verification fails on individual profile level, instead of entire verification failing.
return replacedContents
@@ -294,14 +306,18 @@ func PreprocessWindowsProfileContentsForVerification(ctx context.Context, logger
// PreprocessWindowsProfileContentsForDeployment processes Windows configuration profiles to replace Fleet variables
// with their actual values for each host during profile deployment.
-func PreprocessWindowsProfileContentsForDeployment(ctx context.Context, logger kitlog.Logger, ds fleet.Datastore, appConfig *fleet.AppConfig, hostUUID string, hostCmdUUID string, profileUUID string, groupedCAs *fleet.GroupedCertificateAuthorities, profileContents string) (string, error) {
+func PreprocessWindowsProfileContentsForDeployment(ctx context.Context, logger kitlog.Logger, ds fleet.Datastore,
+ appConfig *fleet.AppConfig, hostUUID string, hostCmdUUID string, profileUUID string,
+ groupedCAs *fleet.GroupedCertificateAuthorities, profileContents string,
+ params PreprocessingParameters,
+) (string, error) {
// TODO: Should we avoid iterating this list for every profile?
customSCEPCAs := make(map[string]*fleet.CustomSCEPProxyCA, len(groupedCAs.CustomScepProxy))
for _, ca := range groupedCAs.CustomScepProxy {
customSCEPCAs[ca.Name] = &ca
}
- return preprocessWindowsProfileContents(ctx, logger, ds, appConfig, false, hostUUID, hostCmdUUID, profileUUID, customSCEPCAs, profileContents)
+ return preprocessWindowsProfileContents(ctx, logger, ds, appConfig, false, hostUUID, hostCmdUUID, profileUUID, customSCEPCAs, profileContents, params)
}
// This error type is used to indicate errors during Microsoft profile processing, such as variable replacement failures.
@@ -339,6 +355,7 @@ func (e *MicrosoftProfileProcessingError) Error() string {
func preprocessWindowsProfileContents(ctx context.Context, logger kitlog.Logger, ds fleet.Datastore, appConfig *fleet.AppConfig,
isVerifying bool, hostUUID string, hostCmdUUID string, profileUUID string,
customSCEPCAs map[string]*fleet.CustomSCEPProxyCA, profileContents string,
+ params PreprocessingParameters,
) (string, error) {
// Check if Fleet variables are present
fleetVars := variables.Find(profileContents)
@@ -349,16 +366,18 @@ func preprocessWindowsProfileContents(ctx context.Context, logger kitlog.Logger,
// Process each Fleet variable
result := profileContents
- for fleetVar := range fleetVars {
+ for _, fleetVar := range fleetVars {
if fleetVar == string(fleet.FleetVarHostUUID) {
result = profiles.ReplaceFleetVariableInXML(fleet.FleetVarHostUUIDRegexp, result, hostUUID)
- } else if fleetVar == string(fleet.FleetVarHostEndUserEmailIDP) {
- replacedContents, replacedVariable, err := profiles.ReplaceHostEndUserEmailIDPVariable(ctx, ds, result, hostUUID)
+ } else if slices.Contains(fleet.IDPFleetVariables, fleet.FleetVarName(fleetVar)) {
+ replacedContents, replacedVariable, err := profiles.ReplaceHostEndUserIDPVariables(ctx, ds, fleetVar, result, hostUUID, params.HostIDForUUIDCache, func(errMsg string) error {
+ return &MicrosoftProfileProcessingError{message: errMsg}
+ })
if err != nil {
- return profileContents, ctxerr.Wrap(ctx, err, "replacing host end user email IDP variable")
+ return profileContents, err
}
if !replacedVariable {
- return profileContents, &MicrosoftProfileProcessingError{message: fleet.HostEndUserEmailIDPVariableReplacementFailedError}
+ return profileContents, ctxerr.Wrap(ctx, err, "host end user IDP variable replacement failed for variable")
}
result = replacedContents
}
diff --git a/server/mdm/microsoft/profile_verifier_test.go b/server/mdm/microsoft/profile_verifier_test.go
index 3157d145b2..3570615da5 100644
--- a/server/mdm/microsoft/profile_verifier_test.go
+++ b/server/mdm/microsoft/profile_verifier_test.go
@@ -3,6 +3,7 @@ package microsoft_mdm
import (
"context"
"encoding/xml"
+ "fmt"
"io"
"strings"
"testing"
@@ -825,13 +826,6 @@ type hostProfile struct {
func TestPreprocessWindowsProfileContentsForVerification(t *testing.T) {
ds := new(mock.Store)
- ds.GetHostEmailsFunc = func(ctx context.Context, hostUUID, source string) ([]string, error) {
- if source == fleet.DeviceMappingMDMIdpAccounts && strings.Contains(hostUUID, "end-user-email") {
- return []string{"test@idp.com"}, nil
- }
- return nil, nil
- }
-
tests := []struct {
name string
hostUUID string
@@ -892,12 +886,6 @@ func TestPreprocessWindowsProfileContentsForVerification(t *testing.T) {
profileContents: `ID1: $FLEET_VAR_HOST_UUID, ID2: ${FLEET_VAR_HOST_UUID}`,
expectedContents: `ID1: test-host-1234-uuid, ID2: test-host-1234-uuid`,
},
- {
- name: "fleet variable with db access",
- hostUUID: "test-host-end-user-email",
- profileContents: `ID: $FLEET_VAR_HOST_UUID, Other: $FLEET_VAR_HOST_END_USER_EMAIL_IDP`,
- expectedContents: `ID: test-host-end-user-email, Other: test@idp.com`,
- },
{
name: "skips scep windows id var",
hostUUID: "test-host-1234-uuid",
@@ -906,9 +894,13 @@ func TestPreprocessWindowsProfileContentsForVerification(t *testing.T) {
},
}
+ params := PreprocessingParameters{
+ HostIDForUUIDCache: make(map[string]uint),
+ }
+
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
- result := PreprocessWindowsProfileContentsForVerification(t.Context(), log.NewNopLogger(), ds, tt.hostUUID, uuid.NewString(), tt.profileContents)
+ result := PreprocessWindowsProfileContentsForVerification(t.Context(), log.NewNopLogger(), ds, tt.hostUUID, uuid.NewString(), tt.profileContents, params)
require.Equal(t, tt.expectedContents, result)
})
}
@@ -918,12 +910,6 @@ func TestPreprocessWindowsProfileContentsForDeployment(t *testing.T) {
ds := new(mock.Store)
baseSetup := func() {
- ds.GetHostEmailsFunc = func(ctx context.Context, hostUUID, source string) ([]string, error) {
- if source == fleet.DeviceMappingMDMIdpAccounts && strings.Contains(hostUUID, "end-user-email") {
- return []string{"test@idp.com"}, nil
- }
- return nil, nil
- }
ds.GetGroupedCertificateAuthoritiesFunc = func(ctx context.Context, includeSecrets bool) (*fleet.GroupedCertificateAuthorities, error) {
if ds.GetAllCertificateAuthoritiesFunc == nil {
return &fleet.GroupedCertificateAuthorities{
@@ -945,6 +931,34 @@ func TestPreprocessWindowsProfileContentsForDeployment(t *testing.T) {
},
}, nil
}
+ ds.HostIDsByIdentifierFunc = func(ctx context.Context, filter fleet.TeamFilter, hostnames []string) ([]uint, error) {
+ return []uint{42}, nil
+ }
+ ds.ScimUserByHostIDFunc = func(ctx context.Context, hostID uint) (*fleet.ScimUser, error) {
+ if hostID == 42 {
+ return &fleet.ScimUser{
+ UserName: "test@idp.com",
+ GivenName: ptr.String("First"),
+ FamilyName: ptr.String("Last"),
+ Department: ptr.String("Department"),
+ Groups: []fleet.ScimUserGroup{
+ {
+ ID: 1,
+ DisplayName: "Group One",
+ },
+ {
+ ID: 2,
+ DisplayName: "Group Two",
+ },
+ },
+ }, nil
+ }
+
+ return nil, fmt.Errorf("no scim user for host id %d", hostID)
+ }
+ ds.ListHostDeviceMappingFunc = func(ctx context.Context, id uint) ([]*fleet.HostDeviceMapping, error) {
+ return []*fleet.HostDeviceMapping{}, nil
+ }
}
// use the same uuid for all profile UUID actions
@@ -973,19 +987,6 @@ func TestPreprocessWindowsProfileContentsForDeployment(t *testing.T) {
profileContents: `- ./Device/TestDevice ID: $FLEET_VAR_HOST_UUID
`,
expectedContents: `- ./Device/TestDevice ID: test-uuid-456
`,
},
- {
- name: "host end user email idp",
- hostUUID: "test-uuid-end-user-email",
- profileContents: `Email: $FLEET_VAR_HOST_END_USER_EMAIL_IDP`,
- expectedContents: `Email: test@idp.com`,
- },
- {
- name: "no host end user email idp found",
- hostUUID: "test-uuid-no-end-user",
- profileContents: `Email: $FLEET_VAR_HOST_END_USER_EMAIL_IDP`,
- expectError: true,
- processingError: fleet.HostEndUserEmailIDPVariableReplacementFailedError,
- },
{
name: "scep windows certificate id",
hostUUID: "test-host-1234-uuid",
@@ -1073,6 +1074,17 @@ func TestPreprocessWindowsProfileContentsForDeployment(t *testing.T) {
}
},
},
+ {
+ name: "all idp variables",
+ hostUUID: "idp-host-uuid",
+ hostCmdUUID: "cmd-uuid-5678",
+ profileContents: `- ./Device/TestUser: $FLEET_VAR_HOST_END_USER_IDP_USERNAME - $FLEET_VAR_HOST_END_USER_IDP_USERNAME_LOCAL_PART - $FLEET_VAR_HOST_END_USER_IDP_GROUPS - $FLEET_VAR_HOST_END_USER_IDP_DEPARTMENT - $FLEET_VAR_HOST_END_USER_IDP_FULL_NAME
`,
+ expectedContents: `- ./Device/TestUser: test@idp.com - test - Group One,Group Two - Department - First Last
`,
+ },
+ }
+
+ params := PreprocessingParameters{
+ HostIDForUUIDCache: make(map[string]uint),
}
for _, tt := range tests {
@@ -1100,7 +1112,7 @@ func TestPreprocessWindowsProfileContentsForDeployment(t *testing.T) {
groupedCAs, err := ds.GetGroupedCertificateAuthorities(ctx, true)
require.NoError(t, err)
- result, err := PreprocessWindowsProfileContentsForDeployment(ctx, log.NewNopLogger(), ds, appConfig, tt.hostUUID, tt.hostCmdUUID, profileUUID, groupedCAs, tt.profileContents)
+ result, err := PreprocessWindowsProfileContentsForDeployment(ctx, log.NewNopLogger(), ds, appConfig, tt.hostUUID, tt.hostCmdUUID, profileUUID, groupedCAs, tt.profileContents, params)
if tt.expectError {
require.Error(t, err)
if tt.processingError != "" {
diff --git a/server/mdm/profiles/profile_variables.go b/server/mdm/profiles/profile_variables.go
index 03998cf589..fc82f8e288 100644
--- a/server/mdm/profiles/profile_variables.go
+++ b/server/mdm/profiles/profile_variables.go
@@ -13,6 +13,7 @@ import (
"github.com/fleetdm/fleet/v4/server/contexts/license"
"github.com/fleetdm/fleet/v4/server/fleet"
apple_mdm "github.com/fleetdm/fleet/v4/server/mdm/apple"
+ "github.com/fleetdm/fleet/v4/server/ptr"
kitlog "github.com/go-kit/log"
"github.com/go-kit/log/level"
)
@@ -79,18 +80,109 @@ func ReplaceCustomSCEPProxyURLVariable(ctx context.Context, logger kitlog.Logger
return contents, managedCertificate, true, nil
}
-// ! Important if we add new replacedVariable=false cases, that we verify the caller functions still behave correctly, as some run actions based on whether a variable was replaced or not.
-func ReplaceHostEndUserEmailIDPVariable(ctx context.Context, ds fleet.Datastore, profileContents string, hostUUID string) (contents string, replacedVariable bool, err error) {
- email, err := fleet.GetFirstIDPEmail(ctx, ds, hostUUID)
+func ReplaceHostEndUserIDPVariables(ctx context.Context, ds fleet.Datastore,
+ fleetVar string, profileContents string, hostUUID string,
+ hostIDForUUIDCache map[string]uint,
+ onError func(errMsg string) error,
+) (replacedContents string, replacedVariable bool, err error) {
+ user, ok, err := getHostEndUserIDPUser(ctx, ds, hostUUID, fleetVar, hostIDForUUIDCache, onError)
if err != nil {
- return "", false, ctxerr.Wrap(ctx, err, "getting IDP email")
+ return "", false, err
}
- if email == nil {
+ if !ok {
return "", false, nil
}
- contents = ReplaceFleetVariableInXML(fleet.FleetVarHostEndUserEmailIDPRegexp, profileContents, *email)
- return contents, true, nil
+ var rx *regexp.Regexp
+ var value string
+ switch fleetVar {
+ case string(fleet.FleetVarHostEndUserIDPUsername):
+ rx = fleet.FleetVarHostEndUserIDPUsernameRegexp
+ value = user.IdpUserName
+ case string(fleet.FleetVarHostEndUserIDPUsernameLocalPart):
+ rx = fleet.FleetVarHostEndUserIDPUsernameLocalPartRegexp
+ value = getEmailLocalPart(user.IdpUserName)
+ case string(fleet.FleetVarHostEndUserIDPGroups):
+ rx = fleet.FleetVarHostEndUserIDPGroupsRegexp
+ value = strings.Join(user.IdpGroups, ",")
+ case string(fleet.FleetVarHostEndUserIDPDepartment):
+ rx = fleet.FleetVarHostEndUserIDPDepartmentRegexp
+ value = user.Department
+ case string(fleet.FleetVarHostEndUserIDPFullname):
+ rx = fleet.FleetVarHostEndUserIDPFullnameRegexp
+ value = strings.TrimSpace(user.IdpFullName)
+ }
+ replacedContents = ReplaceFleetVariableInXML(rx, profileContents, value)
+
+ return replacedContents, true, nil
+}
+
+func getHostEndUserIDPUser(ctx context.Context, ds fleet.Datastore,
+ hostUUID, fleetVar string, hostIDForUUIDCache map[string]uint,
+ onError func(errMsg string) error,
+) (*fleet.HostEndUser, bool, error) {
+ hostID, ok := hostIDForUUIDCache[hostUUID]
+ if !ok {
+ filter := fleet.TeamFilter{User: &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)}}
+ ids, err := ds.HostIDsByIdentifier(ctx, filter, []string{hostUUID})
+ if err != nil {
+ return nil, false, ctxerr.Wrap(ctx, err, "get host id from uuid")
+ }
+
+ if len(ids) != 1 {
+ // Something went wrong. Maybe host was deleted, or we have multiple
+ // hosts with the same UUID.
+ return nil, false, onError(fmt.Sprintf("Unexpected number of hosts (%d) for UUID %s. ", len(ids), hostUUID))
+ }
+ hostID = ids[0]
+ hostIDForUUIDCache[hostUUID] = hostID
+ }
+
+ users, err := fleet.GetEndUsers(ctx, ds, hostID)
+ if err != nil {
+ return nil, false, ctxerr.Wrap(ctx, err, "get end users for host")
+ }
+
+ noGroupsErr := fmt.Sprintf("There is no IdP groups for this host. Fleet couldn’t populate $FLEET_VAR_%s.", fleet.FleetVarHostEndUserIDPGroups)
+ noDepartmentErr := fmt.Sprintf("There is no IdP department for this host. Fleet couldn’t populate $FLEET_VAR_%s.", fleet.FleetVarHostEndUserIDPDepartment)
+ noFullnameErr := fmt.Sprintf("There is no IdP full name for this host. Fleet couldn’t populate $FLEET_VAR_%s.", fleet.FleetVarHostEndUserIDPFullname)
+ if len(users) > 0 && users[0].IdpUserName != "" {
+ idpUser := users[0]
+
+ if fleetVar == string(fleet.FleetVarHostEndUserIDPGroups) && len(idpUser.IdpGroups) == 0 {
+ return nil, false, onError(noGroupsErr)
+ }
+ if fleetVar == string(fleet.FleetVarHostEndUserIDPDepartment) && idpUser.Department == "" {
+ return nil, false, onError(noDepartmentErr)
+ }
+ if fleetVar == string(fleet.FleetVarHostEndUserIDPFullname) && strings.TrimSpace(idpUser.IdpFullName) == "" {
+ return nil, false, onError(noFullnameErr)
+ }
+
+ return &idpUser, true, nil
+ }
+
+ // otherwise there's no IdP user, mark the profile as failed with the
+ // appropriate detail message.
+ var detail string
+ switch fleetVar {
+ case string(fleet.FleetVarHostEndUserIDPUsername), string(fleet.FleetVarHostEndUserIDPUsernameLocalPart):
+ detail = fmt.Sprintf("There is no IdP username for this host. Fleet couldn't populate $FLEET_VAR_%s.", fleetVar)
+ case string(fleet.FleetVarHostEndUserIDPGroups):
+ detail = noGroupsErr
+ case string(fleet.FleetVarHostEndUserIDPDepartment):
+ detail = noDepartmentErr
+ case string(fleet.FleetVarHostEndUserIDPFullname):
+ detail = noFullnameErr
+ }
+ return nil, false, onError(detail)
+}
+
+func getEmailLocalPart(email string) string {
+ // if there is a "@" in the email, return the part before that "@", otherwise
+ // return the string unchanged.
+ local, _, _ := strings.Cut(email, "@")
+ return local
}
func ReplaceExactFleetPrefixVariableInXML(prefix string, suffix string, contents string, replacement string) (string, error) {
diff --git a/server/service/apple_mdm.go b/server/service/apple_mdm.go
index 5c32bb4303..064ed76bf2 100644
--- a/server/service/apple_mdm.go
+++ b/server/service/apple_mdm.go
@@ -70,16 +70,11 @@ const (
)
var (
- fleetVarHostEndUserEmailIDPRegexp = regexp.MustCompile(fmt.Sprintf(`(\$FLEET_VAR_%s)|(\${FLEET_VAR_%[1]s})`, fleet.FleetVarHostEndUserEmailIDP))
- fleetVarHostHardwareSerialRegexp = regexp.MustCompile(fmt.Sprintf(`(\$FLEET_VAR_%s)|(\${FLEET_VAR_%[1]s})`, fleet.FleetVarHostHardwareSerial))
- fleetVarHostEndUserIDPUsernameRegexp = regexp.MustCompile(fmt.Sprintf(`(\$FLEET_VAR_%s)|(\${FLEET_VAR_%[1]s})`, fleet.FleetVarHostEndUserIDPUsername))
- fleetVarHostEndUserIDPDepartmentRegexp = regexp.MustCompile(fmt.Sprintf(`(\$FLEET_VAR_%s)|(\${FLEET_VAR_%[1]s})`, fleet.FleetVarHostEndUserIDPDepartment))
- fleetVarHostEndUserIDPUsernameLocalPartRegexp = regexp.MustCompile(fmt.Sprintf(`(\$FLEET_VAR_%s)|(\${FLEET_VAR_%[1]s})`, fleet.FleetVarHostEndUserIDPUsernameLocalPart))
- fleetVarHostEndUserIDPGroupsRegexp = regexp.MustCompile(fmt.Sprintf(`(\$FLEET_VAR_%s)|(\${FLEET_VAR_%[1]s})`, fleet.FleetVarHostEndUserIDPGroups))
- fleetVarNDESSCEPChallengeRegexp = regexp.MustCompile(fmt.Sprintf(`(\$FLEET_VAR_%s)|(\${FLEET_VAR_%[1]s})`, fleet.FleetVarNDESSCEPChallenge))
- fleetVarNDESSCEPProxyURLRegexp = regexp.MustCompile(fmt.Sprintf(`(\$FLEET_VAR_%s)|(\${FLEET_VAR_%[1]s})`, fleet.FleetVarNDESSCEPProxyURL))
- fleetVarHostEndUserIDPFullnameRegexp = regexp.MustCompile(fmt.Sprintf(`(\$FLEET_VAR_%s)|(\${FLEET_VAR_%[1]s})`, fleet.FleetVarHostEndUserIDPFullname))
- fleetVarSCEPRenewalIDRegexp = regexp.MustCompile(fmt.Sprintf(`(\$FLEET_VAR_%s)|(\${FLEET_VAR_%[1]s})`, fleet.FleetVarSCEPRenewalID))
+ fleetVarHostEndUserEmailIDPRegexp = regexp.MustCompile(fmt.Sprintf(`(\$FLEET_VAR_%s)|(\${FLEET_VAR_%[1]s})`, fleet.FleetVarHostEndUserEmailIDP))
+ fleetVarHostHardwareSerialRegexp = regexp.MustCompile(fmt.Sprintf(`(\$FLEET_VAR_%s)|(\${FLEET_VAR_%[1]s})`, fleet.FleetVarHostHardwareSerial))
+ fleetVarNDESSCEPChallengeRegexp = regexp.MustCompile(fmt.Sprintf(`(\$FLEET_VAR_%s)|(\${FLEET_VAR_%[1]s})`, fleet.FleetVarNDESSCEPChallenge))
+ fleetVarNDESSCEPProxyURLRegexp = regexp.MustCompile(fmt.Sprintf(`(\$FLEET_VAR_%s)|(\${FLEET_VAR_%[1]s})`, fleet.FleetVarNDESSCEPProxyURL))
+ fleetVarSCEPRenewalIDRegexp = regexp.MustCompile(fmt.Sprintf(`(\$FLEET_VAR_%s)|(\${FLEET_VAR_%[1]s})`, fleet.FleetVarSCEPRenewalID))
// TODO(HCA): Can we come up with a clearer name? This looks like any variables not in this slice is not supported,
// but that is not the case, digicert, custom scep, hydrant and smallstep are totally supported just in a different way (multiple CA's)
@@ -456,7 +451,7 @@ func (svc *Service) NewMDMAppleConfigProfile(ctx context.Context, teamID uint, d
// Convert profile variable names to FleetVarName type
varNames := make([]fleet.FleetVarName, 0, len(profileVars))
- for varName := range profileVars {
+ for _, varName := range profileVars {
varNames = append(varNames, fleet.FleetVarName(varName))
}
newCP, err := svc.ds.NewMDMAppleConfigProfile(ctx, *cp, varNames)
@@ -512,7 +507,7 @@ func CheckProfileIsNotSigned(data []byte) error {
return nil
}
-func validateConfigProfileFleetVariables(contents string, lic *fleet.LicenseInfo, groupedCAs *fleet.GroupedCertificateAuthorities) (map[string]struct{}, error) {
+func validateConfigProfileFleetVariables(contents string, lic *fleet.LicenseInfo, groupedCAs *fleet.GroupedCertificateAuthorities) ([]string, error) {
fleetVars := variables.FindKeepDuplicates(contents)
if len(fleetVars) == 0 {
return nil, nil
@@ -673,12 +668,8 @@ func validateConfigProfileFleetVariables(contents string, lic *fleet.LicenseInfo
return nil, err
}
}
- // Convert slice to map for deduplication
- result := make(map[string]struct{}, len(fleetVars))
- for _, v := range fleetVars {
- result[v] = struct{}{}
- }
- return result, nil
+
+ return variables.Dedupe(fleetVars), nil
}
// additionalDigiCertValidation checks that Password/ContentType fields match DigiCert Fleet variables exactly,
@@ -5097,7 +5088,7 @@ func preprocessProfileContents(
// validation works as expected
// In the future we should expand variablesUpdatedAt logic to include non-CA variables as
// well
- for fleetVar := range fleetVars {
+ for _, fleetVar := range fleetVars {
if fleetVar == string(fleet.FleetVarSCEPRenewalID) ||
fleetVar == string(fleet.FleetVarNDESSCEPChallenge) || fleetVar == string(fleet.FleetVarNDESSCEPProxyURL) ||
strings.HasPrefix(fleetVar, string(fleet.FleetVarSmallstepSCEPChallengePrefix)) || strings.HasPrefix(fleetVar, string(fleet.FleetVarSmallstepSCEPProxyURLPrefix)) ||
@@ -5110,7 +5101,7 @@ func preprocessProfileContents(
}
initialFleetVarLoop:
- for fleetVar := range fleetVars {
+ for _, fleetVar := range fleetVars {
switch {
case fleetVar == string(fleet.FleetVarNDESSCEPChallenge) || fleetVar == string(fleet.FleetVarNDESSCEPProxyURL):
configured, err := isNDESSCEPConfigured(ctx, groupedCAs, ds, hostProfilesToInstallMap, userEnrollmentsToHostUUIDsMap, profUUID, target)
@@ -5233,7 +5224,7 @@ func preprocessProfileContents(
hostContents := contentsStr
failed := false
fleetVarLoop:
- for fleetVar := range fleetVars {
+ for _, fleetVar := range fleetVars {
var err error
switch {
case fleetVar == string(fleet.FleetVarNDESSCEPChallenge):
@@ -5371,26 +5362,15 @@ func preprocessProfileContents(
}
case fleetVar == string(fleet.FleetVarHostEndUserEmailIDP):
- replacedContents, replacedVariable, err := profiles.ReplaceHostEndUserEmailIDPVariable(ctx, ds, hostContents, hostUUID)
+ email, ok, err := getFirstIDPEmail(ctx, ds, target, hostUUID)
if err != nil {
- return ctxerr.Wrap(ctx, err, "replacing host end user email IDP variable")
+ return ctxerr.Wrap(ctx, err, "getting IDP email")
}
- if !replacedVariable {
- // We couldn't retrieve the end user email IDP, so mark the profile as failed with additional detail.
- err = ds.UpdateOrDeleteHostMDMAppleProfile(ctx, &fleet.HostMDMAppleProfile{
- CommandUUID: target.cmdUUID,
- HostUUID: hostUUID,
- Status: &fleet.MDMDeliveryFailed,
- Detail: fleet.HostEndUserEmailIDPVariableReplacementFailedError,
- OperationType: fleet.MDMOperationTypeInstall,
- })
- if err != nil {
- return ctxerr.Wrap(ctx, err, "updating host MDM Apple profile for host end user email IDP")
- }
+ if !ok {
failed = true
break fleetVarLoop
}
- hostContents = replacedContents
+ hostContents = profiles.ReplaceFleetVariableInXML(fleetVarHostEndUserEmailIDPRegexp, hostContents, email)
case fleetVar == string(fleet.FleetVarHostHardwareSerial):
hardwareSerial, ok, err := getHostHardwareSerial(ctx, ds, target, hostUUID)
@@ -5406,35 +5386,25 @@ func preprocessProfileContents(
case fleetVar == string(fleet.FleetVarHostEndUserIDPUsername) || fleetVar == string(fleet.FleetVarHostEndUserIDPUsernameLocalPart) ||
fleetVar == string(fleet.FleetVarHostEndUserIDPGroups) || fleetVar == string(fleet.FleetVarHostEndUserIDPDepartment) ||
fleetVar == string(fleet.FleetVarHostEndUserIDPFullname):
- user, ok, err := getHostEndUserIDPUser(ctx, ds, target, hostUUID, fleetVar, hostIDForUUIDCache)
+ replacedContents, replacedVariable, err := profiles.ReplaceHostEndUserIDPVariables(ctx, ds, fleetVar, hostContents, hostUUID, hostIDForUUIDCache, func(errMsg string) error {
+ err := ds.UpdateOrDeleteHostMDMAppleProfile(ctx, &fleet.HostMDMAppleProfile{
+ CommandUUID: target.cmdUUID,
+ HostUUID: hostUUID,
+ Status: &fleet.MDMDeliveryFailed,
+ Detail: errMsg,
+ OperationType: fleet.MDMOperationTypeInstall,
+ })
+ return err
+ })
if err != nil {
- return ctxerr.Wrap(ctx, err, "getting host end user IDP username")
+ return ctxerr.Wrap(ctx, err, "replacing host end user IDP variables")
}
- if !ok {
+ if !replacedVariable {
failed = true
break fleetVarLoop
}
- var rx *regexp.Regexp
- var value string
- switch fleetVar {
- case string(fleet.FleetVarHostEndUserIDPUsername):
- rx = fleetVarHostEndUserIDPUsernameRegexp
- value = user.IdpUserName
- case string(fleet.FleetVarHostEndUserIDPUsernameLocalPart):
- rx = fleetVarHostEndUserIDPUsernameLocalPartRegexp
- value = getEmailLocalPart(user.IdpUserName)
- case string(fleet.FleetVarHostEndUserIDPGroups):
- rx = fleetVarHostEndUserIDPGroupsRegexp
- value = strings.Join(user.IdpGroups, ",")
- case string(fleet.FleetVarHostEndUserIDPDepartment):
- rx = fleetVarHostEndUserIDPDepartmentRegexp
- value = user.Department
- case string(fleet.FleetVarHostEndUserIDPFullname):
- rx = fleetVarHostEndUserIDPFullnameRegexp
- value = strings.TrimSpace(user.IdpFullName)
- }
- hostContents = profiles.ReplaceFleetVariableInXML(rx, hostContents, value)
+ hostContents = replacedContents
case strings.HasPrefix(fleetVar, string(fleet.FleetVarDigiCertPasswordPrefix)):
// We will replace the password when we populate the certificate data
@@ -5554,35 +5524,50 @@ func preprocessProfileContents(
return nil
}
+func getFirstIDPEmail(ctx context.Context, ds fleet.Datastore, target *cmdTarget, hostUUID string) (string, bool, error) {
+ // Insert the end user email IDP into the profile contents
+ emails, err := ds.GetHostEmails(ctx, hostUUID, fleet.DeviceMappingMDMIdpAccounts)
+ if err != nil {
+ // This is a server error, so we exit.
+ return "", false, ctxerr.Wrap(ctx, err, "getting host emails")
+ }
+ if len(emails) == 0 {
+ // We couldn't retrieve the end user email IDP, so mark the profile as failed with additional detail.
+ err := ds.UpdateOrDeleteHostMDMAppleProfile(ctx, &fleet.HostMDMAppleProfile{
+ CommandUUID: target.cmdUUID,
+ HostUUID: hostUUID,
+ Status: &fleet.MDMDeliveryFailed,
+ Detail: fmt.Sprintf("There is no IdP email for this host. "+
+ "Fleet couldn't populate $FLEET_VAR_%s. "+
+ "[Learn more](https://fleetdm.com/learn-more-about/idp-email)",
+ fleet.FleetVarHostEndUserEmailIDP),
+ OperationType: fleet.MDMOperationTypeInstall,
+ })
+ if err != nil {
+ return "", false, ctxerr.Wrap(ctx, err, "updating host MDM Apple profile for end user email IdP")
+ }
+ return "", false, nil
+ }
+ return emails[0], true, nil
+}
+
func replaceFleetVarInItem(ctx context.Context, ds fleet.Datastore, target *cmdTarget, hostUUID string, caVarsCache map[string]string, item *string,
) (bool, error) {
caFleetVars := variables.Find(*item)
- for caVar := range caFleetVars {
+ for _, caVar := range caFleetVars {
switch caVar {
case string(fleet.FleetVarHostEndUserEmailIDP):
email, ok := caVarsCache[string(fleet.FleetVarHostEndUserEmailIDP)]
if !ok {
var err error
- foundEmail, err := fleet.GetFirstIDPEmail(ctx, ds, hostUUID)
+ email, ok, err = getFirstIDPEmail(ctx, ds, target, hostUUID)
if err != nil {
return false, ctxerr.Wrap(ctx, err, "getting IDP email")
}
- if foundEmail == nil {
- // We couldn't retrieve the end user email IDP, so mark the profile as failed with additional detail.
- err := ds.UpdateOrDeleteHostMDMAppleProfile(ctx, &fleet.HostMDMAppleProfile{
- CommandUUID: target.cmdUUID,
- HostUUID: hostUUID,
- Status: &fleet.MDMDeliveryFailed,
- Detail: fleet.HostEndUserEmailIDPVariableReplacementFailedError,
- OperationType: fleet.MDMOperationTypeInstall,
- })
- if err != nil {
- return false, err
- }
+ if !ok {
return false, nil
}
- caVarsCache[string(fleet.FleetVarHostEndUserEmailIDP)] = *foundEmail
- email = *foundEmail
+ caVarsCache[string(fleet.FleetVarHostEndUserEmailIDP)] = email
}
*item = profiles.ReplaceFleetVariableInXML(fleetVarHostEndUserEmailIDPRegexp, *item, email)
case string(fleet.FleetVarHostHardwareSerial):
@@ -5606,124 +5591,6 @@ func replaceFleetVarInItem(ctx context.Context, ds fleet.Datastore, target *cmdT
return true, nil
}
-func getHostEndUserIDPUser(ctx context.Context, ds fleet.Datastore, target *cmdTarget,
- hostUUID, fleetVar string, hostIDForUUIDCache map[string]uint,
-) (*fleet.HostEndUser, bool, error) {
- hostID, ok := hostIDForUUIDCache[hostUUID]
- if !ok {
- filter := fleet.TeamFilter{User: &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)}}
- ids, err := ds.HostIDsByIdentifier(ctx, filter, []string{hostUUID})
- if err != nil {
- return nil, false, ctxerr.Wrap(ctx, err, "get host id from uuid")
- }
-
- if len(ids) != 1 {
- // Something went wrong. Maybe host was deleted, or we have multiple
- // hosts with the same UUID. Mark the profile as failed with additional
- // detail.
- err := ds.UpdateOrDeleteHostMDMAppleProfile(ctx, &fleet.HostMDMAppleProfile{
- CommandUUID: target.cmdUUID,
- HostUUID: hostUUID,
- Status: &fleet.MDMDeliveryFailed,
- Detail: fmt.Sprintf("Unexpected number of hosts (%d) for UUID %s. ", len(ids), hostUUID),
- OperationType: fleet.MDMOperationTypeInstall,
- })
- if err != nil {
- return nil, false, ctxerr.Wrap(ctx, err, "updating host MDM Apple profile for end user IDP")
- }
- return nil, false, nil
- }
- hostID = ids[0]
- hostIDForUUIDCache[hostUUID] = hostID
- }
-
- users, err := getEndUsers(ctx, ds, hostID)
- if err != nil {
- return nil, false, ctxerr.Wrap(ctx, err, "get end users for host")
- }
-
- noGroupsErr := fmt.Sprintf("There is no IdP groups for this host. Fleet couldn’t populate $FLEET_VAR_%s.", fleet.FleetVarHostEndUserIDPGroups)
- noDepartmentErr := fmt.Sprintf("There is no IdP department for this host. Fleet couldn’t populate $FLEET_VAR_%s.", fleet.FleetVarHostEndUserIDPDepartment)
- noFullnameErr := fmt.Sprintf("There is no IdP full name for this host. Fleet couldn’t populate $FLEET_VAR_%s.", fleet.FleetVarHostEndUserIDPFullname)
- if len(users) > 0 && users[0].IdpUserName != "" {
- idpUser := users[0]
-
- if fleetVar == string(fleet.FleetVarHostEndUserIDPGroups) && len(idpUser.IdpGroups) == 0 {
- err = ds.UpdateOrDeleteHostMDMAppleProfile(ctx, &fleet.HostMDMAppleProfile{
- CommandUUID: target.cmdUUID,
- HostUUID: hostUUID,
- Status: &fleet.MDMDeliveryFailed,
- Detail: noGroupsErr,
- OperationType: fleet.MDMOperationTypeInstall,
- })
- if err != nil {
- return nil, false, ctxerr.Wrap(ctx, err, "updating host MDM Apple profile for end user IDP (no groups)")
- }
- return nil, false, nil
- }
- if fleetVar == string(fleet.FleetVarHostEndUserIDPDepartment) && idpUser.Department == "" {
- err = ds.UpdateOrDeleteHostMDMAppleProfile(ctx, &fleet.HostMDMAppleProfile{
- CommandUUID: target.cmdUUID,
- HostUUID: hostUUID,
- Status: &fleet.MDMDeliveryFailed,
- Detail: noDepartmentErr,
- OperationType: fleet.MDMOperationTypeInstall,
- })
- if err != nil {
- return nil, false, ctxerr.Wrap(ctx, err, "updating host MDM Apple profile for end user IDP (no department)")
- }
- return nil, false, nil
- }
- if fleetVar == string(fleet.FleetVarHostEndUserIDPFullname) && strings.TrimSpace(idpUser.IdpFullName) == "" {
- err = ds.UpdateOrDeleteHostMDMAppleProfile(ctx, &fleet.HostMDMAppleProfile{
- CommandUUID: target.cmdUUID,
- HostUUID: hostUUID,
- Status: &fleet.MDMDeliveryFailed,
- Detail: noFullnameErr,
- OperationType: fleet.MDMOperationTypeInstall,
- })
- if err != nil {
- return nil, false, ctxerr.Wrap(ctx, err, "updating host MDM Apple profile for end user IDP (no fullname)")
- }
- return nil, false, nil
- }
-
- return &idpUser, true, nil
- }
-
- // otherwise there's no IdP user, mark the profile as failed with the
- // appropriate detail message.
- var detail string
- switch fleetVar {
- case string(fleet.FleetVarHostEndUserIDPUsername), string(fleet.FleetVarHostEndUserIDPUsernameLocalPart):
- detail = fmt.Sprintf("There is no IdP username for this host. Fleet couldn't populate $FLEET_VAR_%s.", fleetVar)
- case string(fleet.FleetVarHostEndUserIDPGroups):
- detail = noGroupsErr
- case string(fleet.FleetVarHostEndUserIDPDepartment):
- detail = noDepartmentErr
- case string(fleet.FleetVarHostEndUserIDPFullname):
- detail = noFullnameErr
- }
- err = ds.UpdateOrDeleteHostMDMAppleProfile(ctx, &fleet.HostMDMAppleProfile{
- CommandUUID: target.cmdUUID,
- HostUUID: hostUUID,
- Status: &fleet.MDMDeliveryFailed,
- Detail: detail,
- OperationType: fleet.MDMOperationTypeInstall,
- })
- if err != nil {
- return nil, false, ctxerr.Wrap(ctx, err, "updating host MDM Apple profile for end user IDP")
- }
- return nil, false, nil
-}
-
-func getEmailLocalPart(email string) string {
- // if there is a "@" in the email, return the part before that "@", otherwise
- // return the string unchanged.
- local, _, _ := strings.Cut(email, "@")
- return local
-}
-
func getHostHardwareSerial(ctx context.Context, ds fleet.Datastore, target *cmdTarget, hostUUID string) (string, bool, error) {
hosts, err := ds.ListHostsLiteByUUIDs(ctx, fleet.TeamFilter{User: &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)}}, []string{hostUUID})
if err != nil {
diff --git a/server/service/apple_mdm_test.go b/server/service/apple_mdm_test.go
index c09deb7f9e..8e81323487 100644
--- a/server/service/apple_mdm_test.go
+++ b/server/service/apple_mdm_test.go
@@ -5848,11 +5848,7 @@ func TestValidateConfigProfileFleetVariables(t *testing.T) {
assert.Empty(t, vars)
} else {
assert.NoError(t, err)
- gotVars := make([]string, 0, len(vars))
- for v := range vars {
- gotVars = append(gotVars, v)
- }
- assert.ElementsMatch(t, tc.vars, gotVars)
+ assert.ElementsMatch(t, tc.vars, vars)
}
})
}
diff --git a/server/service/hosts.go b/server/service/hosts.go
index 004d250acf..c2f02cb5fb 100644
--- a/server/service/hosts.go
+++ b/server/service/hosts.go
@@ -1385,7 +1385,7 @@ func (svc *Service) getHostDetails(ctx context.Context, host *fleet.Host, opts f
host.Policies = policies
- endUsers, err := getEndUsers(ctx, svc.ds, host.ID)
+ endUsers, err := fleet.GetEndUsers(ctx, svc.ds, host.ID)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "get end users for host")
}
@@ -1402,61 +1402,6 @@ func (svc *Service) getHostDetails(ctx context.Context, host *fleet.Host, opts f
}, nil
}
-func getEndUsers(ctx context.Context, ds fleet.Datastore, hostID uint) ([]fleet.HostEndUser, error) {
- scimUser, err := ds.ScimUserByHostID(ctx, hostID)
- if err != nil && !fleet.IsNotFound(err) {
- return nil, ctxerr.Wrap(ctx, err, "get scim user by host id")
- }
-
- var endUsers []fleet.HostEndUser
- if scimUser != nil {
- endUser := fleet.HostEndUser{
- IdpUserName: scimUser.UserName,
- IdpFullName: scimUser.DisplayName(),
- IdpInfoUpdatedAt: ptr.Time(scimUser.UpdatedAt),
- }
-
- if scimUser.ExternalID != nil {
- endUser.IdpID = *scimUser.ExternalID
- }
- for _, group := range scimUser.Groups {
- endUser.IdpGroups = append(endUser.IdpGroups, group.DisplayName)
- }
- if scimUser.Department != nil {
- endUser.Department = *scimUser.Department
- }
- endUsers = append(endUsers, endUser)
- }
-
- deviceMapping, err := ds.ListHostDeviceMapping(ctx, hostID)
- if err != nil {
- return nil, ctxerr.Wrap(ctx, err, "get host device mapping")
- }
-
- if len(deviceMapping) > 0 {
- endUser := fleet.HostEndUser{}
- for _, email := range deviceMapping {
- switch {
- case (email.Source == fleet.DeviceMappingMDMIdpAccounts || email.Source == fleet.DeviceMappingIDP) && len(endUsers) == 0:
- // If SCIM data is missing, we still populate IdpUserName if present.
- // For DeviceMappingIDP source, this is the user-provided IDP username.
- // Note: Username and email is the same thing here until we split them with https://github.com/fleetdm/fleet/issues/27952
- endUser.IdpUserName = email.Email
- case email.Source != fleet.DeviceMappingMDMIdpAccounts && email.Source != fleet.DeviceMappingIDP:
- // Only add to OtherEmails if it's not an IDP source
- endUser.OtherEmails = append(endUser.OtherEmails, *email)
- }
- }
- if len(endUsers) > 0 {
- endUsers[0].OtherEmails = endUser.OtherEmails
- } else {
- endUsers = append(endUsers, endUser)
- }
- }
-
- return endUsers, nil
-}
-
////////////////////////////////////////////////////////////////////////////////
// Get Host Query Report
////////////////////////////////////////////////////////////////////////////////
diff --git a/server/service/mdm.go b/server/service/mdm.go
index ee00d096c3..2c932904b7 100644
--- a/server/service/mdm.go
+++ b/server/service/mdm.go
@@ -1756,7 +1756,7 @@ func (svc *Service) NewMDMWindowsConfigProfile(ctx context.Context, teamID uint,
// Collect Fleet variables used in the profile
var usesFleetVars []fleet.FleetVarName
- for varName := range foundVars {
+ for _, varName := range foundVars {
usesFleetVars = append(usesFleetVars, fleet.FleetVarName(varName))
}
@@ -1800,7 +1800,7 @@ var fleetVarsSupportedInWindowsProfiles = []fleet.FleetVarName{
fleet.FleetVarHostUUID,
}
-func validateWindowsProfileFleetVariables(contents string, lic *fleet.LicenseInfo) (map[string]struct{}, error) {
+func validateWindowsProfileFleetVariables(contents string, lic *fleet.LicenseInfo) ([]string, error) {
foundVars := variables.Find(contents)
if len(foundVars) == 0 {
return nil, nil
@@ -1812,7 +1812,7 @@ func validateWindowsProfileFleetVariables(contents string, lic *fleet.LicenseInf
}
// Check if all found variables are supported
- for varName := range foundVars {
+ for _, varName := range foundVars {
if !slices.Contains(fleetVarsSupportedInWindowsProfiles, fleet.FleetVarName(varName)) {
return nil, fleet.NewInvalidArgumentError("profile", fmt.Sprintf("Fleet variable $FLEET_VAR_%s is not supported in Windows profiles.", varName))
}
@@ -2081,7 +2081,7 @@ func (svc *Service) BatchSetMDMProfiles(
profilesVariablesByIdentifier := make([]fleet.MDMProfileIdentifierFleetVariables, 0, len(profilesVariablesByIdentifierMap))
for identifier, variables := range profilesVariablesByIdentifierMap {
varNames := make([]fleet.FleetVarName, 0, len(variables))
- for varName := range variables {
+ for _, varName := range variables {
varNames = append(varNames, fleet.FleetVarName(varName))
}
profilesVariablesByIdentifier = append(profilesVariablesByIdentifier, fleet.MDMProfileIdentifierFleetVariables{
@@ -2207,7 +2207,7 @@ func (svc *Service) BatchSetMDMProfiles(
func validateFleetVariables(ctx context.Context, ds fleet.Datastore, appConfig *fleet.AppConfig, lic *fleet.LicenseInfo, appleProfiles map[int]*fleet.MDMAppleConfigProfile,
windowsProfiles map[int]*fleet.MDMWindowsConfigProfile, appleDecls map[int]*fleet.MDMAppleDeclaration,
-) (map[string]map[string]struct{}, error) {
+) (map[string][]string, error) {
var err error
groupedCAs, err := ds.GetGroupedCertificateAuthorities(ctx, true)
@@ -2215,7 +2215,7 @@ func validateFleetVariables(ctx context.Context, ds fleet.Datastore, appConfig *
return nil, ctxerr.Wrap(ctx, err, "getting grouped certificate authorities")
}
- profileVarsByProfIdentifier := make(map[string]map[string]struct{})
+ profileVarsByProfIdentifier := make(map[string][]string)
for _, p := range appleProfiles {
profileVars, err := validateConfigProfileFleetVariables(string(p.Mobileconfig), lic, groupedCAs)
if err != nil {
diff --git a/server/service/microsoft_mdm.go b/server/service/microsoft_mdm.go
index 1420ee9504..787eed0ff4 100644
--- a/server/service/microsoft_mdm.go
+++ b/server/service/microsoft_mdm.go
@@ -2315,6 +2315,10 @@ func ReconcileWindowsProfiles(ctx context.Context, ds fleet.Datastore, logger ki
return ctxerr.Wrap(ctx, err, "getting grouped certificate authorities")
}
+ params := microsoft_mdm.PreprocessingParameters{
+ HostIDForUUIDCache: make(map[string]uint),
+ }
+
for profUUID, target := range installTargets {
p, ok := profileContents[profUUID]
if !ok {
@@ -2347,7 +2351,7 @@ func ReconcileWindowsProfiles(ctx context.Context, ds fleet.Datastore, logger ki
hostCmdUUID := uuid.New().String()
// Preprocess the profile content for this specific host
- processedContent, err := microsoft_mdm.PreprocessWindowsProfileContentsForDeployment(ctx, logger, ds, appConfig, hostUUID, hostCmdUUID, profUUID, groupedCAs, string(p.SyncML))
+ processedContent, err := microsoft_mdm.PreprocessWindowsProfileContentsForDeployment(ctx, logger, ds, appConfig, hostUUID, hostCmdUUID, profUUID, groupedCAs, string(p.SyncML), params)
var profileProcessingError *microsoft_mdm.MicrosoftProfileProcessingError
if err != nil && !errors.As(err, &profileProcessingError) {
return ctxerr.Wrapf(ctx, err, "preprocessing profile contents for host %s and profile %s", hostUUID, profUUID)
diff --git a/server/variables/variables.go b/server/variables/variables.go
index 3757330cc0..c4e1a3a24a 100644
--- a/server/variables/variables.go
+++ b/server/variables/variables.go
@@ -26,12 +26,12 @@ var ProfileDataVariableRegex = regexp.MustCompile(`(\$FLEET_VAR_DIGICERT_DATA_(?
//
// For example, if the content contains "$FLEET_VAR_HOST_UUID" and "${FLEET_VAR_HOST_EMAIL}",
// this function will return map[string]struct{}{"HOST_UUID": {}, "HOST_EMAIL": {}}.
-func Find(contents string) map[string]struct{} {
+func Find(contents string) []string {
resultSlice := FindKeepDuplicates(contents)
if len(resultSlice) == 0 {
return nil
}
- return dedupe(resultSlice)
+ return Dedupe(resultSlice)
}
// FindKeepDuplicates finds all Fleet variables in the given content and returns them
@@ -70,11 +70,19 @@ func FindKeepDuplicates(contents string) []string {
return sortedResults
}
-// dedupe removes duplicates from the slice and returns a map for O(1) lookups.
-func dedupe(varsWithDupes []string) map[string]struct{} {
- result := make(map[string]struct{}, len(varsWithDupes))
+// Dedupe removes duplicates from the slice and returns a slice to keep order of variables
+func Dedupe(varsWithDupes []string) []string {
+ if len(varsWithDupes) == 0 {
+ return []string{}
+ }
+
+ seenMap := make(map[string]bool, len(varsWithDupes))
+ var result []string
for _, v := range varsWithDupes {
- result[v] = struct{}{}
+ if !seenMap[v] {
+ result = append(result, v)
+ seenMap[v] = true
+ }
}
return result
}
diff --git a/server/variables/variables_test.go b/server/variables/variables_test.go
index f83e621d81..45aa162107 100644
--- a/server/variables/variables_test.go
+++ b/server/variables/variables_test.go
@@ -10,7 +10,7 @@ func TestFind(t *testing.T) {
tests := []struct {
name string
content string
- expected map[string]struct{}
+ expected []string
}{
{
name: "no variables",
@@ -20,45 +20,45 @@ func TestFind(t *testing.T) {
{
name: "single variable without braces",
content: "Device ID: $FLEET_VAR_HOST_UUID",
- expected: map[string]struct{}{
- "HOST_UUID": {},
+ expected: []string{
+ "HOST_UUID",
},
},
{
name: "single variable with braces",
content: "Device ID: ${FLEET_VAR_HOST_UUID}",
- expected: map[string]struct{}{
- "HOST_UUID": {},
+ expected: []string{
+ "HOST_UUID",
},
},
{
name: "multiple different variables",
content: "Host: $FLEET_VAR_HOST_UUID, Email: ${FLEET_VAR_HOST_EMAIL}, Serial: $FLEET_VAR_HOST_SERIAL",
- expected: map[string]struct{}{
- "HOST_UUID": {},
- "HOST_EMAIL": {},
- "HOST_SERIAL": {},
+ expected: []string{
+ "HOST_SERIAL",
+ "HOST_EMAIL",
+ "HOST_UUID",
},
},
{
name: "duplicate variables",
content: "ID1: $FLEET_VAR_HOST_UUID, ID2: ${FLEET_VAR_HOST_UUID}, ID3: $FLEET_VAR_HOST_UUID",
- expected: map[string]struct{}{
- "HOST_UUID": {},
+ expected: []string{
+ "HOST_UUID",
},
},
{
name: "variables in XML content",
content: `Device: $FLEET_VAR_HOST_UUID`,
- expected: map[string]struct{}{
- "HOST_UUID": {},
+ expected: []string{
+ "HOST_UUID",
},
},
{
name: "mixed case sensitivity",
content: "Valid: $FLEET_VAR_HOST_UUID, Invalid: $fleet_var_host_uuid",
- expected: map[string]struct{}{
- "HOST_UUID": {},
+ expected: []string{
+ "HOST_UUID",
},
},
}