diff --git a/conf/ldap.toml b/conf/ldap.toml index 4d890b180fc..49b95536b36 100644 --- a/conf/ldap.toml +++ b/conf/ldap.toml @@ -25,6 +25,9 @@ bind_dn = "cn=admin,dc=grafana,dc=org" # If the password contains # or ; you have to wrap it with triple quotes. Ex """#password;""" bind_password = 'grafana' +# Timeout in seconds (applies to each host specified in the 'host' entry (space separated)) +timeout = 10 + # User search filter, for example "(cn=%s)" or "(sAMAccountName=%s)" or "(uid=%s)" search_filter = "(cn=%s)" diff --git a/docs/sources/setup-grafana/configure-security/configure-authentication/ldap.md b/docs/sources/setup-grafana/configure-security/configure-authentication/ldap.md index 6a87bd93725..85fc53e6780 100644 --- a/docs/sources/setup-grafana/configure-security/configure-authentication/ldap.md +++ b/docs/sources/setup-grafana/configure-security/configure-authentication/ldap.md @@ -72,6 +72,9 @@ bind_dn = "cn=admin,dc=grafana,dc=org" # If the password contains # or ; you have to wrap it with triple quotes. Ex """#password;""" bind_password = "grafana" +# Timeout in seconds. Applies to each host specified in the 'host' entry (space separated). +timeout = 10 + # User search filter, for example "(cn=%s)" or "(sAMAccountName=%s)" or "(uid=%s)" # Allow login from email or username, example "(|(sAMAccountName=%s)(userPrincipalName=%s))" search_filter = "(cn=%s)" diff --git a/pkg/services/ldap/ldap.go b/pkg/services/ldap/ldap.go index 42b83741513..977f7f3db35 100644 --- a/pkg/services/ldap/ldap.go +++ b/pkg/services/ldap/ldap.go @@ -10,6 +10,7 @@ import ( "net" "strconv" "strings" + "time" "github.com/davecgh/go-spew/spew" "gopkg.in/ldap.v3" @@ -114,6 +115,9 @@ func (server *Server) Dial() error { return err } } + + timeout := time.Duration(server.Config.Timeout) * time.Second + for _, host := range strings.Split(server.Config.Host, " ") { // Remove any square brackets enclosing IPv6 addresses, a format we support for backwards compatibility host = strings.TrimSuffix(strings.TrimPrefix(host, "["), "]") @@ -128,17 +132,17 @@ func (server *Server) Dial() error { tlsCfg.Certificates = append(tlsCfg.Certificates, clientCert) } if server.Config.StartTLS { - server.Connection, err = ldap.Dial("tcp", address) + server.Connection, err = dialWithTimeout("tcp", address, timeout) if err == nil { if err = server.Connection.StartTLS(tlsCfg); err == nil { return nil } } } else { - server.Connection, err = ldap.DialTLS("tcp", address, tlsCfg) + server.Connection, err = dialTLSWithTimeout("tcp", address, tlsCfg, timeout) } } else { - server.Connection, err = ldap.Dial("tcp", address) + server.Connection, err = dialWithTimeout("tcp", address, timeout) } if err == nil { @@ -148,6 +152,30 @@ func (server *Server) Dial() error { return err } +// dialWithTimeout applies the specified timeout +// and connects to the given address on the given network using net.Dial +func dialWithTimeout(network, addr string, timeout time.Duration) (*ldap.Conn, error) { + c, err := net.DialTimeout(network, addr, timeout) + if err != nil { + return nil, err + } + conn := ldap.NewConn(c, false) + conn.Start() + return conn, nil +} + +// dialTLSWithTimeout applies the specified timeout +// connects to the given address on the given network using tls.Dial +func dialTLSWithTimeout(network, addr string, config *tls.Config, timeout time.Duration) (*ldap.Conn, error) { + c, err := tls.DialWithDialer(&net.Dialer{Timeout: timeout}, network, addr, config) + if err != nil { + return nil, err + } + conn := ldap.NewConn(c, true) + conn.Start() + return conn, nil +} + // Close closes the LDAP connection // Dial() sets the connection with the server for this Struct. Therefore, we require a // call to Dial() before being able to execute this function. diff --git a/pkg/services/ldap/settings.go b/pkg/services/ldap/settings.go index 17b091c688a..54afa4bb544 100644 --- a/pkg/services/ldap/settings.go +++ b/pkg/services/ldap/settings.go @@ -12,6 +12,8 @@ import ( "github.com/grafana/grafana/pkg/setting" ) +const defaultTimeout = 10 + // Config holds list of connections to LDAP type Config struct { Servers []*ServerConfig `toml:"servers"` @@ -29,6 +31,7 @@ type ServerConfig struct { ClientKey string `toml:"client_key"` BindDN string `toml:"bind_dn"` BindPassword string `toml:"bind_password"` + Timeout int `toml:"timeout"` Attr AttributeMap `toml:"attributes"` SearchFilter string `toml:"search_filter"` @@ -140,8 +143,8 @@ func readConfig(configFile string) (*Config, error) { return nil, fmt.Errorf("LDAP enabled but no LDAP servers defined in config file") } - // set default org id for _, server := range result.Servers { + // set default org id err = assertNotEmptyCfg(server.SearchFilter, "search_filter") if err != nil { return nil, fmt.Errorf("%v: %w", "Failed to validate SearchFilter section", err) @@ -160,6 +163,11 @@ func readConfig(configFile string) (*Config, error) { groupMap.OrgId = 1 } } + + // set default timeout if unspecified + if server.Timeout == 0 { + server.Timeout = defaultTimeout + } } return result, nil