diff --git a/cmd/admin_auth_ldap.go b/cmd/admin_auth_ldap.go index e3c81809f8d2..dd435cc1c220 100644 --- a/cmd/admin_auth_ldap.go +++ b/cmd/admin_auth_ldap.go @@ -46,8 +46,8 @@ var ( Usage: "Disable TLS verification.", }, &cli.StringFlag{ - Name: "host", - Usage: "The address where the LDAP server can be reached.", + Name: "host-list", + Usage: "List of addresses where the LDAP server(s) can be reached.", }, &cli.IntFlag{ Name: "port", @@ -206,8 +206,8 @@ func parseLdapConfig(c *cli.Context, config *ldap.Source) error { if c.IsSet("name") { config.Name = c.String("name") } - if c.IsSet("host") { - config.Host = c.String("host") + if c.IsSet("host-list") { + config.HostList = c.String("host-list") } if c.IsSet("port") { config.Port = c.Int("port") @@ -308,7 +308,7 @@ func (a *authService) getAuthSource(ctx context.Context, c *cli.Context, authTyp // addLdapBindDn adds a new LDAP via Bind DN authentication source. func (a *authService) addLdapBindDn(c *cli.Context) error { - if err := argsSet(c, "name", "security-protocol", "host", "port", "user-search-base", "user-filter", "email-attribute"); err != nil { + if err := argsSet(c, "name", "security-protocol", "host-list", "port", "user-search-base", "user-filter", "email-attribute"); err != nil { return err } @@ -359,7 +359,7 @@ func (a *authService) updateLdapBindDn(c *cli.Context) error { // addLdapSimpleAuth adds a new LDAP (simple auth) authentication source. func (a *authService) addLdapSimpleAuth(c *cli.Context) error { - if err := argsSet(c, "name", "security-protocol", "host", "port", "user-dn", "user-filter", "email-attribute"); err != nil { + if err := argsSet(c, "name", "security-protocol", "host-list", "port", "user-dn", "user-filter", "email-attribute"); err != nil { return err } diff --git a/cmd/admin_auth_ldap_test.go b/cmd/admin_auth_ldap_test.go index 7791f3a9cc14..0539a15e4ccf 100644 --- a/cmd/admin_auth_ldap_test.go +++ b/cmd/admin_auth_ldap_test.go @@ -34,7 +34,7 @@ func TestAddLdapBindDn(t *testing.T) { "--not-active", "--security-protocol", "ldaps", "--skip-tls-verify", - "--host", "ldap-bind-server full", + "--host-list", "ldap-bind-server full", "--port", "9876", "--user-search-base", "ou=Users,dc=full-domain-bind,dc=org", "--user-filter", "(memberOf=cn=user-group,ou=example,dc=full-domain-bind,dc=org)", @@ -59,7 +59,7 @@ func TestAddLdapBindDn(t *testing.T) { IsSyncEnabled: true, Cfg: &ldap.Source{ Name: "ldap (via Bind DN) source full", - Host: "ldap-bind-server full", + HostList: "ldap-bind-server full", Port: 9876, SecurityProtocol: ldap.SecurityProtocol(1), SkipVerify: true, @@ -87,7 +87,7 @@ func TestAddLdapBindDn(t *testing.T) { "ldap-test", "--name", "ldap (via Bind DN) source min", "--security-protocol", "unencrypted", - "--host", "ldap-bind-server min", + "--host-list", "ldap-bind-server min", "--port", "1234", "--user-search-base", "ou=Users,dc=min-domain-bind,dc=org", "--user-filter", "(memberOf=cn=user-group,ou=example,dc=min-domain-bind,dc=org)", @@ -99,7 +99,7 @@ func TestAddLdapBindDn(t *testing.T) { IsActive: true, Cfg: &ldap.Source{ Name: "ldap (via Bind DN) source min", - Host: "ldap-bind-server min", + HostList: "ldap-bind-server min", Port: 1234, SecurityProtocol: ldap.SecurityProtocol(0), UserBase: "ou=Users,dc=min-domain-bind,dc=org", @@ -115,7 +115,7 @@ func TestAddLdapBindDn(t *testing.T) { "ldap-test", "--name", "ldap (via Bind DN) source", "--security-protocol", "zzzzz", - "--host", "ldap-server", + "--host-list", "ldap-server", "--port", "1234", "--user-search-base", "ou=Users,dc=domain,dc=org", "--user-filter", "(memberOf=cn=user-group,ou=example,dc=domain,dc=org)", @@ -128,7 +128,7 @@ func TestAddLdapBindDn(t *testing.T) { args: []string{ "ldap-test", "--security-protocol", "unencrypted", - "--host", "ldap-server", + "--host-list", "ldap-server", "--port", "1234", "--user-search-base", "ou=Users,dc=domain,dc=org", "--user-filter", "(memberOf=cn=user-group,ou=example,dc=domain,dc=org)", @@ -141,7 +141,7 @@ func TestAddLdapBindDn(t *testing.T) { args: []string{ "ldap-test", "--name", "ldap (via Bind DN) source", - "--host", "ldap-server", + "--host-list", "ldap-server", "--port", "1234", "--user-search-base", "ou=Users,dc=domain,dc=org", "--user-filter", "(memberOf=cn=user-group,ou=example,dc=domain,dc=org)", @@ -160,7 +160,7 @@ func TestAddLdapBindDn(t *testing.T) { "--user-filter", "(memberOf=cn=user-group,ou=example,dc=domain,dc=org)", "--email-attribute", "mail", }, - errMsg: "host is not set", + errMsg: "host-list is not set", }, // case 6 { @@ -168,7 +168,7 @@ func TestAddLdapBindDn(t *testing.T) { "ldap-test", "--name", "ldap (via Bind DN) source", "--security-protocol", "unencrypted", - "--host", "ldap-server", + "--host-list", "ldap-server", "--user-search-base", "ou=Users,dc=domain,dc=org", "--user-filter", "(memberOf=cn=user-group,ou=example,dc=domain,dc=org)", "--email-attribute", "mail", @@ -181,7 +181,7 @@ func TestAddLdapBindDn(t *testing.T) { "ldap-test", "--name", "ldap (via Bind DN) source", "--security-protocol", "unencrypted", - "--host", "ldap-server", + "--host-list", "ldap-server", "--port", "1234", "--user-search-base", "ou=Users,dc=domain,dc=org", "--email-attribute", "mail", @@ -194,7 +194,7 @@ func TestAddLdapBindDn(t *testing.T) { "ldap-test", "--name", "ldap (via Bind DN) source", "--security-protocol", "unencrypted", - "--host", "ldap-server", + "--host-list", "ldap-server", "--port", "1234", "--user-search-base", "ou=Users,dc=domain,dc=org", "--user-filter", "(memberOf=cn=user-group,ou=example,dc=domain,dc=org)", @@ -260,7 +260,7 @@ func TestAddLdapSimpleAuth(t *testing.T) { "--not-active", "--security-protocol", "starttls", "--skip-tls-verify", - "--host", "ldap-simple-server full", + "--host-list", "ldap-simple-server full", "--port", "987", "--user-search-base", "ou=Users,dc=full-domain-simple,dc=org", "--user-filter", "(&(objectClass=posixAccount)(full-simple-cn=%s))", @@ -280,7 +280,7 @@ func TestAddLdapSimpleAuth(t *testing.T) { IsActive: false, Cfg: &ldap.Source{ Name: "ldap (simple auth) source full", - Host: "ldap-simple-server full", + HostList: "ldap-simple-server full", Port: 987, SecurityProtocol: ldap.SecurityProtocol(2), SkipVerify: true, @@ -305,7 +305,7 @@ func TestAddLdapSimpleAuth(t *testing.T) { "ldap-test", "--name", "ldap (simple auth) source min", "--security-protocol", "unencrypted", - "--host", "ldap-simple-server min", + "--host-list", "ldap-simple-server min", "--port", "123", "--user-filter", "(&(objectClass=posixAccount)(min-simple-cn=%s))", "--email-attribute", "mail-simple min", @@ -317,7 +317,7 @@ func TestAddLdapSimpleAuth(t *testing.T) { IsActive: true, Cfg: &ldap.Source{ Name: "ldap (simple auth) source min", - Host: "ldap-simple-server min", + HostList: "ldap-simple-server min", Port: 123, SecurityProtocol: ldap.SecurityProtocol(0), UserDN: "cn=%s,ou=Users,dc=min-domain-simple,dc=org", @@ -333,7 +333,7 @@ func TestAddLdapSimpleAuth(t *testing.T) { "ldap-test", "--name", "ldap (simple auth) source", "--security-protocol", "zzzzz", - "--host", "ldap-server", + "--host-list", "ldap-server", "--port", "123", "--user-filter", "(&(objectClass=posixAccount)(cn=%s))", "--email-attribute", "mail", @@ -346,7 +346,7 @@ func TestAddLdapSimpleAuth(t *testing.T) { args: []string{ "ldap-test", "--security-protocol", "unencrypted", - "--host", "ldap-server", + "--host-list", "ldap-server", "--port", "123", "--user-filter", "(&(objectClass=posixAccount)(cn=%s))", "--email-attribute", "mail", @@ -359,7 +359,7 @@ func TestAddLdapSimpleAuth(t *testing.T) { args: []string{ "ldap-test", "--name", "ldap (simple auth) source", - "--host", "ldap-server", + "--host-list", "ldap-server", "--port", "123", "--user-filter", "(&(objectClass=posixAccount)(cn=%s))", "--email-attribute", "mail", @@ -378,7 +378,7 @@ func TestAddLdapSimpleAuth(t *testing.T) { "--email-attribute", "mail", "--user-dn", "cn=%s,ou=Users,dc=domain,dc=org", }, - errMsg: "host is not set", + errMsg: "host-list is not set", }, // case 6 { @@ -386,7 +386,7 @@ func TestAddLdapSimpleAuth(t *testing.T) { "ldap-test", "--name", "ldap (simple auth) source", "--security-protocol", "unencrypted", - "--host", "ldap-server", + "--host-list", "ldap-server", "--user-filter", "(&(objectClass=posixAccount)(cn=%s))", "--email-attribute", "mail", "--user-dn", "cn=%s,ou=Users,dc=domain,dc=org", @@ -399,7 +399,7 @@ func TestAddLdapSimpleAuth(t *testing.T) { "ldap-test", "--name", "ldap (simple auth) source", "--security-protocol", "unencrypted", - "--host", "ldap-server", + "--host-list", "ldap-server", "--port", "123", "--email-attribute", "mail", "--user-dn", "cn=%s,ou=Users,dc=domain,dc=org", @@ -412,7 +412,7 @@ func TestAddLdapSimpleAuth(t *testing.T) { "ldap-test", "--name", "ldap (simple auth) source", "--security-protocol", "unencrypted", - "--host", "ldap-server", + "--host-list", "ldap-server", "--port", "123", "--user-filter", "(&(objectClass=posixAccount)(cn=%s))", "--user-dn", "cn=%s,ou=Users,dc=domain,dc=org", @@ -425,7 +425,7 @@ func TestAddLdapSimpleAuth(t *testing.T) { "ldap-test", "--name", "ldap (simple auth) source", "--security-protocol", "unencrypted", - "--host", "ldap-server", + "--host-list", "ldap-server", "--port", "123", "--user-filter", "(&(objectClass=posixAccount)(cn=%s))", "--email-attribute", "mail", @@ -494,7 +494,7 @@ func TestUpdateLdapBindDn(t *testing.T) { "--not-active", "--security-protocol", "LDAPS", "--skip-tls-verify", - "--host", "ldap-bind-server full", + "--host-list", "ldap-bind-server full", "--port", "9876", "--user-search-base", "ou=Users,dc=full-domain-bind,dc=org", "--user-filter", "(memberOf=cn=user-group,ou=example,dc=full-domain-bind,dc=org)", @@ -526,7 +526,7 @@ func TestUpdateLdapBindDn(t *testing.T) { IsSyncEnabled: true, Cfg: &ldap.Source{ Name: "ldap (via Bind DN) source full", - Host: "ldap-bind-server full", + HostList: "ldap-bind-server full", Port: 9876, SecurityProtocol: ldap.SecurityProtocol(1), SkipVerify: true, @@ -625,12 +625,12 @@ func TestUpdateLdapBindDn(t *testing.T) { args: []string{ "ldap-test", "--id", "1", - "--host", "ldap-server", + "--host-list", "ldap-server", }, authSource: &auth.Source{ Type: auth.LDAP, Cfg: &ldap.Source{ - Host: "ldap-server", + HostList: "ldap-server", }, }, }, @@ -957,7 +957,7 @@ func TestUpdateLdapSimpleAuth(t *testing.T) { "--not-active", "--security-protocol", "starttls", "--skip-tls-verify", - "--host", "ldap-simple-server full", + "--host-list", "ldap-simple-server full", "--port", "987", "--user-search-base", "ou=Users,dc=full-domain-simple,dc=org", "--user-filter", "(&(objectClass=posixAccount)(full-simple-cn=%s))", @@ -978,7 +978,7 @@ func TestUpdateLdapSimpleAuth(t *testing.T) { IsActive: false, Cfg: &ldap.Source{ Name: "ldap (simple auth) source full", - Host: "ldap-simple-server full", + HostList: "ldap-simple-server full", Port: 987, SecurityProtocol: ldap.SecurityProtocol(2), SkipVerify: true, @@ -1073,12 +1073,12 @@ func TestUpdateLdapSimpleAuth(t *testing.T) { args: []string{ "ldap-test", "--id", "1", - "--host", "ldap-server", + "--host-list", "ldap-server", }, authSource: &auth.Source{ Type: auth.DLDAP, Cfg: &ldap.Source{ - Host: "ldap-server", + HostList: "ldap-server", }, }, }, diff --git a/routers/web/admin/auths.go b/routers/web/admin/auths.go index 3b89be0f8fc2..4de0bb277a49 100644 --- a/routers/web/admin/auths.go +++ b/routers/web/admin/auths.go @@ -121,7 +121,7 @@ func parseLDAPConfig(form forms.AuthenticationForm) *ldap.Source { } return &ldap.Source{ Name: form.Name, - Host: form.Host, + HostList: form.Host, Port: form.Port, SecurityProtocol: ldap.SecurityProtocol(form.SecurityProtocol), SkipVerify: form.SkipVerify, diff --git a/services/auth/source/ldap/README.md b/services/auth/source/ldap/README.md index 34c811703f65..ec09eee05d99 100644 --- a/services/auth/source/ldap/README.md +++ b/services/auth/source/ldap/README.md @@ -32,8 +32,9 @@ share the following fields: * A name to assign to the new method of authorization. * Host **(required)** - * The address where the LDAP server can be reached. + * The list of addresses where the LDAP server(s) can be reached. * Example: mydomain.com + * Example (with multiple server hosts): mydomain.com, myotherdomain.com, mytempdomain.com * Port **(required)** * The port to use when connecting to the server. diff --git a/services/auth/source/ldap/source.go b/services/auth/source/ldap/source.go index dc4cb2c94031..8f986db1805c 100644 --- a/services/auth/source/ldap/source.go +++ b/services/auth/source/ldap/source.go @@ -25,7 +25,7 @@ import ( // Source Basic LDAP authentication service type Source struct { Name string // canonical name (ie. corporate.ad) - Host string // LDAP host + HostList string // list containing LDAP host(s) Port int // port number SecurityProtocol SecurityProtocol SkipVerify bool diff --git a/services/auth/source/ldap/source_search.go b/services/auth/source/ldap/source_search.go index 2a61386ae106..2b3cc091fd83 100644 --- a/services/auth/source/ldap/source_search.go +++ b/services/auth/source/ldap/source_search.go @@ -10,6 +10,8 @@ import ( "net" "strconv" "strings" + "sync" + "time" "code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/log" @@ -31,6 +33,12 @@ type SearchResult struct { Groups container.Set[string] } +// DialResult : dial response +type DialResult struct { + conn *ldap.Conn + err error +} + func (source *Source) sanitizedUserQuery(username string) (string, bool) { // See http://tools.ietf.org/search/rfc4515 badCharacters := "\x00()*\\" @@ -108,31 +116,80 @@ func (source *Source) findUserDN(l *ldap.Conn, name string) (string, bool) { return userDN, true } -func dial(source *Source) (*ldap.Conn, error) { - log.Trace("Dialing LDAP with security protocol (%v) without verifying: %v", source.SecurityProtocol, source.SkipVerify) +func dialHost(host string, source *Source, results chan DialResult, wg *sync.WaitGroup) { + defer wg.Done() tlsConfig := &tls.Config{ - ServerName: source.Host, + ServerName: host, InsecureSkipVerify: source.SkipVerify, } + var conn *ldap.Conn + var err error + if source.SecurityProtocol == SecurityProtocolLDAPS { - return ldap.DialTLS("tcp", net.JoinHostPort(source.Host, strconv.Itoa(source.Port)), tlsConfig) + conn, err = ldap.DialTLS("tcp", net.JoinHostPort(host, strconv.Itoa(source.Port)), tlsConfig) + } else { + conn, err = ldap.Dial("tcp", net.JoinHostPort(host, strconv.Itoa(source.Port))) + if err == nil && source.SecurityProtocol == SecurityProtocolStartTLS { + err = conn.StartTLS(tlsConfig) + } } - conn, err := ldap.Dial("tcp", net.JoinHostPort(source.Host, strconv.Itoa(source.Port))) if err != nil { - return nil, fmt.Errorf("error during Dial: %w", err) + if conn != nil { + conn.Close() + } + log.Trace("error during Dial for host %s: %w", host, err) + results <- DialResult{nil, err} + return } - if source.SecurityProtocol == SecurityProtocolStartTLS { - if err = conn.StartTLS(tlsConfig); err != nil { - conn.Close() - return nil, fmt.Errorf("error during StartTLS: %w", err) + conn.SetTimeout(time.Second * 10) + results <- DialResult{conn, nil} +} + +func dial(source *Source) (*ldap.Conn, error) { + log.Trace("Dialing LDAP with security protocol (%v) without verifying: %v", source.SecurityProtocol, source.SkipVerify) + + ldap.DefaultTimeout = time.Second * 10 + // Remove any extra spaces in HostList string + tempHostList := strings.ReplaceAll(source.HostList, " ", "") + // HostList is a list of hosts separated by commas + hostList := strings.Split(tempHostList, ",") + + results := make(chan DialResult, len(hostList)) + var wg sync.WaitGroup + + // Race all connections + for _, host := range hostList { + wg.Add(1) + go dialHost(host, source, results, &wg) + } + + // Close the results channel after all goroutines finish + go func() { + wg.Wait() + close(results) + }() + + for range hostList { + r := <-results + if r.err == nil { + // Close other connections still in progress + go func() { + for range hostList { + r := <-results + if r.conn != nil { + r.conn.Close() + } + } + }() + return r.conn, nil } } - return conn, nil + return nil, fmt.Errorf("dial failed for all provided servers: %s", hostList) } func bindUser(l *ldap.Conn, userDN, passwd string) error { @@ -257,7 +314,7 @@ func (source *Source) SearchEntry(name, passwd string, directBind bool) *SearchR } l, err := dial(source) if err != nil { - log.Error("LDAP Connect error, %s:%v", source.Host, err) + log.Error("LDAP Connect error, %s:%v", source.HostList, err) source.Enabled = false return nil } @@ -421,7 +478,7 @@ func (source *Source) UsePagedSearch() bool { func (source *Source) SearchEntries() ([]*SearchResult, error) { l, err := dial(source) if err != nil { - log.Error("LDAP Connect error, %s:%v", source.Host, err) + log.Error("LDAP Connect error, %s:%v", source.HostList, err) source.Enabled = false return nil, err } diff --git a/templates/admin/auth/edit.tmpl b/templates/admin/auth/edit.tmpl index 660f0d088154..0e669e60cae2 100644 --- a/templates/admin/auth/edit.tmpl +++ b/templates/admin/auth/edit.tmpl @@ -36,7 +36,7 @@
- +