summaryrefslogtreecommitdiff
path: root/internal/authentication/ldap_user_provider_lifecycle.go
diff options
context:
space:
mode:
authorJames Elliott <james-d-elliott@users.noreply.github.com>2025-02-23 19:05:57 +1100
committerGitHub <noreply@github.com>2025-02-23 08:05:57 +0000
commit0af038e0ced689db90da480876a0bb26d78c6fb9 (patch)
tree5d97fe07636fcc5f7c6d87d6535bc5e1f0a9f2eb /internal/authentication/ldap_user_provider_lifecycle.go
parent197b45521f5e3799d0b9ef1ec0000d4f83abdee9 (diff)
feat(authentication): ldap connection pooling (#7217)
This implements optional LDAP connection pooling to optimize the speed of LDAP transactions. Signed-off-by: James Elliott <james-d-elliott@users.noreply.github.com>
Diffstat (limited to 'internal/authentication/ldap_user_provider_lifecycle.go')
-rw-r--r--internal/authentication/ldap_user_provider_lifecycle.go232
1 files changed, 232 insertions, 0 deletions
diff --git a/internal/authentication/ldap_user_provider_lifecycle.go b/internal/authentication/ldap_user_provider_lifecycle.go
new file mode 100644
index 000000000..73dceaa6a
--- /dev/null
+++ b/internal/authentication/ldap_user_provider_lifecycle.go
@@ -0,0 +1,232 @@
+package authentication
+
+import (
+ "fmt"
+ "strings"
+
+ "github.com/go-ldap/ldap/v3"
+
+ "github.com/authelia/authelia/v4/internal/configuration/schema"
+ "github.com/authelia/authelia/v4/internal/utils"
+)
+
+func (p *LDAPUserProvider) Shutdown() (err error) {
+ return p.factory.Close()
+}
+
+// StartupCheck implements the startup check provider interface.
+func (p *LDAPUserProvider) StartupCheck() (err error) {
+ if err = p.factory.Initialize(); err != nil {
+ return err
+ }
+
+ var client ldap.Client
+
+ if client, err = p.factory.GetClient(); err != nil {
+ return err
+ }
+
+ defer func() {
+ if err := p.factory.ReleaseClient(client); err != nil {
+ p.log.WithError(err).Warn("Error occurred releasing the LDAP client")
+ }
+ }()
+
+ if p.features, err = p.getServerSupportedFeatures(client); err != nil {
+ return err
+ }
+
+ if !p.features.Extensions.PwdModifyExOp && !p.disableResetPassword &&
+ p.config.Implementation != schema.LDAPImplementationActiveDirectory {
+ p.log.Warn("Your LDAP server implementation may not support a method for password hashing " +
+ "known to Authelia, it's strongly recommended you ensure your directory server hashes the password " +
+ "attribute when users reset their password via Authelia.")
+ }
+
+ if p.features.Extensions.TLS && !p.config.StartTLS && !p.config.Address.IsExplicitlySecure() {
+ p.log.Error("Your LDAP Server supports TLS but you don't appear to be utilizing it. We strongly " +
+ "recommend using the scheme 'ldaps://' or enabling the StartTLS option to secure connections with your " +
+ "LDAP Server.")
+ }
+
+ return nil
+}
+
+func (p *LDAPUserProvider) getServerSupportedFeatures(client ldap.Client) (features LDAPSupportedFeatures, err error) {
+ var (
+ request *ldap.SearchRequest
+ result *ldap.SearchResult
+ )
+
+ request = ldap.NewSearchRequest("", ldap.ScopeBaseObject, ldap.NeverDerefAliases,
+ 1, 0, false, ldapBaseObjectFilter, []string{ldapSupportedExtensionAttribute, ldapSupportedControlAttribute}, nil)
+
+ if result, err = client.Search(request); err != nil {
+ if p.config.PermitFeatureDetectionFailure {
+ p.log.WithError(err).Warnf("Error occurred during RootDSE search. This may result in reduced functionality.")
+
+ return features, nil
+ }
+
+ return features, fmt.Errorf("error occurred during RootDSE search: %w", err)
+ }
+
+ if len(result.Entries) != 1 {
+ p.log.Errorf("The LDAP Server did not respond appropriately to a RootDSE search. This may result in reduced functionality.")
+
+ return features, nil
+ }
+
+ var controlTypeOIDs, extensionOIDs []string
+
+ controlTypeOIDs, extensionOIDs, features = ldapGetFeatureSupportFromEntry(result.Entries[0])
+
+ controlTypes, extensions := none, none
+
+ if len(controlTypeOIDs) != 0 {
+ controlTypes = strings.Join(controlTypeOIDs, ", ")
+ }
+
+ if len(extensionOIDs) != 0 {
+ extensions = strings.Join(extensionOIDs, ", ")
+ }
+
+ p.log.Debugf("LDAP Supported OIDs. Control Types: %s. Extensions: %s", controlTypes, extensions)
+
+ return features, nil
+}
+
+//nolint:gocyclo
+func (p *LDAPUserProvider) parseDynamicUsersConfiguration() {
+ p.config.UsersFilter = strings.ReplaceAll(p.config.UsersFilter, ldapPlaceholderDistinguishedNameAttribute, p.config.Attributes.DistinguishedName)
+ p.config.UsersFilter = strings.ReplaceAll(p.config.UsersFilter, ldapPlaceholderUsernameAttribute, p.config.Attributes.Username)
+ p.config.UsersFilter = strings.ReplaceAll(p.config.UsersFilter, ldapPlaceholderDisplayNameAttribute, p.config.Attributes.DisplayName)
+ p.config.UsersFilter = strings.ReplaceAll(p.config.UsersFilter, ldapPlaceholderMailAttribute, p.config.Attributes.Mail)
+ p.config.UsersFilter = strings.ReplaceAll(p.config.UsersFilter, ldapPlaceholderMemberOfAttribute, p.config.Attributes.MemberOf)
+
+ p.log.Tracef("Dynamically generated users filter is %s", p.config.UsersFilter)
+
+ if len(p.config.Attributes.Username) != 0 && !utils.IsStringInSlice(p.config.Attributes.Username, p.usersAttributes) {
+ p.usersAttributes = append(p.usersAttributes, p.config.Attributes.Username)
+ p.usersAttributesExtended = append(p.usersAttributesExtended, p.config.Attributes.Username)
+ }
+
+ if len(p.config.Attributes.Mail) != 0 && !utils.IsStringInSlice(p.config.Attributes.Mail, p.usersAttributes) {
+ p.usersAttributes = append(p.usersAttributes, p.config.Attributes.Mail)
+ p.usersAttributesExtended = append(p.usersAttributesExtended, p.config.Attributes.Mail)
+ }
+
+ if len(p.config.Attributes.DisplayName) != 0 && !utils.IsStringInSlice(p.config.Attributes.DisplayName, p.usersAttributes) {
+ p.usersAttributes = append(p.usersAttributes, p.config.Attributes.DisplayName)
+ p.usersAttributesExtended = append(p.usersAttributesExtended, p.config.Attributes.DisplayName)
+ }
+
+ if len(p.config.Attributes.MemberOf) != 0 {
+ if !utils.IsStringInSlice(p.config.Attributes.MemberOf, p.usersAttributes) {
+ p.usersAttributes = append(p.usersAttributes, p.config.Attributes.MemberOf)
+ p.usersAttributesExtended = append(p.usersAttributesExtended, p.config.Attributes.MemberOf)
+ }
+ }
+
+ attributesExtended := []string{
+ p.config.Attributes.GivenName,
+ p.config.Attributes.MiddleName,
+ p.config.Attributes.FamilyName,
+ p.config.Attributes.Nickname,
+ p.config.Attributes.Gender,
+ p.config.Attributes.Birthdate,
+ p.config.Attributes.Website,
+ p.config.Attributes.Profile,
+ p.config.Attributes.Picture,
+ p.config.Attributes.ZoneInfo,
+ p.config.Attributes.Locale,
+ p.config.Attributes.PhoneNumber,
+ p.config.Attributes.PhoneExtension,
+ p.config.Attributes.StreetAddress,
+ p.config.Attributes.Locality,
+ p.config.Attributes.Region,
+ p.config.Attributes.PostalCode,
+ p.config.Attributes.Country,
+ }
+
+ for _, attribute := range attributesExtended {
+ if len(attribute) != 0 && !utils.IsStringInSlice(attribute, p.usersAttributesExtended) {
+ p.usersAttributesExtended = append(p.usersAttributesExtended, attribute)
+ }
+ }
+
+ for attribute := range p.config.Attributes.Extra {
+ if !utils.IsStringInSlice(attribute, p.usersAttributesExtended) {
+ p.usersAttributesExtended = append(p.usersAttributesExtended, attribute)
+ }
+ }
+
+ if p.config.AdditionalUsersDN != "" {
+ p.usersBaseDN = p.config.AdditionalUsersDN + "," + p.config.BaseDN
+ } else {
+ p.usersBaseDN = p.config.BaseDN
+ }
+
+ p.log.Tracef("Dynamically generated users BaseDN is %s", p.usersBaseDN)
+
+ if strings.Contains(p.config.UsersFilter, ldapPlaceholderInput) {
+ p.usersFilterReplacementInput = true
+ }
+
+ if strings.Contains(p.config.UsersFilter, ldapPlaceholderDateTimeGeneralized) {
+ p.usersFilterReplacementDateTimeGeneralized = true
+ }
+
+ if strings.Contains(p.config.UsersFilter, ldapPlaceholderDateTimeUnixEpoch) {
+ p.usersFilterReplacementDateTimeUnixEpoch = true
+ }
+
+ if strings.Contains(p.config.UsersFilter, ldapPlaceholderDateTimeMicrosoftNTTimeEpoch) {
+ p.usersFilterReplacementDateTimeMicrosoftNTTimeEpoch = true
+ }
+
+ p.log.Tracef("Detected user filter replacements that need to be resolved per lookup are: %s=%v",
+ ldapPlaceholderInput, p.usersFilterReplacementInput)
+}
+
+func (p *LDAPUserProvider) parseDynamicGroupsConfiguration() {
+ p.config.GroupsFilter = strings.ReplaceAll(p.config.GroupsFilter, ldapPlaceholderDistinguishedNameAttribute, p.config.Attributes.DistinguishedName)
+ p.config.GroupsFilter = strings.ReplaceAll(p.config.GroupsFilter, ldapPlaceholderUsernameAttribute, p.config.Attributes.Username)
+ p.config.GroupsFilter = strings.ReplaceAll(p.config.GroupsFilter, ldapPlaceholderDisplayNameAttribute, p.config.Attributes.DisplayName)
+ p.config.GroupsFilter = strings.ReplaceAll(p.config.GroupsFilter, ldapPlaceholderMailAttribute, p.config.Attributes.Mail)
+ p.config.GroupsFilter = strings.ReplaceAll(p.config.GroupsFilter, ldapPlaceholderMemberOfAttribute, p.config.Attributes.MemberOf)
+
+ if len(p.config.Attributes.GroupName) != 0 && !utils.IsStringInSlice(p.config.Attributes.GroupName, p.groupsAttributes) {
+ p.groupsAttributes = append(p.groupsAttributes, p.config.Attributes.GroupName)
+ }
+
+ if p.config.AdditionalGroupsDN != "" {
+ p.groupsBaseDN = p.config.AdditionalGroupsDN + "," + p.config.BaseDN
+ } else {
+ p.groupsBaseDN = p.config.BaseDN
+ }
+
+ p.log.Tracef("Dynamically generated groups BaseDN is %s", p.groupsBaseDN)
+
+ if strings.Contains(p.config.GroupsFilter, ldapPlaceholderInput) {
+ p.groupsFilterReplacementInput = true
+ }
+
+ if strings.Contains(p.config.GroupsFilter, ldapPlaceholderUsername) {
+ p.groupsFilterReplacementUsername = true
+ }
+
+ if strings.Contains(p.config.GroupsFilter, ldapPlaceholderDistinguishedName) {
+ p.groupsFilterReplacementDN = true
+ }
+
+ if strings.Contains(p.config.GroupsFilter, ldapPlaceholderMemberOfDistinguishedName) {
+ p.groupsFilterReplacementsMemberOfDN = true
+ }
+
+ if strings.Contains(p.config.GroupsFilter, ldapPlaceholderMemberOfRelativeDistinguishedName) {
+ p.groupsFilterReplacementsMemberOfRDN = true
+ }
+
+ p.log.Tracef("Detected group filter replacements that need to be resolved per lookup are: input=%v, username=%v, dn=%v", p.groupsFilterReplacementInput, p.groupsFilterReplacementUsername, p.groupsFilterReplacementDN)
+}