package authentication import ( "crypto/x509" "fmt" "net/url" "strconv" "strings" "github.com/go-ldap/ldap/v3" "github.com/sirupsen/logrus" "golang.org/x/text/language" "github.com/authelia/authelia/v4/internal/clock" "github.com/authelia/authelia/v4/internal/configuration/schema" "github.com/authelia/authelia/v4/internal/logging" "github.com/authelia/authelia/v4/internal/utils" ) // LDAPUserProvider is a UserProvider that connects to LDAP servers like ActiveDirectory, OpenLDAP, OpenDJ, FreeIPA, etc. type LDAPUserProvider struct { config *schema.AuthenticationBackendLDAP log *logrus.Logger factory LDAPClientFactory clock clock.Provider disableResetPassword bool // Automatically detected LDAP features. features LDAPSupportedFeatures // Dynamically generated users values. usersBaseDN string usersAttributes []string usersAttributesExtended []string usersFilterReplacementInput bool usersFilterReplacementDateTimeGeneralized bool usersFilterReplacementDateTimeUnixEpoch bool usersFilterReplacementDateTimeMicrosoftNTTimeEpoch bool // Dynamically generated groups values. groupsBaseDN string groupsAttributes []string groupsFilterReplacementInput bool groupsFilterReplacementUsername bool groupsFilterReplacementDN bool groupsFilterReplacementsMemberOfDN bool groupsFilterReplacementsMemberOfRDN bool } // NewLDAPUserProvider creates a new instance of LDAPUserProvider with the StandardLDAPClientFactory. func NewLDAPUserProvider(config schema.AuthenticationBackend, certs *x509.CertPool) (provider *LDAPUserProvider) { if config.LDAP.TLS == nil { config.LDAP.TLS = schema.DefaultLDAPAuthenticationBackendConfigurationImplementationCustom.TLS } var factory LDAPClientFactory if config.LDAP.Pooling.Enable { factory = NewPooledLDAPClientFactory(config.LDAP, certs, nil) } else { factory = NewStandardLDAPClientFactory(config.LDAP, certs, nil) } return NewLDAPUserProviderWithFactory(config.LDAP, config.PasswordReset.Disable, factory) } // NewLDAPUserProviderWithFactory creates a new instance of LDAPUserProvider with the specified LDAPClientFactory. func NewLDAPUserProviderWithFactory(config *schema.AuthenticationBackendLDAP, disableResetPassword bool, factory LDAPClientFactory) (provider *LDAPUserProvider) { provider = &LDAPUserProvider{ config: config, log: logging.Logger(), factory: factory, disableResetPassword: disableResetPassword, clock: clock.New(), } provider.parseDynamicUsersConfiguration() provider.parseDynamicGroupsConfiguration() return provider } // CheckUserPassword checks if provided password matches for the given user. func (p *LDAPUserProvider) CheckUserPassword(username string, password string) (valid bool, err error) { var ( client, uclient ldap.Client profile *ldapUserProfile ) if client, err = p.factory.GetClient(); err != nil { return false, err } defer func() { if err := p.factory.ReleaseClient(client); err != nil { p.log.WithError(err).Warn("Error occurred releasing the LDAP client") } }() if profile, err = p.getUserProfile(client, username); err != nil { return false, err } if uclient, err = p.factory.GetClient(WithUsername(profile.DN), WithPassword(password)); err != nil { return false, fmt.Errorf("authentication failed. Cause: %w", err) } defer func() { if err := p.factory.ReleaseClient(uclient); err != nil { p.log.WithError(err).Warn("Error occurred releasing the LDAP client") } }() return true, nil } // GetDetails retrieve the groups a user belongs to. func (p *LDAPUserProvider) GetDetails(username string) (details *UserDetails, err error) { var ( client ldap.Client profile *ldapUserProfile ) if client, err = p.factory.GetClient(); err != nil { return nil, err } defer func() { if err := p.factory.ReleaseClient(client); err != nil { p.log.WithError(err).Warn("Error occurred releasing the LDAP client") } }() if profile, err = p.getUserProfile(client, username); err != nil { return nil, err } var ( groups []string ) if groups, err = p.getUserGroups(client, username, profile); err != nil { return nil, err } return &UserDetails{ Username: profile.Username, DisplayName: profile.DisplayName, Emails: profile.Emails, Groups: groups, }, nil } // GetDetailsExtended retrieves the UserDetailsExtended values. func (p *LDAPUserProvider) GetDetailsExtended(username string) (details *UserDetailsExtended, err error) { var ( client ldap.Client profile *ldapUserProfileExtended ) if client, err = p.factory.GetClient(); err != nil { return nil, err } defer func() { if err := p.factory.ReleaseClient(client); err != nil { p.log.WithError(err).Warn("Error occurred releasing the LDAP client") } }() if profile, err = p.getUserProfileExtended(client, username); err != nil { return nil, err } var ( groups []string ) if groups, err = p.getUserGroups(client, username, profile.ldapUserProfile); err != nil { return nil, err } details = &UserDetailsExtended{ GivenName: profile.GivenName, FamilyName: profile.FamilyName, MiddleName: profile.MiddleName, Nickname: profile.Nickname, Gender: profile.Gender, Birthdate: profile.Birthdate, ZoneInfo: profile.ZoneInfo, PhoneNumber: profile.PhoneNumber, PhoneExtension: profile.PhoneExtension, Address: profile.Address, UserDetails: &UserDetails{ Username: profile.Username, DisplayName: profile.DisplayName, Emails: profile.Emails, Groups: groups, }, Extra: profile.Extra, } var ( uri *url.URL locale language.Tag ) if profile.Profile != "" { if uri, err = parseAttributeURI(username, "profile", p.config.Attributes.Profile, profile.Profile); err != nil { return nil, err } else { details.Profile = uri } } if profile.Picture != "" { if uri, err = parseAttributeURI(username, "picture", p.config.Attributes.Picture, profile.Picture); err != nil { return nil, err } else { details.Picture = uri } } if profile.Website != "" { if uri, err = parseAttributeURI(username, "website", p.config.Attributes.Website, profile.Website); err != nil { return nil, err } else { details.Website = uri } } if profile.Locale != "" { if locale, err = language.Parse(profile.Locale); err != nil { return nil, fmt.Errorf("error occurred parsing user details for '%s': failed to parse the locale attribute '%s' with value '%s': %w", username, p.config.Attributes.Locale, profile.Locale, err) } else { details.Locale = &locale } } return details, nil } // UpdatePassword update the password of the given user. func (p *LDAPUserProvider) UpdatePassword(username, password string) (err error) { var ( client ldap.Client profile *ldapUserProfile ) if client, err = p.factory.GetClient(); err != nil { return fmt.Errorf("unable to update password. Cause: %w", err) } defer func() { if err := p.factory.ReleaseClient(client); err != nil { p.log.WithError(err).Warn("Error occurred releasing the LDAP client") } }() if profile, err = p.getUserProfile(client, username); err != nil { return fmt.Errorf("unable to update password. Cause: %w", err) } var controls []ldap.Control switch { case p.features.ControlTypes.MsftPwdPolHints: controls = append(controls, &controlMsftServerPolicyHints{ldapOIDControlMsftServerPolicyHints}) case p.features.ControlTypes.MsftPwdPolHintsDeprecated: controls = append(controls, &controlMsftServerPolicyHints{ldapOIDControlMsftServerPolicyHintsDeprecated}) } switch { case p.features.Extensions.PwdModifyExOp: pwdModifyRequest := ldap.NewPasswordModifyRequest( profile.DN, "", password, ) err = p.pwdModify(client, pwdModifyRequest) case p.config.Implementation == schema.LDAPImplementationActiveDirectory: modifyRequest := ldap.NewModifyRequest(profile.DN, controls) // The password needs to be enclosed in quotes // https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-adts/6e803168-f140-4d23-b2d3-c3a8ab5917d2 pwdEncoded, _ := encodingUTF16LittleEndian.NewEncoder().String(fmt.Sprintf("\"%s\"", password)) modifyRequest.Replace(ldapAttributeUnicodePwd, []string{pwdEncoded}) err = p.modify(client, modifyRequest) default: modifyRequest := ldap.NewModifyRequest(profile.DN, controls) modifyRequest.Replace(ldapAttributeUserPassword, []string{password}) err = p.modify(client, modifyRequest) } if err != nil { return fmt.Errorf("unable to update password. Cause: %w", err) } return nil } // ChangePassword is used to change a user's password but requires their old password to be successfully verified. // //nolint:gocyclo func (p *LDAPUserProvider) ChangePassword(username, oldPassword string, newPassword string) (err error) { var ( client ldap.Client profile *ldapUserProfile ) if client, err = p.factory.GetClient(); err != nil { return fmt.Errorf("unable to update password for user '%s'. Cause: %w", username, err) } defer func() { if err := p.factory.ReleaseClient(client); err != nil { p.log.WithError(err).Warn("Error occurred releasing the LDAP client") } }() if profile, err = p.getUserProfile(client, username); err != nil { return fmt.Errorf("unable to update password for user '%s'. Cause: %w", username, err) } var controls []ldap.Control switch { case p.features.ControlTypes.MsftPwdPolHints: controls = append(controls, &controlMsftServerPolicyHints{ldapOIDControlMsftServerPolicyHints}) case p.features.ControlTypes.MsftPwdPolHintsDeprecated: controls = append(controls, &controlMsftServerPolicyHints{ldapOIDControlMsftServerPolicyHintsDeprecated}) } userPasswordOk, err := p.CheckUserPassword(username, oldPassword) if err != nil { errorCode := ldapGetErrorCode(err) if errorCode == ldap.LDAPResultInvalidCredentials { return ErrIncorrectPassword } else { return err } } if !userPasswordOk { return ErrIncorrectPassword } if oldPassword == newPassword { return ErrPasswordWeak } switch { case p.features.Extensions.PwdModifyExOp: pwdModifyRequest := ldap.NewPasswordModifyRequest( profile.DN, oldPassword, newPassword, ) err = p.pwdModify(client, pwdModifyRequest) case p.config.Implementation == schema.LDAPImplementationActiveDirectory: modifyRequest := ldap.NewModifyRequest(profile.DN, controls) // The password needs to be enclosed in quotes // https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-adts/6e803168-f140-4d23-b2d3-c3a8ab5917d2 pwdEncoded, err := encodingUTF16LittleEndian.NewEncoder().String(fmt.Sprintf("\"%s\"", newPassword)) if err != nil { return fmt.Errorf("failed to encode new password for user '%s'. Cause: %w", username, err) } modifyRequest.Replace(ldapAttributeUnicodePwd, []string{pwdEncoded}) //nolint err = p.modify(client, modifyRequest) default: modifyRequest := ldap.NewModifyRequest(profile.DN, controls) modifyRequest.Replace(ldapAttributeUserPassword, []string{newPassword}) err = p.modify(client, modifyRequest) } //TODO: Better inform users regarding password reuse/password history. if err != nil { if errorCode := ldapGetErrorCode(err); errorCode != -1 { switch errorCode { case ldap.LDAPResultInvalidCredentials, ldap.LDAPResultInappropriateAuthentication: return ErrIncorrectPassword case ldap.LDAPResultConstraintViolation, ldap.LDAPResultObjectClassViolation, ldap.ErrorEmptyPassword, ldap.LDAPResultUnwillingToPerform: return ErrPasswordWeak case ldap.LDAPResultInsufficientAccessRights: return ErrOperationFailed default: return ErrOperationFailed } } return ErrOperationFailed } return nil } func (p *LDAPUserProvider) search(client ldap.Client, request *ldap.SearchRequest) (result *ldap.SearchResult, err error) { if result, err = client.Search(request); err != nil { if referral, ok := p.getReferral(err); ok { if result == nil { result = &ldap.SearchResult{ Referrals: []string{referral}, } } else { result.Referrals = append(result.Referrals, referral) } } else { return nil, err } } if !p.config.PermitReferrals || len(result.Referrals) == 0 { return result, nil } if err = p.searchReferrals(request, result); err != nil { return nil, err } return result, nil } func (p *LDAPUserProvider) searchReferral(referral string, request *ldap.SearchRequest, searchResult *ldap.SearchResult) (err error) { var ( client ldap.Client result *ldap.SearchResult ) if client, err = p.factory.GetClient(WithAddress(referral)); err != nil { return fmt.Errorf("error occurred connecting to referred LDAP server '%s': %w", referral, err) } defer func() { if err := p.factory.ReleaseClient(client); err != nil { p.log.WithError(err).Warn("Error occurred releasing the LDAP client") } }() if result, err = client.Search(request); err != nil { return fmt.Errorf("error occurred performing search on referred LDAP server '%s': %w", referral, err) } for i := 0; i < len(result.Entries); i++ { if !ldapEntriesContainsEntry(result.Entries[i], searchResult.Entries) { searchResult.Entries = append(searchResult.Entries, result.Entries[i]) } } return nil } func (p *LDAPUserProvider) searchReferrals(request *ldap.SearchRequest, result *ldap.SearchResult) (err error) { for i := 0; i < len(result.Referrals); i++ { if err = p.searchReferral(result.Referrals[i], request, result); err != nil { return err } } return nil } func (p *LDAPUserProvider) getUserProfile(client ldap.Client, username string) (profile *ldapUserProfile, err error) { // Search for the given username. request := ldap.NewSearchRequest( p.usersBaseDN, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 1, 0, false, p.resolveUsersFilter(username), p.usersAttributes, nil, ) p.log. WithField("base_dn", request.BaseDN). WithField("filter", request.Filter). WithField("attr", request.Attributes). WithField("scope", request.Scope). WithField("deref", request.DerefAliases). Trace("Performing user search") var result *ldap.SearchResult if result, err = p.search(client, request); err != nil { return nil, fmt.Errorf("cannot find user DN of user '%s'. Cause: %w", username, err) } if len(result.Entries) == 0 { return nil, ErrUserNotFound } if len(result.Entries) > 1 { return nil, fmt.Errorf("there were %d users found when searching for '%s' but there should only be 1", len(result.Entries), username) } return p.getUserProfileResultToProfile(username, result.Entries[0]) } func (p *LDAPUserProvider) getUserProfileResultToProfile(username string, entry *ldap.Entry) (profile *ldapUserProfile, err error) { userProfile := ldapUserProfile{ DN: entry.DN, Emails: getValuesFromEntry(entry, p.config.Attributes.Mail), DisplayName: getValueFromEntry(entry, p.config.Attributes.DisplayName), MemberOf: getValuesFromEntry(entry, p.config.Attributes.MemberOf), } attrUsername := getValuesFromEntry(entry, p.config.Attributes.Username) switch n := len(attrUsername); n { case 1: userProfile.Username = attrUsername[0] if p.config.Attributes.Username == p.config.Attributes.DisplayName && userProfile.DisplayName == "" { userProfile.DisplayName = attrUsername[0] } if p.config.Attributes.Username == p.config.Attributes.Mail && len(userProfile.Emails) == 0 { userProfile.Emails = []string{attrUsername[0]} } case 0: return nil, fmt.Errorf("user '%s' must have value for attribute '%s'", username, p.config.Attributes.Username) default: return nil, fmt.Errorf("user '%s' has %d values for for attribute '%s' but the attribute must be a single value attribute", username, n, p.config.Attributes.Username) } if userProfile.Username == "" { return nil, fmt.Errorf("user '%s' must have value for attribute '%s'", username, p.config.Attributes.Username) } if userProfile.DN == "" { return nil, fmt.Errorf("user '%s' must have a distinguished name but the result returned an empty distinguished name", username) } return &userProfile, nil } func (p *LDAPUserProvider) getUserProfileExtended(client ldap.Client, username string) (profile *ldapUserProfileExtended, err error) { // Search for the given username. request := ldap.NewSearchRequest( p.usersBaseDN, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 1, 0, false, p.resolveUsersFilter(username), p.usersAttributesExtended, nil, ) p.log. WithField("base_dn", request.BaseDN). WithField("filter", request.Filter). WithField("attr", request.Attributes). WithField("scope", request.Scope). WithField("deref", request.DerefAliases). Trace("Performing extended user search") var result *ldap.SearchResult if result, err = p.search(client, request); err != nil { return nil, fmt.Errorf("cannot find user DN of user '%s'. Cause: %w", username, err) } if len(result.Entries) == 0 { return nil, ErrUserNotFound } if len(result.Entries) > 1 { return nil, fmt.Errorf("there were %d users found when searching for '%s' but there should only be 1", len(result.Entries), username) } return p.getUserProfileResultToProfileExtended(username, result.Entries[0]) } func (p *LDAPUserProvider) getUserProfileResultToProfileExtended(username string, entry *ldap.Entry) (profile *ldapUserProfileExtended, err error) { base, err := p.getUserProfileResultToProfile(username, entry) if err != nil { return nil, err } userProfile := ldapUserProfileExtended{ GivenName: getValueFromEntry(entry, p.config.Attributes.GivenName), FamilyName: getValueFromEntry(entry, p.config.Attributes.FamilyName), MiddleName: getValueFromEntry(entry, p.config.Attributes.MiddleName), Nickname: getValueFromEntry(entry, p.config.Attributes.Nickname), Profile: getValueFromEntry(entry, p.config.Attributes.Profile), Picture: getValueFromEntry(entry, p.config.Attributes.Picture), Website: getValueFromEntry(entry, p.config.Attributes.Website), Gender: getValueFromEntry(entry, p.config.Attributes.Gender), Birthdate: getValueFromEntry(entry, p.config.Attributes.Birthdate), ZoneInfo: getValueFromEntry(entry, p.config.Attributes.ZoneInfo), Locale: getValueFromEntry(entry, p.config.Attributes.Locale), PhoneNumber: getValueFromEntry(entry, p.config.Attributes.PhoneNumber), PhoneExtension: getValueFromEntry(entry, p.config.Attributes.PhoneExtension), Extra: map[string]any{}, ldapUserProfile: base, } street, locality, region, postcode, country := getValueFromEntry(entry, p.config.Attributes.StreetAddress), getValueFromEntry(entry, p.config.Attributes.Locality), getValueFromEntry(entry, p.config.Attributes.Region), getValueFromEntry(entry, p.config.Attributes.PostalCode), getValueFromEntry(entry, p.config.Attributes.Country) if street != "" || locality != "" || region != "" || postcode != "" || country != "" { userProfile.Address = &UserDetailsAddress{ StreetAddress: street, Locality: locality, Region: region, PostalCode: postcode, Country: country, } } var attr any for attribute, properties := range p.config.Attributes.Extra { if attr, err = getExtraValueFromEntry(entry, attribute, properties); err != nil { return nil, err } if attr == nil { continue } if len(properties.Name) == 0 { userProfile.Extra[attribute] = attr } else { userProfile.Extra[properties.Name] = attr } } return &userProfile, nil } func (p *LDAPUserProvider) getUserGroups(client ldap.Client, username string, profile *ldapUserProfile) (groups []string, err error) { request := ldap.NewSearchRequest( p.groupsBaseDN, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, p.resolveGroupsFilter(username, profile), p.groupsAttributes, nil, ) p.log. WithField("base_dn", request.BaseDN). WithField("filter", request.Filter). WithField("attributes", request.Attributes). WithField("scope", request.Scope). WithField("deref", request.DerefAliases). WithField("mode", p.config.GroupSearchMode). Trace("Performing group search") switch p.config.GroupSearchMode { case "", "filter": return p.getUserGroupsRequestFilter(client, username, profile, request) case "memberof": return p.getUserGroupsRequestMemberOf(client, username, profile, request) default: return nil, fmt.Errorf("could not perform group search with mode '%s' as it's unknown", p.config.GroupSearchMode) } } func (p *LDAPUserProvider) getUserGroupsRequestFilter(client ldap.Client, username string, _ *ldapUserProfile, request *ldap.SearchRequest) (groups []string, err error) { var result *ldap.SearchResult if result, err = p.search(client, request); err != nil { return nil, fmt.Errorf("unable to retrieve groups of user '%s'. Cause: %w", username, err) } for _, entry := range result.Entries { if group := p.getUserGroupFromEntry(entry); len(group) != 0 { groups = append(groups, group) } } return groups, nil } func (p *LDAPUserProvider) getUserGroupsRequestMemberOf(client ldap.Client, username string, profile *ldapUserProfile, request *ldap.SearchRequest) (groups []string, err error) { var result *ldap.SearchResult if result, err = p.search(client, request); err != nil { return nil, fmt.Errorf("unable to retrieve groups of user '%s'. Cause: %w", username, err) } for _, entry := range result.Entries { if len(entry.Attributes) == 0 { p.log. WithField("dn", entry.DN). WithField("attributes", request.Attributes). WithField("mode", "memberof"). Trace("Skipping Group as the server did not return any requested attributes") continue } if !utils.IsStringInSliceFold(entry.DN, profile.MemberOf) { p.log. WithField("dn", entry.DN). WithField("mode", "memberof"). Trace("Skipping Group as it doesn't match the users memberof entries") continue } if group := p.getUserGroupFromEntry(entry); len(group) != 0 { groups = append(groups, group) } } return groups, nil } func (p *LDAPUserProvider) getUserGroupFromEntry(entry *ldap.Entry) string { attributes: for _, attr := range entry.Attributes { switch strings.ToLower(attr.Name) { case strings.ToLower(p.config.Attributes.GroupName): switch len(attr.Values) { case 0: p.log. WithField("dn", entry.DN). WithField("attribute", attr.Name). Trace("Group skipped as the server returned a null attribute") case 1: switch len(attr.Values[0]) { case 0: p.log. WithField("dn", entry.DN). WithField("attribute", attr.Name). Trace("Skipping group as the configured group name attribute had no value") default: return attr.Values[0] } default: p.log. WithField("dn", entry.DN). WithField("attribute", attr.Name). Trace("Group skipped as the server returned a multi-valued attribute but it should be a single-valued attribute") } break attributes } } return "" } func (p *LDAPUserProvider) resolveUsersFilter(input string) (filter string) { filter = p.config.UsersFilter if p.usersFilterReplacementInput { // The {input} placeholder is replaced by the username input. filter = strings.ReplaceAll(filter, ldapPlaceholderInput, ldapEscape(input)) } if p.usersFilterReplacementDateTimeGeneralized { filter = strings.ReplaceAll(filter, ldapPlaceholderDateTimeGeneralized, p.clock.Now().UTC().Format(ldapGeneralizedTimeDateTimeFormat)) } if p.usersFilterReplacementDateTimeUnixEpoch { filter = strings.ReplaceAll(filter, ldapPlaceholderDateTimeUnixEpoch, strconv.Itoa(int(p.clock.Now().Unix()))) } if p.usersFilterReplacementDateTimeMicrosoftNTTimeEpoch { filter = strings.ReplaceAll(filter, ldapPlaceholderDateTimeMicrosoftNTTimeEpoch, strconv.FormatUint(utils.UnixNanoTimeToMicrosoftNTEpoch(p.clock.Now().UnixNano()), 10)) } p.log.Tracef("Detected user filter is %s", filter) return filter } func (p *LDAPUserProvider) resolveGroupsFilter(input string, profile *ldapUserProfile) (filter string) { filter = p.config.GroupsFilter if p.groupsFilterReplacementInput { // The {input} placeholder is replaced by the users username input. filter = strings.ReplaceAll(p.config.GroupsFilter, ldapPlaceholderInput, ldapEscape(input)) } if profile != nil { if p.groupsFilterReplacementUsername { filter = strings.ReplaceAll(filter, ldapPlaceholderUsername, ldap.EscapeFilter(profile.Username)) } if p.groupsFilterReplacementDN { filter = strings.ReplaceAll(filter, ldapPlaceholderDistinguishedName, ldap.EscapeFilter(profile.DN)) } } if p.groupsFilterReplacementsMemberOfDN { sep := fmt.Sprintf(")(%s=", p.config.Attributes.DistinguishedName) values := make([]string, len(profile.MemberOf)) for i, memberof := range profile.MemberOf { values[i] = ldap.EscapeFilter(memberof) } filter = strings.ReplaceAll(filter, ldapPlaceholderMemberOfDistinguishedName, fmt.Sprintf("(%s=%s)", p.config.Attributes.DistinguishedName, strings.Join(values, sep))) } if p.groupsFilterReplacementsMemberOfRDN { values := make([]string, len(profile.MemberOf)) for i, memberof := range profile.MemberOf { values[i] = ldap.EscapeFilter(strings.SplitN(memberof, ",", 2)[0]) } filter = strings.ReplaceAll(filter, ldapPlaceholderMemberOfRelativeDistinguishedName, fmt.Sprintf("(%s)", strings.Join(values, ")("))) } p.log.Tracef("Computed groups filter is %s", filter) return filter } func (p *LDAPUserProvider) modify(client ldap.Client, modifyRequest *ldap.ModifyRequest) (err error) { if err = client.Modify(modifyRequest); err != nil { var ( referral string ok bool ) if referral, ok = p.getReferral(err); !ok { return err } p.log.Debugf("Attempting Modify on referred URL %s", referral) var ( clientRef ldap.Client errRef error ) if clientRef, errRef = p.factory.GetClient(WithAddress(referral)); errRef != nil { return fmt.Errorf("error occurred connecting to referred LDAP server '%s': %+v. Original Error: %w", referral, errRef, err) } defer func() { if err := p.factory.ReleaseClient(clientRef); err != nil { p.log.WithError(err).Warn("Error occurred releasing the LDAP client") } }() if errRef = clientRef.Modify(modifyRequest); errRef != nil { return fmt.Errorf("error occurred performing modify on referred LDAP server '%s': %+v. Original Error: %w", referral, errRef, err) } return nil } return nil } func (p *LDAPUserProvider) pwdModify(client ldap.Client, pwdModifyRequest *ldap.PasswordModifyRequest) (err error) { if _, err = client.PasswordModify(pwdModifyRequest); err != nil { var ( referral string ok bool ) if referral, ok = p.getReferral(err); !ok { return err } p.log.Debugf("Attempting PwdModify ExOp (1.3.6.1.4.1.4203.1.11.1) on referred URL %s", referral) var ( clientRef ldap.Client errRef error ) if clientRef, errRef = p.factory.GetClient(WithAddress(referral)); errRef != nil { return fmt.Errorf("error occurred connecting to referred LDAP server '%s': %+v. Original Error: %w", referral, errRef, err) } defer func() { if err := p.factory.ReleaseClient(clientRef); err != nil { p.log.WithError(err).Warn("Error occurred releasing the LDAP client") } }() if _, errRef = clientRef.PasswordModify(pwdModifyRequest); errRef != nil { return fmt.Errorf("error occurred performing password modify on referred LDAP server '%s': %+v. Original Error: %w", referral, errRef, err) } return nil } return nil } func (p *LDAPUserProvider) getReferral(err error) (referral string, ok bool) { if !p.config.PermitReferrals { return "", false } return ldapGetReferral(err) } func parseAttributeURI(username, attributeName, attribute, value string) (uri *url.URL, err error) { if uri, err = url.ParseRequestURI(value); err == nil { if uri.Scheme != "http" && uri.Scheme != "https" { err = fmt.Errorf("invalid URL scheme '%s' for profile attribute", uri.Scheme) } } if err != nil { if attributeName == "" { return nil, fmt.Errorf("error occurred parsing user details for '%s': failed to parse the %s attribute with value '%s': %w", username, attribute, value, err) } return nil, fmt.Errorf("error occurred parsing user details for '%s': failed to parse the %s attribute '%s' with value '%s': %w", username, attributeName, attribute, value, err) } return uri, nil }