Merge pull request #483 from ericchiang/ldap-groups
Clean up LDAP Connector
This commit is contained in:
commit
b5d2b7eba5
9 changed files with 356 additions and 308 deletions
|
@ -12,6 +12,7 @@ go:
|
||||||
env:
|
env:
|
||||||
- DEX_TEST_DSN="postgres://postgres@127.0.0.1:15432/postgres?sslmode=disable" ISOLATED=true
|
- DEX_TEST_DSN="postgres://postgres@127.0.0.1:15432/postgres?sslmode=disable" ISOLATED=true
|
||||||
DEX_TEST_LDAP_HOST="tlstest.local:1389"
|
DEX_TEST_LDAP_HOST="tlstest.local:1389"
|
||||||
|
DEX_TEST_LDAPS_HOST="tlstest.local:1636"
|
||||||
DEX_TEST_LDAP_BINDNAME="cn=admin,dc=example,dc=org"
|
DEX_TEST_LDAP_BINDNAME="cn=admin,dc=example,dc=org"
|
||||||
DEX_TEST_LDAP_BINDPASS="admin"
|
DEX_TEST_LDAP_BINDPASS="admin"
|
||||||
|
|
||||||
|
|
|
@ -45,11 +45,8 @@ The configuration for the local connector is always the same; it looks like this
|
||||||
This connector config lets users authenticate with other OIDC providers. In addition to `id` and `type`, the `oidc` connector takes the following additional fields:
|
This connector config lets users authenticate with other OIDC providers. In addition to `id` and `type`, the `oidc` connector takes the following additional fields:
|
||||||
|
|
||||||
* issuerURL: a `string`. The base URL for the OIDC provider. Should be a URL with an `https` scheme.
|
* issuerURL: a `string`. The base URL for the OIDC provider. Should be a URL with an `https` scheme.
|
||||||
|
|
||||||
* clientID: a `string`. The OIDC client ID.
|
* clientID: a `string`. The OIDC client ID.
|
||||||
|
|
||||||
* clientSecret: a `string`. The OIDC client secret.
|
* clientSecret: a `string`. The OIDC client secret.
|
||||||
|
|
||||||
* trustedEmailProvider: a `boolean`. If true dex will trust the email address claims from this provider and not require that users verify their emails.
|
* trustedEmailProvider: a `boolean`. If true dex will trust the email address claims from this provider and not require that users verify their emails.
|
||||||
|
|
||||||
In order to use the `oidc` connector you must register dex as an OIDC client; this mechanism is different from provider to provider. For Google, follow the instructions at their [developer site](https://developers.google.com/identity/protocols/OpenIDConnect?hl=en). Regardless of your provider, registering your client will also provide you with the client ID and secret.
|
In order to use the `oidc` connector you must register dex as an OIDC client; this mechanism is different from provider to provider. For Google, follow the instructions at their [developer site](https://developers.google.com/identity/protocols/OpenIDConnect?hl=en). Regardless of your provider, registering your client will also provide you with the client ID and secret.
|
||||||
|
@ -80,7 +77,6 @@ Here's what a `oidc` connector looks like configured for authenticating with Goo
|
||||||
This connector config lets users authenticate through [GitHub](https://github.com/). In addition to `id` and `type`, the `github` connector takes the following additional fields:
|
This connector config lets users authenticate through [GitHub](https://github.com/). In addition to `id` and `type`, the `github` connector takes the following additional fields:
|
||||||
|
|
||||||
* clientID: a `string`. The GitHub OAuth application client ID.
|
* clientID: a `string`. The GitHub OAuth application client ID.
|
||||||
|
|
||||||
* clientSecret: a `string`. The GitHub OAuth application client secret.
|
* clientSecret: a `string`. The GitHub OAuth application client secret.
|
||||||
|
|
||||||
To begin, register an OAuth application with GitHub through your, or your organization's [account settings](ttps://github.com/settings/applications/new). To register dex as a client of your GitHub application, enter dex's redirect URL under 'Authorization callback URL':
|
To begin, register an OAuth application with GitHub through your, or your organization's [account settings](ttps://github.com/settings/applications/new). To register dex as a client of your GitHub application, enter dex's redirect URL under 'Authorization callback URL':
|
||||||
|
@ -109,7 +105,6 @@ The `github` connector requests read only access to user's email through the [`u
|
||||||
This connector config lets users authenticate through [Bitbucket](https://bitbucket.org/). In addition to `id` and `type`, the `bitbucket` connector takes the following additional fields:
|
This connector config lets users authenticate through [Bitbucket](https://bitbucket.org/). In addition to `id` and `type`, the `bitbucket` connector takes the following additional fields:
|
||||||
|
|
||||||
* clientID: a `string`. The Bitbucket OAuth consumer client ID.
|
* clientID: a `string`. The Bitbucket OAuth consumer client ID.
|
||||||
|
|
||||||
* clientSecret: a `string`. The Bitbucket OAuth consumer client secret.
|
* clientSecret: a `string`. The Bitbucket OAuth consumer client secret.
|
||||||
|
|
||||||
To begin, register an OAuth consumer with Bitbucket through your, or your teams's management page. Follow the documentation at their [developer site](https://confluence.atlassian.com/bitbucket/oauth-on-bitbucket-cloud-238027431.html).
|
To begin, register an OAuth consumer with Bitbucket through your, or your teams's management page. Follow the documentation at their [developer site](https://confluence.atlassian.com/bitbucket/oauth-on-bitbucket-cloud-238027431.html).
|
||||||
|
@ -136,83 +131,85 @@ Here's an example of a `bitbucket` connector; the clientID and clientSecret shou
|
||||||
|
|
||||||
### `ldap` connector
|
### `ldap` connector
|
||||||
|
|
||||||
The `ldap` connector allows email/password based authentication hosted by dex, backed by a LDAP directory.
|
The `ldap` connector allows email/password based authentication hosted by dex, backed by a LDAP directory. The connector can operate in two primary modes:
|
||||||
|
|
||||||
Authentication is performed by binding to the configured LDAP server using the user supplied credentials. Successfull bind equals authenticated user.
|
1. Binding against a specific directory using the end user's credentials.
|
||||||
|
2. Searching a directory for a entry using a service account then attempting to bind with the user's credentials.
|
||||||
|
|
||||||
Optionally the connector can be configured to search before authentication. The entryDN found will be used to bind to the LDAP server.
|
User entries are expected to have an email attribute (configurable through "emailAttribute"), and optionally a display name attribute (configurable through "nameAttribute").
|
||||||
|
|
||||||
This feature must be enabled to get supplementary information from the directory (ID, Name, Email). This feature can also be used to limit access to the service.
|
___NOTE:___ Dex currently requires user registration with the dex system, even if that user already has an account with the upstream LDAP system. Installations that use this connector are recommended to provide the "--enable-automatic-registration" flag.
|
||||||
|
|
||||||
Example use case: Allow your users to log in with e-mail address as username instead of the identification string in your DNs (typically username).
|
|
||||||
|
|
||||||
___NOTE:___ Users must register with dex at first login. For this to work you have to run dex-worker with --enable-registration.
|
|
||||||
|
|
||||||
In addition to `id` and `type`, the `ldap` connector takes the following additional fields:
|
In addition to `id` and `type`, the `ldap` connector takes the following additional fields:
|
||||||
* serverHost: a `string`. The hostname for the LDAP Server.
|
|
||||||
|
|
||||||
* serverPort: a `unsigned 16-bit integer`. The port for the LDAP Server.
|
|
||||||
|
|
||||||
* timeout: `duration in milliseconds`. The timeout for connecting to and reading from LDAP Server in Milliseconds. Default: `60000` (60 Seconds)
|
|
||||||
|
|
||||||
|
* host: a `string`. The host and port of the LDAP server in form "host:port".
|
||||||
* useTLS: a `boolean`. Whether the LDAP Connector should issue a StartTLS after successfully connecting to the LDAP Server.
|
* useTLS: a `boolean`. Whether the LDAP Connector should issue a StartTLS after successfully connecting to the LDAP Server.
|
||||||
|
|
||||||
* useSSL: a `boolean`. Whether the LDAP Connector should expect the connection to be encrypted, typically used with ldaps port (636/tcp).
|
* useSSL: a `boolean`. Whether the LDAP Connector should expect the connection to be encrypted, typically used with ldaps port (636/tcp).
|
||||||
|
* certFile: a `string`. Optional path to x509 client certificate to present to LDAP server.
|
||||||
* certFile: a `string`. Optional Certificate to present to LDAP server.
|
* keyFile: a `string`. Key associated with x509 client cert specified in `certFile`.
|
||||||
|
|
||||||
* keyFile: a `string`. Key associated with Certificate specified in `certFile`.
|
|
||||||
|
|
||||||
* caFile: a `string`. Filename for PEM-file containing the set of root certificate authorities that the LDAP client use when verifying the server certificates. Default: use the host's root CA set.
|
* caFile: a `string`. Filename for PEM-file containing the set of root certificate authorities that the LDAP client use when verifying the server certificates. Default: use the host's root CA set.
|
||||||
|
|
||||||
* skipCertVerification: a `boolean`. Skip server certificate chain verification.
|
|
||||||
|
|
||||||
* maxIdleConn: a `integer`. Maximum number of idle LDAP Connections to keep in connection pool. Default: `5`
|
|
||||||
|
|
||||||
* baseDN: a `string`. Base DN from which Bind DN is built and searches are based.
|
* baseDN: a `string`. Base DN from which Bind DN is built and searches are based.
|
||||||
|
* nameAttribute: a `string`. Entity attribute to map to display name of users. Default: `cn`
|
||||||
* nameAttribute: a `string`. Attribute to map to Name. Default: `cn`
|
* emailAttribute: a `string`. Required. Attribute to map to Email. Default: `mail`
|
||||||
|
|
||||||
* emailAttribute: a `string`. Attribute to map to Email. Default: `mail`
|
|
||||||
|
|
||||||
* searchBeforeAuth: a `boolean`. Perform search for entryDN to be used for bind.
|
* searchBeforeAuth: a `boolean`. Perform search for entryDN to be used for bind.
|
||||||
|
* searchFilter: a `string`. Filter to apply to search. Variable substititions: `%u` User supplied username/e-mail address. `%b` BaseDN. Searches that return multiple entries are considered ambiguous and will return an error.
|
||||||
* searchFilter: a `string`. Filter to apply to search. Variable substititions: `%u` User supplied username/e-mail address. `%b` BaseDN.
|
|
||||||
|
|
||||||
* searchScope: a `string`. Scope of the search. `base|one|sub`. Default: `one`
|
* searchScope: a `string`. Scope of the search. `base|one|sub`. Default: `one`
|
||||||
|
|
||||||
* searchBindDN: a `string`. DN to bind as for search operations.
|
* searchBindDN: a `string`. DN to bind as for search operations.
|
||||||
|
|
||||||
* searchBindPw: a `string`. Password for bind for search operations.
|
* searchBindPw: a `string`. Password for bind for search operations.
|
||||||
|
* bindTemplate: a `string`. Template to build bindDN from user supplied credentials. Variable subtitutions: `%u` User supplied username/e-mail address. `%b` BaseDN. Default: `uid=%u,%b`.
|
||||||
|
|
||||||
* bindTemplate: a `string`. Template to build bindDN from user supplied credentials. Variable subtitutions: `%u` User supplied username/e-mail address. `%b` BaseDN. Default: `uid=%u,%b` ___NOTE:___ This is not used when searchBeforeAuth is enabled.
|
### Example: Authenticating against a specific directory
|
||||||
|
|
||||||
* trustedEmailProvider: a `boolean`. If true dex will trust the email address claims from this provider and not require that users verify their emails.
|
To authenticate against a specific LDAP directory level, use the "bindTemplate" field. This string describes how to map a username to a LDAP entity.
|
||||||
|
|
||||||
Here's an example of a `ldap` connector;
|
|
||||||
|
|
||||||
```
|
```
|
||||||
{
|
{
|
||||||
"type": "ldap",
|
"type": "ldap",
|
||||||
"id": "ldap",
|
"id": "ldap",
|
||||||
"serverHost": "127.0.0.1",
|
"host": "127.0.0.1:389",
|
||||||
"serverPort": 389,
|
"baseDN": "cn=users,cn=accounts,dc=auth,dc=example,dc=com",
|
||||||
"useTLS": true,
|
"bindTemplate": "uid=%u,%d"
|
||||||
"useSSL": false,
|
|
||||||
"skipCertVerification": false,
|
|
||||||
"baseDN": "ou=People,dc=example,dc=com",
|
|
||||||
"nameAttribute": "cn",
|
|
||||||
"emailAttribute": "mail",
|
|
||||||
"searchBeforeAuth": true,
|
|
||||||
"searchFilter": "(mail=%u)",
|
|
||||||
"searchScope": "one",
|
|
||||||
"searchBindDN": "searchuser",
|
|
||||||
"searchBindPw": "supersecret",
|
|
||||||
"bindTemplate": "uid=%u,%b",
|
|
||||||
"trustedEmailProvider": true
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
"bindTemplate" is a format string. `%d` is replaced by the value of "baseDN" and `%u` is replaced by the username attempting to login. In this case if a user "janedoe" attempts to authenticate, the bindTemplate will be expanded to:
|
||||||
|
|
||||||
|
```
|
||||||
|
uid=janedoe,cn=users,cn=accounts,dc=auth,dc=example,dc=com
|
||||||
|
```
|
||||||
|
|
||||||
|
The connector then attempts to bind as this entry using the password provided by the end user.
|
||||||
|
|
||||||
|
### Example: Searching the directory
|
||||||
|
|
||||||
|
The following configuration will search a directory using an LDAP filter. With FreeIPA
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
"type": "ldap",
|
||||||
|
"id": "ldap",
|
||||||
|
"host": "127.0.0.1:389",
|
||||||
|
"baseDN": "cn=auth,dc=example,dc=com",
|
||||||
|
|
||||||
|
"searchBeforeAuth": true,
|
||||||
|
"searchFilter": "(&(objectClass=person)(uid=%u))",
|
||||||
|
"searchScope": "sub",
|
||||||
|
|
||||||
|
"searchBindDN": "serviceAccountUser",
|
||||||
|
"searchBindPw": "serviceAccountPassword"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
"searchFilter" is a format string expanded in a similar manner as "bindTemplate". If the user "janedoe" attempts to authenticate, the connector will run the following query using the service account credentials.
|
||||||
|
|
||||||
|
```
|
||||||
|
(&(objectClass=person)(uid=janedoe))
|
||||||
|
```
|
||||||
|
|
||||||
|
If the search finds an entry, it will attempt to use the provided password to bind as that entry.
|
||||||
|
|
||||||
|
__NOTE__: Searches that return multiple entries will return an error.
|
||||||
|
|
||||||
## Setting the Configuration
|
## Setting the Configuration
|
||||||
|
|
||||||
To set a connectors configuration in dex, put it in some temporary file, then use the dexctl command to upload it to dex:
|
To set a connectors configuration in dex, put it in some temporary file, then use the dexctl command to upload it to dex:
|
||||||
|
|
|
@ -3,6 +3,8 @@ package connector
|
||||||
import (
|
import (
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
|
"errors"
|
||||||
|
"net"
|
||||||
|
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
|
@ -28,30 +30,63 @@ const (
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
RegisterConnectorConfigType(LDAPConnectorType, func() ConnectorConfig { return &LDAPConnectorConfig{} })
|
RegisterConnectorConfigType(LDAPConnectorType, func() ConnectorConfig { return &LDAPConnectorConfig{} })
|
||||||
|
|
||||||
|
// Set default ldap timeout.
|
||||||
|
ldap.DefaultTimeout = 30 * time.Second
|
||||||
}
|
}
|
||||||
|
|
||||||
type LDAPConnectorConfig struct {
|
type LDAPConnectorConfig struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
ServerHost string `json:"serverHost"`
|
|
||||||
ServerPort uint16 `json:"serverPort"`
|
// Host and port of ldap service in form "host:port"
|
||||||
Timeout time.Duration `json:"timeout"`
|
Host string `json:"host"`
|
||||||
UseTLS bool `json:"useTLS"`
|
|
||||||
UseSSL bool `json:"useSSL"`
|
// UseTLS indicates that the connector should use the TLS port.
|
||||||
CertFile string `json:"certFile"`
|
UseTLS bool `json:"useTLS"`
|
||||||
KeyFile string `json:"keyFile"`
|
UseSSL bool `json:"useSSL"`
|
||||||
CaFile string `json:"caFile"`
|
|
||||||
SkipCertVerification bool `json:"skipCertVerification"`
|
// Trusted TLS certificate when connecting to the LDAP server. If empty the
|
||||||
MaxIdleConn int `json:"maxIdleConn"`
|
// host's root certificates will be used.
|
||||||
BaseDN string `json:"baseDN"`
|
CaFile string `json:"caFile"`
|
||||||
NameAttribute string `json:"nameAttribute"`
|
// CertFile and KeyFile are used to specifiy client certificate data.
|
||||||
EmailAttribute string `json:"emailAttribute"`
|
CertFile string `json:"certFile"`
|
||||||
SearchBeforeAuth bool `json:"searchBeforeAuth"`
|
KeyFile string `json:"keyFile"`
|
||||||
SearchFilter string `json:"searchFilter"`
|
|
||||||
SearchScope string `json:"searchScope"`
|
MaxIdleConn int `json:"maxIdleConn"`
|
||||||
SearchBindDN string `json:"searchBindDN"`
|
|
||||||
SearchBindPw string `json:"searchBindPw"`
|
NameAttribute string `json:"nameAttribute"`
|
||||||
BindTemplate string `json:"bindTemplate"`
|
EmailAttribute string `json:"emailAttribute"`
|
||||||
TrustedEmailProvider bool `json:"trustedEmailProvider"`
|
|
||||||
|
// The place to start all searches from.
|
||||||
|
BaseDN string `json:"baseDN"`
|
||||||
|
|
||||||
|
// Search fields indicate how to search for user records in LDAP.
|
||||||
|
SearchBeforeAuth bool `json:"searchBeforeAuth"`
|
||||||
|
SearchFilter string `json:"searchFilter"`
|
||||||
|
SearchScope string `json:"searchScope"`
|
||||||
|
SearchBindDN string `json:"searchBindDN"`
|
||||||
|
SearchBindPw string `json:"searchBindPw"`
|
||||||
|
SearchGroupFilter string `json:"searchGroupFilter"`
|
||||||
|
|
||||||
|
// BindTemplate is a format string that maps user names to a record to bind as.
|
||||||
|
// It's passed both the username entered by the end user and the base DN.
|
||||||
|
//
|
||||||
|
// For example the bindTemplate
|
||||||
|
//
|
||||||
|
// "uid=%u,%d"
|
||||||
|
//
|
||||||
|
// with the username "johndoe" and basename "ou=People,dc=example,dc=com" would attempt
|
||||||
|
// to bind as
|
||||||
|
//
|
||||||
|
// "uid=johndoe,ou=People,dc=example,dc=com"
|
||||||
|
//
|
||||||
|
BindTemplate string `json:"bindTemplate"`
|
||||||
|
|
||||||
|
// DEPRICATED fields that exist for backward compatibility.
|
||||||
|
// Use "host" instead of "ServerHost" and "ServerPort"
|
||||||
|
ServerHost string `json:"serverHost"`
|
||||||
|
ServerPort uint16 `json:"serverPort"`
|
||||||
|
Timeout time.Duration `json:"timeout"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cfg *LDAPConnectorConfig) ConnectorID() string {
|
func (cfg *LDAPConnectorConfig) ConnectorID() string {
|
||||||
|
@ -63,14 +98,28 @@ func (cfg *LDAPConnectorConfig) ConnectorType() string {
|
||||||
}
|
}
|
||||||
|
|
||||||
type LDAPConnector struct {
|
type LDAPConnector struct {
|
||||||
id string
|
id string
|
||||||
idp *LDAPIdentityProvider
|
namespace url.URL
|
||||||
namespace url.URL
|
loginFunc oidc.LoginFunc
|
||||||
trustedEmailProvider bool
|
loginTpl *template.Template
|
||||||
loginFunc oidc.LoginFunc
|
|
||||||
loginTpl *template.Template
|
baseDN string
|
||||||
|
nameAttribute string
|
||||||
|
emailAttribute string
|
||||||
|
|
||||||
|
searchBeforeAuth bool
|
||||||
|
searchFilter string
|
||||||
|
searchScope int
|
||||||
|
searchBindDN string
|
||||||
|
searchBindPw string
|
||||||
|
|
||||||
|
bindTemplate string
|
||||||
|
|
||||||
|
ldapPool *LDAPPool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const defaultPoolCheckTimer = 7200 * time.Second
|
||||||
|
|
||||||
func (cfg *LDAPConnectorConfig) Connector(ns url.URL, lf oidc.LoginFunc, tpls *template.Template) (Connector, error) {
|
func (cfg *LDAPConnectorConfig) Connector(ns url.URL, lf oidc.LoginFunc, tpls *template.Template) (Connector, error) {
|
||||||
ns.Path = path.Join(ns.Path, httpPathCallback)
|
ns.Path = path.Join(ns.Path, httpPathCallback)
|
||||||
tpl := tpls.Lookup(LDAPLoginPageTemplateName)
|
tpl := tpls.Lookup(LDAPLoginPageTemplateName)
|
||||||
|
@ -78,14 +127,6 @@ func (cfg *LDAPConnectorConfig) Connector(ns url.URL, lf oidc.LoginFunc, tpls *t
|
||||||
return nil, fmt.Errorf("unable to find necessary HTML template")
|
return nil, fmt.Errorf("unable to find necessary HTML template")
|
||||||
}
|
}
|
||||||
|
|
||||||
// defaults
|
|
||||||
const defaultNameAttribute = "cn"
|
|
||||||
const defaultEmailAttribute = "mail"
|
|
||||||
const defaultBindTemplate = "uid=%u,%b"
|
|
||||||
const defaultSearchScope = ldap.ScopeWholeSubtree
|
|
||||||
const defaultMaxIdleConns = 5
|
|
||||||
const defaultPoolCheckTimer = 7200 * time.Second
|
|
||||||
|
|
||||||
if cfg.UseTLS && cfg.UseSSL {
|
if cfg.UseTLS && cfg.UseSSL {
|
||||||
return nil, fmt.Errorf("Invalid configuration. useTLS and useSSL are mutual exclusive.")
|
return nil, fmt.Errorf("Invalid configuration. useTLS and useSSL are mutual exclusive.")
|
||||||
}
|
}
|
||||||
|
@ -94,26 +135,23 @@ func (cfg *LDAPConnectorConfig) Connector(ns url.URL, lf oidc.LoginFunc, tpls *t
|
||||||
return nil, fmt.Errorf("Invalid configuration. Both certFile and keyFile must be specified.")
|
return nil, fmt.Errorf("Invalid configuration. Both certFile and keyFile must be specified.")
|
||||||
}
|
}
|
||||||
|
|
||||||
nameAttribute := defaultNameAttribute
|
// Set default values
|
||||||
if len(cfg.NameAttribute) > 0 {
|
if cfg.NameAttribute == "" {
|
||||||
nameAttribute = cfg.NameAttribute
|
cfg.NameAttribute = "cn"
|
||||||
}
|
}
|
||||||
|
if cfg.EmailAttribute == "" {
|
||||||
emailAttribute := defaultEmailAttribute
|
cfg.EmailAttribute = "mail"
|
||||||
if len(cfg.EmailAttribute) > 0 {
|
|
||||||
emailAttribute = cfg.EmailAttribute
|
|
||||||
}
|
}
|
||||||
|
if cfg.MaxIdleConn > 0 {
|
||||||
bindTemplate := defaultBindTemplate
|
cfg.MaxIdleConn = 5
|
||||||
if len(cfg.BindTemplate) > 0 {
|
|
||||||
if cfg.SearchBeforeAuth {
|
|
||||||
log.Warningf("bindTemplate not used when searchBeforeAuth specified.")
|
|
||||||
}
|
|
||||||
bindTemplate = cfg.BindTemplate
|
|
||||||
}
|
}
|
||||||
|
if cfg.BindTemplate == "" {
|
||||||
searchScope := defaultSearchScope
|
cfg.BindTemplate = "uid=%u,%b"
|
||||||
if len(cfg.SearchScope) > 0 {
|
} else if cfg.SearchBeforeAuth {
|
||||||
|
log.Warningf("bindTemplate not used when searchBeforeAuth specified.")
|
||||||
|
}
|
||||||
|
searchScope := ldap.ScopeWholeSubtree
|
||||||
|
if cfg.SearchScope != "" {
|
||||||
switch {
|
switch {
|
||||||
case strings.EqualFold(cfg.SearchScope, "BASE"):
|
case strings.EqualFold(cfg.SearchScope, "BASE"):
|
||||||
searchScope = ldap.ScopeBaseObject
|
searchScope = ldap.ScopeBaseObject
|
||||||
|
@ -126,15 +164,21 @@ func (cfg *LDAPConnectorConfig) Connector(ns url.URL, lf oidc.LoginFunc, tpls *t
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if cfg.Timeout != 0 {
|
if cfg.Host == "" {
|
||||||
ldap.DefaultTimeout = cfg.Timeout * time.Millisecond
|
if cfg.ServerHost == "" {
|
||||||
|
return nil, errors.New("no host provided")
|
||||||
|
}
|
||||||
|
// For backward compatibility construct host form old fields.
|
||||||
|
cfg.Host = fmt.Sprintf("%s:%d", cfg.ServerHost, cfg.ServerPort)
|
||||||
}
|
}
|
||||||
|
|
||||||
tlsConfig := &tls.Config{
|
host, _, err := net.SplitHostPort(cfg.Host)
|
||||||
ServerName: cfg.ServerHost,
|
if err != nil {
|
||||||
InsecureSkipVerify: cfg.SkipCertVerification,
|
return nil, fmt.Errorf("host is not of form 'host:port': %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tlsConfig := &tls.Config{ServerName: host}
|
||||||
|
|
||||||
if (cfg.UseTLS || cfg.UseSSL) && len(cfg.CaFile) > 0 {
|
if (cfg.UseTLS || cfg.UseSSL) && len(cfg.CaFile) > 0 {
|
||||||
buf, err := ioutil.ReadFile(cfg.CaFile)
|
buf, err := ioutil.ReadFile(cfg.CaFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -158,41 +202,28 @@ func (cfg *LDAPConnectorConfig) Connector(ns url.URL, lf oidc.LoginFunc, tpls *t
|
||||||
tlsConfig.Certificates = []tls.Certificate{cert}
|
tlsConfig.Certificates = []tls.Certificate{cert}
|
||||||
}
|
}
|
||||||
|
|
||||||
maxIdleConn := defaultMaxIdleConns
|
idpc := &LDAPConnector{
|
||||||
if cfg.MaxIdleConn > 0 {
|
id: cfg.ID,
|
||||||
maxIdleConn = cfg.MaxIdleConn
|
namespace: ns,
|
||||||
}
|
loginFunc: lf,
|
||||||
|
loginTpl: tpl,
|
||||||
ldapPool := &LDAPPool{
|
|
||||||
MaxIdleConn: maxIdleConn,
|
|
||||||
PoolCheckTimer: defaultPoolCheckTimer,
|
|
||||||
ServerHost: cfg.ServerHost,
|
|
||||||
ServerPort: cfg.ServerPort,
|
|
||||||
UseTLS: cfg.UseTLS,
|
|
||||||
UseSSL: cfg.UseSSL,
|
|
||||||
TLSConfig: tlsConfig,
|
|
||||||
}
|
|
||||||
|
|
||||||
idp := &LDAPIdentityProvider{
|
|
||||||
baseDN: cfg.BaseDN,
|
baseDN: cfg.BaseDN,
|
||||||
nameAttribute: nameAttribute,
|
nameAttribute: cfg.NameAttribute,
|
||||||
emailAttribute: emailAttribute,
|
emailAttribute: cfg.EmailAttribute,
|
||||||
searchBeforeAuth: cfg.SearchBeforeAuth,
|
searchBeforeAuth: cfg.SearchBeforeAuth,
|
||||||
searchFilter: cfg.SearchFilter,
|
searchFilter: cfg.SearchFilter,
|
||||||
searchScope: searchScope,
|
searchScope: searchScope,
|
||||||
searchBindDN: cfg.SearchBindDN,
|
searchBindDN: cfg.SearchBindDN,
|
||||||
searchBindPw: cfg.SearchBindPw,
|
searchBindPw: cfg.SearchBindPw,
|
||||||
bindTemplate: bindTemplate,
|
bindTemplate: cfg.BindTemplate,
|
||||||
ldapPool: ldapPool,
|
ldapPool: &LDAPPool{
|
||||||
}
|
MaxIdleConn: cfg.MaxIdleConn,
|
||||||
|
PoolCheckTimer: defaultPoolCheckTimer,
|
||||||
idpc := &LDAPConnector{
|
Host: cfg.Host,
|
||||||
id: cfg.ID,
|
UseTLS: cfg.UseTLS,
|
||||||
idp: idp,
|
UseSSL: cfg.UseSSL,
|
||||||
namespace: ns,
|
TLSConfig: tlsConfig,
|
||||||
trustedEmailProvider: cfg.TrustedEmailProvider,
|
},
|
||||||
loginFunc: lf,
|
|
||||||
loginTpl: tpl,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return idpc, nil
|
return idpc, nil
|
||||||
|
@ -203,11 +234,10 @@ func (c *LDAPConnector) ID() string {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *LDAPConnector) Healthy() error {
|
func (c *LDAPConnector) Healthy() error {
|
||||||
ldapConn, err := c.idp.ldapPool.Acquire()
|
return c.ldapPool.Do(func(c *ldap.Conn) error {
|
||||||
if err == nil {
|
// Attempt an anonymous bind.
|
||||||
c.idp.ldapPool.Put(ldapConn)
|
return c.Bind("", "")
|
||||||
}
|
})
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *LDAPConnector) LoginURL(sessionKey, prompt string) (string, error) {
|
func (c *LDAPConnector) LoginURL(sessionKey, prompt string) (string, error) {
|
||||||
|
@ -221,7 +251,7 @@ func (c *LDAPConnector) LoginURL(sessionKey, prompt string) (string, error) {
|
||||||
|
|
||||||
func (c *LDAPConnector) Register(mux *http.ServeMux, errorURL url.URL) {
|
func (c *LDAPConnector) Register(mux *http.ServeMux, errorURL url.URL) {
|
||||||
route := path.Join(c.namespace.Path, "login")
|
route := path.Join(c.namespace.Path, "login")
|
||||||
mux.Handle(route, handleLoginFunc(c.loginFunc, c.loginTpl, c.idp, route, errorURL))
|
mux.Handle(route, handlePasswordLogin(c.loginFunc, c.loginTpl, c, route, errorURL))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *LDAPConnector) Sync() chan struct{} {
|
func (c *LDAPConnector) Sync() chan struct{} {
|
||||||
|
@ -230,8 +260,8 @@ func (c *LDAPConnector) Sync() chan struct{} {
|
||||||
go func() {
|
go func() {
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-time.After(c.idp.ldapPool.PoolCheckTimer):
|
case <-time.After(c.ldapPool.PoolCheckTimer):
|
||||||
alive, killed := c.idp.ldapPool.CheckConnections()
|
alive, killed := c.ldapPool.CheckConnections()
|
||||||
if alive > 0 {
|
if alive > 0 {
|
||||||
log.Infof("Connector ID=%v idle_conns=%v", c.id, alive)
|
log.Infof("Connector ID=%v idle_conns=%v", c.id, alive)
|
||||||
}
|
}
|
||||||
|
@ -247,38 +277,41 @@ func (c *LDAPConnector) Sync() chan struct{} {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *LDAPConnector) TrustedEmailProvider() bool {
|
func (c *LDAPConnector) TrustedEmailProvider() bool {
|
||||||
return c.trustedEmailProvider
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// A LDAPPool is a Connection Pool for LDAP connections
|
// A LDAPPool is a Connection Pool for LDAP connections. Use Do() to request connections
|
||||||
// Initialize exported fields and use Acquire() to get a connection.
|
// from the pool.
|
||||||
// Use Put() to put it back into the pool.
|
|
||||||
type LDAPPool struct {
|
type LDAPPool struct {
|
||||||
m sync.Mutex
|
m sync.Mutex
|
||||||
conns map[*ldap.Conn]struct{}
|
conns map[*ldap.Conn]struct{}
|
||||||
MaxIdleConn int
|
MaxIdleConn int
|
||||||
PoolCheckTimer time.Duration
|
PoolCheckTimer time.Duration
|
||||||
ServerHost string
|
Host string
|
||||||
ServerPort uint16
|
|
||||||
UseTLS bool
|
UseTLS bool
|
||||||
UseSSL bool
|
UseSSL bool
|
||||||
TLSConfig *tls.Config
|
TLSConfig *tls.Config
|
||||||
}
|
}
|
||||||
|
|
||||||
// Acquire removes and returns a random connection from the pool. A new connection is returned
|
// Do runs a function which requires an LDAP connection.
|
||||||
// if there are no connections available in the pool.
|
//
|
||||||
func (p *LDAPPool) Acquire() (*ldap.Conn, error) {
|
// The connection will be unauthenticated with the server and should not be closed by f.
|
||||||
|
func (p *LDAPPool) Do(f func(conn *ldap.Conn) error) (err error) {
|
||||||
conn := p.removeRandomConn()
|
conn := p.removeRandomConn()
|
||||||
if conn != nil {
|
if conn == nil {
|
||||||
return conn, nil
|
conn, err = p.ldapConnect()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return p.ldapConnect()
|
defer p.put(conn)
|
||||||
|
return f(conn)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Put makes a connection ready for re-use and puts it back into the pool. If the connection
|
// put makes a connection ready for re-use and puts it back into the pool. If the connection
|
||||||
// cannot be reused it is discarded. If there already are MaxIdleConn connections in the pool
|
// cannot be reused it is discarded. If there already are MaxIdleConn connections in the pool
|
||||||
// the connection is discarded.
|
// the connection is discarded.
|
||||||
func (p *LDAPPool) Put(c *ldap.Conn) {
|
func (p *LDAPPool) put(c *ldap.Conn) {
|
||||||
p.m.Lock()
|
p.m.Lock()
|
||||||
if p.conns == nil {
|
if p.conns == nil {
|
||||||
// First call to Put, initialize map
|
// First call to Put, initialize map
|
||||||
|
@ -345,7 +378,7 @@ func (p *LDAPPool) CheckConnections() (int, int) {
|
||||||
if ok {
|
if ok {
|
||||||
err := ldapPing(conn)
|
err := ldapPing(conn)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
p.Put(conn)
|
p.put(conn)
|
||||||
alive++
|
alive++
|
||||||
} else {
|
} else {
|
||||||
conn.Close()
|
conn.Close()
|
||||||
|
@ -363,31 +396,17 @@ func ldapPing(conn *ldap.Conn) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
type LDAPIdentityProvider struct {
|
|
||||||
baseDN string
|
|
||||||
nameAttribute string
|
|
||||||
emailAttribute string
|
|
||||||
searchBeforeAuth bool
|
|
||||||
searchFilter string
|
|
||||||
searchScope int
|
|
||||||
searchBindDN string
|
|
||||||
searchBindPw string
|
|
||||||
bindTemplate string
|
|
||||||
ldapPool *LDAPPool
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *LDAPPool) ldapConnect() (*ldap.Conn, error) {
|
func (p *LDAPPool) ldapConnect() (*ldap.Conn, error) {
|
||||||
var err error
|
var err error
|
||||||
var ldapConn *ldap.Conn
|
var ldapConn *ldap.Conn
|
||||||
|
|
||||||
log.Debugf("LDAPConnect()")
|
|
||||||
if p.UseSSL {
|
if p.UseSSL {
|
||||||
ldapConn, err = ldap.DialTLS("tcp", fmt.Sprintf("%s:%d", p.ServerHost, p.ServerPort), p.TLSConfig)
|
ldapConn, err = ldap.DialTLS("tcp", p.Host, p.TLSConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
ldapConn, err = ldap.Dial("tcp", fmt.Sprintf("%s:%d", p.ServerHost, p.ServerPort))
|
ldapConn, err = ldap.Dial("tcp", p.Host)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -402,74 +421,114 @@ func (p *LDAPPool) ldapConnect() (*ldap.Conn, error) {
|
||||||
return ldapConn, err
|
return ldapConn, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *LDAPIdentityProvider) ParseString(template, username string) string {
|
// invalidBindCredentials determines if a bind error was the result of invalid
|
||||||
|
// credentials.
|
||||||
|
func invalidBindCredentials(err error) bool {
|
||||||
|
ldapErr, ok := err.(*ldap.Error)
|
||||||
|
if ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return ldapErr.ResultCode == ldap.LDAPResultInvalidCredentials
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *LDAPConnector) formatDN(template, username string) string {
|
||||||
result := template
|
result := template
|
||||||
result = strings.Replace(result, "%u", username, -1)
|
result = strings.Replace(result, "%u", username, -1)
|
||||||
result = strings.Replace(result, "%b", m.baseDN, -1)
|
result = strings.Replace(result, "%b", c.baseDN, -1)
|
||||||
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *LDAPIdentityProvider) Identity(username, password string) (*oidc.Identity, error) {
|
func (c *LDAPConnector) Identity(username, password string) (*oidc.Identity, error) {
|
||||||
var err error
|
log.Errorf("handling identity")
|
||||||
var bindDN, ldapUid, ldapName, ldapEmail string
|
var (
|
||||||
var ldapConn *ldap.Conn
|
identity *oidc.Identity
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
if c.searchBeforeAuth {
|
||||||
|
err = c.ldapPool.Do(func(conn *ldap.Conn) error {
|
||||||
|
if err := conn.Bind(c.searchBindDN, c.searchBindPw); err != nil {
|
||||||
|
// Don't wrap error as it may be a specific LDAP error.
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
ldapConn, err = m.ldapPool.Acquire()
|
filter := c.formatDN(c.searchFilter, username)
|
||||||
if err != nil {
|
req := &ldap.SearchRequest{
|
||||||
return nil, err
|
BaseDN: c.baseDN,
|
||||||
}
|
Scope: c.searchScope,
|
||||||
defer m.ldapPool.Put(ldapConn)
|
Filter: filter,
|
||||||
|
Attributes: []string{c.nameAttribute, c.emailAttribute},
|
||||||
|
}
|
||||||
|
resp, err := conn.Search(req)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("search failed: %v", err)
|
||||||
|
}
|
||||||
|
switch len(resp.Entries) {
|
||||||
|
case 0:
|
||||||
|
return errors.New("user not found by search")
|
||||||
|
case 1:
|
||||||
|
default:
|
||||||
|
// For now reject searches that return multiple entries to avoid ambiguity.
|
||||||
|
log.Errorf("LDAP search %q returned %d entries. Must disambiguate searchFilter.", filter, len(resp.Entries))
|
||||||
|
return errors.New("search returned multiple entries")
|
||||||
|
}
|
||||||
|
|
||||||
if m.searchBeforeAuth {
|
entry := resp.Entries[0]
|
||||||
err = ldapConn.Bind(m.searchBindDN, m.searchBindPw)
|
email := entry.GetAttributeValue(c.emailAttribute)
|
||||||
if err != nil {
|
if email == "" {
|
||||||
return nil, err
|
return fmt.Errorf("no email attribute found")
|
||||||
}
|
}
|
||||||
|
|
||||||
filter := m.ParseString(m.searchFilter, username)
|
identity = &oidc.Identity{
|
||||||
|
ID: entry.DN,
|
||||||
|
Name: entry.GetAttributeValue(c.nameAttribute),
|
||||||
|
Email: email,
|
||||||
|
}
|
||||||
|
|
||||||
attributes := []string{
|
// Attempt to bind as the end user.
|
||||||
m.nameAttribute,
|
return conn.Bind(entry.DN, password)
|
||||||
m.emailAttribute,
|
})
|
||||||
}
|
|
||||||
|
|
||||||
s := ldap.NewSearchRequest(m.baseDN, m.searchScope, ldap.NeverDerefAliases, 0, 0, false, filter, attributes, nil)
|
|
||||||
|
|
||||||
sr, err := ldapConn.Search(s)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if len(sr.Entries) == 0 {
|
|
||||||
err = fmt.Errorf("Search returned no match. filter='%v' base='%v'", filter, m.baseDN)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
bindDN = sr.Entries[0].DN
|
|
||||||
ldapName = sr.Entries[0].GetAttributeValue(m.nameAttribute)
|
|
||||||
ldapEmail = sr.Entries[0].GetAttributeValue(m.emailAttribute)
|
|
||||||
|
|
||||||
// prepare LDAP connection for bind as user
|
|
||||||
m.ldapPool.Put(ldapConn)
|
|
||||||
ldapConn, err = m.ldapPool.Acquire()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
bindDN = m.ParseString(m.bindTemplate, username)
|
err = c.ldapPool.Do(func(conn *ldap.Conn) error {
|
||||||
|
userBindDN := c.formatDN(c.bindTemplate, username)
|
||||||
|
if err := conn.Bind(userBindDN, password); err != nil {
|
||||||
|
// Don't wrap error as it may be a specific LDAP error.
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
req := &ldap.SearchRequest{
|
||||||
|
BaseDN: userBindDN,
|
||||||
|
Scope: ldap.ScopeBaseObject, // Only attempt to
|
||||||
|
Filter: "(objectClass=*)",
|
||||||
|
}
|
||||||
|
resp, err := conn.Search(req)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("search failed: %v", err)
|
||||||
|
}
|
||||||
|
if len(resp.Entries) == 0 {
|
||||||
|
// Are there cases were a user wouldn't be able to see their own entity?
|
||||||
|
return fmt.Errorf("user not found by search")
|
||||||
|
}
|
||||||
|
entry := resp.Entries[0]
|
||||||
|
email := entry.GetAttributeValue(c.emailAttribute)
|
||||||
|
if email == "" {
|
||||||
|
return fmt.Errorf("no email attribute found")
|
||||||
|
}
|
||||||
|
|
||||||
|
identity = &oidc.Identity{
|
||||||
|
ID: entry.DN,
|
||||||
|
Name: entry.GetAttributeValue(c.nameAttribute),
|
||||||
|
Email: email,
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// authenticate user
|
|
||||||
err = ldapConn.Bind(bindDN, password)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if !invalidBindCredentials(err) {
|
||||||
|
log.Errorf("failed to connect to LDAP for search bind: %v", err)
|
||||||
|
}
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
return identity, nil
|
||||||
ldapUid = bindDN
|
|
||||||
|
|
||||||
return &oidc.Identity{
|
|
||||||
ID: ldapUid,
|
|
||||||
Name: ldapName,
|
|
||||||
Email: ldapEmail,
|
|
||||||
}, nil
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,6 +21,7 @@ func init() {
|
||||||
func TestLDAPConnectorConfigValidTLS(t *testing.T) {
|
func TestLDAPConnectorConfigValidTLS(t *testing.T) {
|
||||||
cc := LDAPConnectorConfig{
|
cc := LDAPConnectorConfig{
|
||||||
ID: "ldap",
|
ID: "ldap",
|
||||||
|
Host: "example.com:636",
|
||||||
UseTLS: true,
|
UseTLS: true,
|
||||||
UseSSL: false,
|
UseSSL: false,
|
||||||
}
|
}
|
||||||
|
@ -34,6 +35,7 @@ func TestLDAPConnectorConfigValidTLS(t *testing.T) {
|
||||||
func TestLDAPConnectorConfigInvalidSSLandTLS(t *testing.T) {
|
func TestLDAPConnectorConfigInvalidSSLandTLS(t *testing.T) {
|
||||||
cc := LDAPConnectorConfig{
|
cc := LDAPConnectorConfig{
|
||||||
ID: "ldap",
|
ID: "ldap",
|
||||||
|
Host: "example.com:636",
|
||||||
UseTLS: true,
|
UseTLS: true,
|
||||||
UseSSL: true,
|
UseSSL: true,
|
||||||
}
|
}
|
||||||
|
@ -47,6 +49,7 @@ func TestLDAPConnectorConfigInvalidSSLandTLS(t *testing.T) {
|
||||||
func TestLDAPConnectorConfigValidSearchScope(t *testing.T) {
|
func TestLDAPConnectorConfigValidSearchScope(t *testing.T) {
|
||||||
cc := LDAPConnectorConfig{
|
cc := LDAPConnectorConfig{
|
||||||
ID: "ldap",
|
ID: "ldap",
|
||||||
|
Host: "example.com:636",
|
||||||
SearchScope: "one",
|
SearchScope: "one",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -59,6 +62,7 @@ func TestLDAPConnectorConfigValidSearchScope(t *testing.T) {
|
||||||
func TestLDAPConnectorConfigInvalidSearchScope(t *testing.T) {
|
func TestLDAPConnectorConfigInvalidSearchScope(t *testing.T) {
|
||||||
cc := LDAPConnectorConfig{
|
cc := LDAPConnectorConfig{
|
||||||
ID: "ldap",
|
ID: "ldap",
|
||||||
|
Host: "example.com:636",
|
||||||
SearchScope: "three",
|
SearchScope: "three",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -71,6 +75,7 @@ func TestLDAPConnectorConfigInvalidSearchScope(t *testing.T) {
|
||||||
func TestLDAPConnectorConfigInvalidCertFileNoKeyFile(t *testing.T) {
|
func TestLDAPConnectorConfigInvalidCertFileNoKeyFile(t *testing.T) {
|
||||||
cc := LDAPConnectorConfig{
|
cc := LDAPConnectorConfig{
|
||||||
ID: "ldap",
|
ID: "ldap",
|
||||||
|
Host: "example.com:636",
|
||||||
CertFile: "/tmp/ldap.crt",
|
CertFile: "/tmp/ldap.crt",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -83,6 +88,7 @@ func TestLDAPConnectorConfigInvalidCertFileNoKeyFile(t *testing.T) {
|
||||||
func TestLDAPConnectorConfigValidCertFileAndKeyFile(t *testing.T) {
|
func TestLDAPConnectorConfigValidCertFileAndKeyFile(t *testing.T) {
|
||||||
cc := LDAPConnectorConfig{
|
cc := LDAPConnectorConfig{
|
||||||
ID: "ldap",
|
ID: "ldap",
|
||||||
|
Host: "example.com:636",
|
||||||
CertFile: "/tmp/ldap.crt",
|
CertFile: "/tmp/ldap.crt",
|
||||||
KeyFile: "/tmp/ldap.key",
|
KeyFile: "/tmp/ldap.key",
|
||||||
}
|
}
|
||||||
|
|
|
@ -87,7 +87,7 @@ func (c *LocalConnector) LoginURL(sessionKey, prompt string) (string, error) {
|
||||||
|
|
||||||
func (c *LocalConnector) Register(mux *http.ServeMux, errorURL url.URL) {
|
func (c *LocalConnector) Register(mux *http.ServeMux, errorURL url.URL) {
|
||||||
route := c.namespace.Path + "/login"
|
route := c.namespace.Path + "/login"
|
||||||
mux.Handle(route, handleLoginFunc(c.loginFunc, c.loginTpl, c.idp, route, errorURL))
|
mux.Handle(route, handlePasswordLogin(c.loginFunc, c.loginTpl, c.idp, route, errorURL))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *LocalConnector) Sync() chan struct{} {
|
func (c *LocalConnector) Sync() chan struct{} {
|
||||||
|
|
|
@ -65,7 +65,3 @@ type ConnectorConfigRepo interface {
|
||||||
GetConnectorByID(repo.Transaction, string) (ConnectorConfig, error)
|
GetConnectorByID(repo.Transaction, string) (ConnectorConfig, error)
|
||||||
Set(cfgs []ConnectorConfig) error
|
Set(cfgs []ConnectorConfig) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type IdentityProvider interface {
|
|
||||||
Identity(email, password string) (*oidc.Identity, error)
|
|
||||||
}
|
|
||||||
|
|
|
@ -18,7 +18,12 @@ func redirectPostError(w http.ResponseWriter, errorURL url.URL, q url.Values) {
|
||||||
w.WriteHeader(http.StatusSeeOther)
|
w.WriteHeader(http.StatusSeeOther)
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleLoginFunc(lf oidc.LoginFunc, tpl *template.Template, idp IdentityProvider, localErrorPath string, errorURL url.URL) http.HandlerFunc {
|
// passwordLoginProvider is a provider which requires a username and password to identify the user.
|
||||||
|
type passwordLoginProvider interface {
|
||||||
|
Identity(email, password string) (*oidc.Identity, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
func handlePasswordLogin(lf oidc.LoginFunc, tpl *template.Template, idp passwordLoginProvider, localErrorPath string, errorURL url.URL) http.HandlerFunc {
|
||||||
handleGET := func(w http.ResponseWriter, r *http.Request, errMsg string) {
|
handleGET := func(w http.ResponseWriter, r *http.Request, errMsg string) {
|
||||||
q := r.URL.Query()
|
q := r.URL.Query()
|
||||||
sessionKey := q.Get("session_key")
|
sessionKey := q.Get("session_key")
|
||||||
|
@ -54,9 +59,7 @@ func handleLoginFunc(lf oidc.LoginFunc, tpl *template.Template, idp IdentityProv
|
||||||
}
|
}
|
||||||
|
|
||||||
ident, err := idp.Identity(userid, password)
|
ident, err := idp.Identity(userid, password)
|
||||||
log.Errorf("IDENTITY: err: %v", err)
|
if err != nil {
|
||||||
|
|
||||||
if ident == nil || err != nil {
|
|
||||||
handleGET(w, r, "invalid login")
|
handleGET(w, r, "invalid login")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,9 @@
|
||||||
package functional
|
package functional
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"html/template"
|
"html/template"
|
||||||
"net"
|
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
|
||||||
"sync"
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
@ -16,45 +13,41 @@ import (
|
||||||
"gopkg.in/ldap.v2"
|
"gopkg.in/ldap.v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
|
||||||
ldapHost string
|
|
||||||
ldapPort uint16
|
|
||||||
ldapBindDN string
|
|
||||||
ldapBindPw string
|
|
||||||
)
|
|
||||||
|
|
||||||
type LDAPServer struct {
|
type LDAPServer struct {
|
||||||
Host string
|
Host string // Address (host:port) of LDAP service.
|
||||||
Port uint16
|
LDAPSHost string // Address (host:port) of LDAPS service (TLS port).
|
||||||
BindDN string
|
BindDN string
|
||||||
BindPw string
|
BindPw string
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
ldapEnvHost = "DEX_TEST_LDAP_HOST"
|
ldapEnvHost = "DEX_TEST_LDAP_HOST"
|
||||||
|
ldapsEnvHost = "DEX_TEST_LDAPS_HOST"
|
||||||
|
|
||||||
ldapEnvBindName = "DEX_TEST_LDAP_BINDNAME"
|
ldapEnvBindName = "DEX_TEST_LDAP_BINDNAME"
|
||||||
ldapEnvBindPass = "DEX_TEST_LDAP_BINDPASS"
|
ldapEnvBindPass = "DEX_TEST_LDAP_BINDPASS"
|
||||||
)
|
)
|
||||||
|
|
||||||
func ldapServer(t *testing.T) LDAPServer {
|
func ldapServer(t *testing.T) LDAPServer {
|
||||||
host := os.Getenv(ldapEnvHost)
|
getenv := func(key string) string {
|
||||||
if host == "" {
|
val := os.Getenv(key)
|
||||||
t.Fatalf("%s not set", ldapEnvHost)
|
if val == "" {
|
||||||
}
|
t.Fatalf("%s not set", key)
|
||||||
var port uint64 = 389
|
|
||||||
if h, p, err := net.SplitHostPort(host); err == nil {
|
|
||||||
port, err = strconv.ParseUint(p, 10, 16)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to parse port: %v", err)
|
|
||||||
}
|
}
|
||||||
host = h
|
t.Logf("%s=%v", key, val)
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
return LDAPServer{
|
||||||
|
Host: getenv(ldapEnvHost),
|
||||||
|
LDAPSHost: getenv(ldapsEnvHost),
|
||||||
|
BindDN: os.Getenv(ldapEnvBindName),
|
||||||
|
BindPw: os.Getenv(ldapEnvBindPass),
|
||||||
}
|
}
|
||||||
return LDAPServer{host, uint16(port), os.Getenv(ldapEnvBindName), os.Getenv(ldapEnvBindPass)}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestLDAPConnect(t *testing.T) {
|
func TestLDAPConnect(t *testing.T) {
|
||||||
server := ldapServer(t)
|
server := ldapServer(t)
|
||||||
l, err := ldap.Dial("tcp", fmt.Sprintf("%s:%d", server.Host, server.Port))
|
l, err := ldap.Dial("tcp", server.Host)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
@ -74,39 +67,35 @@ func TestConnectorLDAPHealthy(t *testing.T) {
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
config: connector.LDAPConnectorConfig{
|
config: connector.LDAPConnectorConfig{
|
||||||
ID: "ldap",
|
ID: "ldap",
|
||||||
ServerHost: server.Host,
|
Host: "localhost:0",
|
||||||
ServerPort: server.Port + 1,
|
|
||||||
},
|
},
|
||||||
wantErr: true,
|
wantErr: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
config: connector.LDAPConnectorConfig{
|
config: connector.LDAPConnectorConfig{
|
||||||
ID: "ldap",
|
ID: "ldap",
|
||||||
ServerHost: server.Host,
|
Host: server.Host,
|
||||||
ServerPort: server.Port,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
config: connector.LDAPConnectorConfig{
|
config: connector.LDAPConnectorConfig{
|
||||||
ID: "ldap",
|
ID: "ldap",
|
||||||
ServerHost: server.Host,
|
Host: server.Host,
|
||||||
ServerPort: server.Port,
|
UseTLS: true,
|
||||||
UseTLS: true,
|
CertFile: "/tmp/ldap.crt",
|
||||||
CertFile: "/tmp/ldap.crt",
|
KeyFile: "/tmp/ldap.key",
|
||||||
KeyFile: "/tmp/ldap.key",
|
CaFile: "/tmp/openldap-ca.pem",
|
||||||
CaFile: "/tmp/openldap-ca.pem",
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
config: connector.LDAPConnectorConfig{
|
config: connector.LDAPConnectorConfig{
|
||||||
ID: "ldap",
|
ID: "ldap",
|
||||||
ServerHost: server.Host,
|
Host: server.LDAPSHost,
|
||||||
ServerPort: server.Port + 247, // 636
|
UseSSL: true,
|
||||||
UseSSL: true,
|
CertFile: "/tmp/ldap.crt",
|
||||||
CertFile: "/tmp/ldap.crt",
|
KeyFile: "/tmp/ldap.key",
|
||||||
KeyFile: "/tmp/ldap.key",
|
CaFile: "/tmp/openldap-ca.pem",
|
||||||
CaFile: "/tmp/openldap-ca.pem",
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -131,8 +120,7 @@ func TestLDAPPoolHighWatermarkAndLockContention(t *testing.T) {
|
||||||
server := ldapServer(t)
|
server := ldapServer(t)
|
||||||
ldapPool := &connector.LDAPPool{
|
ldapPool := &connector.LDAPPool{
|
||||||
MaxIdleConn: 30,
|
MaxIdleConn: 30,
|
||||||
ServerHost: server.Host,
|
Host: server.Host,
|
||||||
ServerPort: server.Port,
|
|
||||||
UseTLS: false,
|
UseTLS: false,
|
||||||
UseSSL: false,
|
UseSSL: false,
|
||||||
}
|
}
|
||||||
|
@ -151,17 +139,16 @@ func TestLDAPPoolHighWatermarkAndLockContention(t *testing.T) {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
return
|
return
|
||||||
default:
|
default:
|
||||||
ldapConn, err := ldapPool.Acquire()
|
err := ldapPool.Do(func(conn *ldap.Conn) error {
|
||||||
if err != nil {
|
s := &ldap.SearchRequest{
|
||||||
t.Errorf("Unable to acquire LDAP Connection: %v", err)
|
Scope: ldap.ScopeBaseObject,
|
||||||
}
|
Filter: "(objectClass=*)",
|
||||||
s := ldap.NewSearchRequest("", ldap.ScopeBaseObject, ldap.NeverDerefAliases, 0, 0, false, "(objectClass=*)", []string{}, nil)
|
}
|
||||||
_, err = ldapConn.Search(s)
|
_, err := conn.Search(s)
|
||||||
|
return err
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("Search request failed. Dead/invalid LDAP connection from pool?: %v", err)
|
t.Errorf("Search request failed. Dead/invalid LDAP connection from pool?: %v", err)
|
||||||
ldapConn.Close()
|
|
||||||
} else {
|
|
||||||
ldapPool.Put(ldapConn)
|
|
||||||
}
|
}
|
||||||
_, _ = ldapPool.CheckConnections()
|
_, _ = ldapPool.CheckConnections()
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,8 +25,7 @@
|
||||||
{
|
{
|
||||||
"type": "ldap",
|
"type": "ldap",
|
||||||
"id": "ldap",
|
"id": "ldap",
|
||||||
"serverHost": "127.0.0.1",
|
"host": "127.0.0.1:389",
|
||||||
"serverPort": 389,
|
|
||||||
"useTLS": true,
|
"useTLS": true,
|
||||||
"useSSL": false,
|
"useSSL": false,
|
||||||
"caFile": "/etc/ssl/certs/example_com_root.crt",
|
"caFile": "/etc/ssl/certs/example_com_root.crt",
|
||||||
|
|
Reference in a new issue