mirror of
https://github.com/fleetdm/fleet
synced 2026-04-21 13:37:30 +00:00
DCSW: Support all IDP variables in Windows config profiles (#34707)
<!-- Add the related story/sub-task/bug number, like Resolves #123, or remove if NA --> **Related issue:** Resolves https://fleetdm.slack.com/archives/C03C41L5YEL/p1761232938314509, but also https://github.com/fleetdm/fleet/issues/34363, as it would have been stupid to only add IDP_USERNAME (to allow for an email) when they are so close together and no extra effort to support the rest. Also does another fix to ensure fleet variables are correctly ordered by longest name to shortest, to ensure variables that are used in longer format (USERNAME, USERNAME_LOCAL_PART) that the LOCAL_PART one gets processed first. # Checklist for submitter If some of the following don't apply, delete the relevant line. ## Testing - [x] Added/updated automated tests - [x] QA'd all new/changed functionality manually
This commit is contained in:
parent
b4aa28f7ee
commit
ba15654498
13 changed files with 343 additions and 352 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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: `<Replace><Data>ID1: $FLEET_VAR_HOST_UUID, ID2: ${FLEET_VAR_HOST_UUID}</Data></Replace>`,
|
||||
expectedContents: `<Replace><Data>ID1: test-host-1234-uuid, ID2: test-host-1234-uuid</Data></Replace>`,
|
||||
},
|
||||
{
|
||||
name: "fleet variable with db access",
|
||||
hostUUID: "test-host-end-user-email",
|
||||
profileContents: `<Replace><Data>ID: $FLEET_VAR_HOST_UUID, Other: $FLEET_VAR_HOST_END_USER_EMAIL_IDP</Data></Replace>`,
|
||||
expectedContents: `<Replace><Data>ID: test-host-end-user-email, Other: test@idp.com</Data></Replace>`,
|
||||
},
|
||||
{
|
||||
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: `<Replace><Item><Target><LocURI>./Device/Test</LocURI></Target><Data>Device ID: $FLEET_VAR_HOST_UUID</Data></Item></Replace>`,
|
||||
expectedContents: `<Replace><Item><Target><LocURI>./Device/Test</LocURI></Target><Data>Device ID: test-uuid-456</Data></Item></Replace>`,
|
||||
},
|
||||
{
|
||||
name: "host end user email idp",
|
||||
hostUUID: "test-uuid-end-user-email",
|
||||
profileContents: `<Replace><Data>Email: $FLEET_VAR_HOST_END_USER_EMAIL_IDP</Data></Replace>`,
|
||||
expectedContents: `<Replace><Data>Email: test@idp.com</Data></Replace>`,
|
||||
},
|
||||
{
|
||||
name: "no host end user email idp found",
|
||||
hostUUID: "test-uuid-no-end-user",
|
||||
profileContents: `<Replace><Data>Email: $FLEET_VAR_HOST_END_USER_EMAIL_IDP</Data></Replace>`,
|
||||
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: `<Replace><Item><Target><LocURI>./Device/Test</LocURI></Target><Data>User: $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</Data></Item></Replace>`,
|
||||
expectedContents: `<Replace><Item><Target><LocURI>./Device/Test</LocURI></Target><Data>User: test@idp.com - test - Group One,Group Two - Department - First Last</Data></Item></Replace>`,
|
||||
},
|
||||
}
|
||||
|
||||
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 != "" {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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: `<Replace><Data>Device: $FLEET_VAR_HOST_UUID</Data></Replace>`,
|
||||
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",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue