mirror of https://github.com/dexidp/dex.git
4 changed files with 435 additions and 0 deletions
@ -0,0 +1,218 @@
|
||||
// Package ldapcluster implements strategies for authenticating with a cluster of LDAP servers using the LDAP protocol.
|
||||
package ldapcluster |
||||
|
||||
import ( |
||||
"context" |
||||
|
||||
"github.com/dexidp/dex/connector" |
||||
conn_ldap "github.com/dexidp/dex/connector/ldap" |
||||
"github.com/dexidp/dex/pkg/log" |
||||
) |
||||
|
||||
// Config holds the configuration parameters for the LDAP cluster connector. The LDAP
|
||||
// connectors require executing two queries, the first to find the user based on
|
||||
// the username and password given to the connector. The second to use the user
|
||||
// entry to search for groups.
|
||||
// The cluster connector takes multiple LDAP connectors.
|
||||
//
|
||||
// An example config:
|
||||
//connectors:
|
||||
//- type: ldapcluster
|
||||
// name: OpenLDAP
|
||||
// id: ldapcluster
|
||||
// config:
|
||||
// clustermembers:
|
||||
// - host: localhost:399
|
||||
//
|
||||
// # No TLS for this setup.
|
||||
// insecureNoSSL: true
|
||||
//
|
||||
// # This would normally be a read-only user.
|
||||
// bindDN: cn=admin,dc=example,dc=org
|
||||
// bindPW: admin
|
||||
//
|
||||
// usernamePrompt: Email Address
|
||||
//
|
||||
// userSearch:
|
||||
// baseDN: ou=People,dc=example,dc=org
|
||||
// filter: "(objectClass=person)"
|
||||
// username: mail
|
||||
// # "DN" (case sensitive) is a special attribute name. It indicates that
|
||||
// # this value should be taken from the entity's DN not an attribute on
|
||||
// # the entity.
|
||||
// idAttr: DN
|
||||
// emailAttr: mail
|
||||
// nameAttr: cn
|
||||
//
|
||||
// groupSearch:
|
||||
// baseDN: ou=Groups,dc=example,dc=org
|
||||
// filter: "(objectClass=groupOfNames)"
|
||||
//
|
||||
// userMatchers:
|
||||
// # A user is a member of a group when their DN matches
|
||||
// # the value of a "member" attribute on the group entity.
|
||||
// - userAttr: DN
|
||||
// groupAttr: member
|
||||
//
|
||||
// # The group name should be the "cn" value.
|
||||
// nameAttr: cn
|
||||
//
|
||||
// - host: localhost:389
|
||||
//
|
||||
// # No TLS for this setup.
|
||||
// insecureNoSSL: true
|
||||
//
|
||||
// # This would normally be a read-only user.
|
||||
// bindDN: cn=admin,dc=example,dc=org
|
||||
// bindPW: admin
|
||||
//
|
||||
// usernamePrompt: Email Address
|
||||
//
|
||||
// userSearch:
|
||||
// baseDN: ou=People,dc=example,dc=org
|
||||
// filter: "(objectClass=person)"
|
||||
// username: mail
|
||||
// # "DN" (case sensitive) is a special attribute name. It indicates that
|
||||
// # this value should be taken from the entity's DN not an attribute on
|
||||
// # the entity.
|
||||
// idAttr: DN
|
||||
// emailAttr: mail
|
||||
// nameAttr: cn
|
||||
//
|
||||
// groupSearch:
|
||||
// baseDN: ou=Groups,dc=example,dc=org
|
||||
// filter: "(objectClass=groupOfNames)"
|
||||
//
|
||||
// userMatchers:
|
||||
// # A user is a member of a group when their DN matches
|
||||
// # the value of a "member" attribute on the group entity.
|
||||
// - userAttr: DN
|
||||
// groupAttr: member
|
||||
//
|
||||
// # The group name should be the "cn" value.
|
||||
// nameAttr: cn
|
||||
//
|
||||
|
||||
type Config struct { |
||||
ClusterMembers []conn_ldap.Config |
||||
} |
||||
|
||||
// Open returns an authentication strategy using LDAP.
|
||||
func (c *Config) Open(id string, logger log.Logger) (connector.Connector, error) { |
||||
conn, err := c.OpenConnector(logger) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
return connector.Connector(conn), nil |
||||
} |
||||
|
||||
// OpenConnector is the same as Open but returns a type with all implemented connector interfaces.
|
||||
func (c *Config) OpenConnector(logger log.Logger) (interface { |
||||
connector.Connector |
||||
connector.PasswordConnector |
||||
connector.RefreshConnector |
||||
}, error) { |
||||
return c.openConnector(logger) |
||||
} |
||||
|
||||
func (c *Config) openConnector(logger log.Logger) (*ldapClusterConnector, error) { |
||||
var lcc ldapClusterConnector |
||||
// Initialize each of the connector members.
|
||||
for _, v := range c.ClusterMembers { |
||||
lc, e := v.OpenConnector(logger) |
||||
if e != nil { |
||||
return nil, e |
||||
} |
||||
lcc.MemberConnectors = append(lcc.MemberConnectors, lc) |
||||
} |
||||
|
||||
lcc.activeMemberIdx = 0 |
||||
lcc.logger = logger |
||||
|
||||
return &lcc, nil |
||||
} |
||||
|
||||
type ConnectorIf interface { |
||||
connector.Connector |
||||
connector.PasswordConnector |
||||
connector.RefreshConnector |
||||
} |
||||
|
||||
type ldapClusterConnector struct { |
||||
MemberConnectors [](ConnectorIf) |
||||
activeMemberIdx int |
||||
logger log.Logger |
||||
} |
||||
|
||||
func (c *ldapClusterConnector) Login(ctx context.Context, s connector.Scopes, username, password string) (ident connector.Identity, validPass bool, err error) { |
||||
// make this check to avoid unauthenticated bind to the LDAP server.
|
||||
if password == "" { |
||||
return connector.Identity{}, false, nil |
||||
} |
||||
|
||||
// Check the active connector first.
|
||||
// If the active connector index is -1, we will start
|
||||
// with first connector.
|
||||
if c.activeMemberIdx == -1 { |
||||
c.activeMemberIdx = 0 |
||||
} |
||||
lc := c.MemberConnectors[c.activeMemberIdx] |
||||
i, b, e := lc.Login(ctx, s, username, password) |
||||
if e != nil { |
||||
c.logger.Infof("Failed to connect to server idx: %d", c.activeMemberIdx) |
||||
// Current active server has returned error.
|
||||
// Try the other servers in round robin manner.
|
||||
// If the error returned by a server is nil,
|
||||
// then make that server as
|
||||
// the current active server.
|
||||
for k, v := range c.MemberConnectors { |
||||
if k == c.activeMemberIdx { |
||||
// we just tried it.
|
||||
// hence skip.
|
||||
continue |
||||
} |
||||
i, b, e = v.Login(ctx, s, username, password) |
||||
if e == nil { |
||||
c.logger.Infof("setting active index as: %d", k) |
||||
c.activeMemberIdx = k |
||||
return i, b, e |
||||
} |
||||
} |
||||
} |
||||
return i, b, e |
||||
} |
||||
|
||||
func (c *ldapClusterConnector) Refresh(ctx context.Context, s connector.Scopes, ident connector.Identity) (connector.Identity, error) { |
||||
lc := c.MemberConnectors[c.activeMemberIdx] |
||||
i, e := lc.Refresh(ctx, s, ident) |
||||
if e != nil { |
||||
c.logger.Infof("Failed to connect to active index: %d", c.activeMemberIdx) |
||||
// current active server has returned error.
|
||||
// Try the other servers in round robin manner.
|
||||
// If the error returned by a server is nil,
|
||||
// then make that server as
|
||||
// the current active server.
|
||||
for k, v := range c.MemberConnectors { |
||||
if k == c.activeMemberIdx { |
||||
// we just tried it.
|
||||
// hence skip.
|
||||
continue |
||||
} |
||||
c.logger.Infof("Trying index: %d", k) |
||||
i, e = v.Refresh(ctx, s, ident) |
||||
if e == nil { |
||||
c.logger.Infof("setting active index as: %d", k) |
||||
c.activeMemberIdx = k |
||||
return i, nil |
||||
} |
||||
c.logger.Errorf("Failed to connect to index: %d", k) |
||||
} |
||||
} |
||||
|
||||
return i, e |
||||
} |
||||
|
||||
func (c *ldapClusterConnector) Prompt() string { |
||||
lc := c.MemberConnectors[c.activeMemberIdx] |
||||
return lc.Prompt() |
||||
} |
||||
@ -0,0 +1,125 @@
|
||||
package ldapcluster |
||||
|
||||
import ( |
||||
"context" |
||||
"errors" |
||||
"testing" |
||||
|
||||
log "github.com/sirupsen/logrus" |
||||
"github.com/stretchr/testify/assert" |
||||
|
||||
"github.com/dexidp/dex/connector" |
||||
) |
||||
|
||||
type MockConn struct { |
||||
status bool |
||||
} |
||||
|
||||
func (c MockConn) Login(ctx context.Context, s connector.Scopes, username, password string) (ident connector.Identity, validPass bool, err error) { |
||||
if c.status { |
||||
return connector.Identity{}, false, nil |
||||
} |
||||
return connector.Identity{}, false, errors.New("failed") |
||||
} |
||||
|
||||
func (c MockConn) Refresh(ctx context.Context, s connector.Scopes, ident connector.Identity) (connector.Identity, error) { |
||||
if c.status { |
||||
return connector.Identity{}, nil |
||||
} |
||||
return connector.Identity{}, errors.New("failed") |
||||
} |
||||
|
||||
func (c MockConn) Prompt() string { |
||||
return "name:" |
||||
} |
||||
|
||||
func TestLoginSingle(t *testing.T) { |
||||
var ctx context.Context |
||||
var s connector.Scopes |
||||
|
||||
var c1 MockConn |
||||
c1.status = true |
||||
|
||||
var lcc ldapClusterConnector |
||||
lcc.MemberConnectors = append(lcc.MemberConnectors, c1) |
||||
lcc.activeMemberIdx = 0 |
||||
|
||||
var logger *log.Logger = log.New() |
||||
lcc.logger = logger |
||||
_, _, e := lcc.Login(ctx, s, "testuser", "password") |
||||
assert.Equal(t, e, nil) |
||||
} |
||||
|
||||
func TestLoginMultiple(t *testing.T) { |
||||
var ctx context.Context |
||||
var s connector.Scopes |
||||
|
||||
var c1 MockConn |
||||
c1.status = false |
||||
|
||||
var c2 MockConn |
||||
c2.status = true |
||||
|
||||
var lcc ldapClusterConnector |
||||
lcc.MemberConnectors = append(lcc.MemberConnectors, c1) |
||||
lcc.activeMemberIdx = 0 |
||||
lcc.MemberConnectors = append(lcc.MemberConnectors, c2) |
||||
|
||||
var logger *log.Logger = log.New() |
||||
lcc.logger = logger |
||||
_, _, e := lcc.Login(ctx, s, "testuser", "password") |
||||
assert.Equal(t, e, nil) |
||||
assert.Equal(t, lcc.activeMemberIdx, 1) |
||||
} |
||||
|
||||
func TestLoginMultiple2(t *testing.T) { |
||||
var ctx context.Context |
||||
var s connector.Scopes |
||||
|
||||
var c1 MockConn |
||||
c1.status = false |
||||
|
||||
var c2 MockConn |
||||
c2.status = false |
||||
|
||||
var c3 MockConn |
||||
c3.status = true |
||||
|
||||
var lcc ldapClusterConnector |
||||
lcc.MemberConnectors = append(lcc.MemberConnectors, c1) |
||||
lcc.activeMemberIdx = 0 |
||||
lcc.MemberConnectors = append(lcc.MemberConnectors, c2) |
||||
lcc.MemberConnectors = append(lcc.MemberConnectors, c3) |
||||
|
||||
var logger *log.Logger = log.New() |
||||
lcc.logger = logger |
||||
_, _, e := lcc.Login(ctx, s, "testuser", "password") |
||||
assert.Equal(t, e, nil) |
||||
assert.Equal(t, lcc.activeMemberIdx, 2) |
||||
} |
||||
|
||||
func TestRefreshMultiple(t *testing.T) { |
||||
var ctx context.Context |
||||
var s connector.Scopes |
||||
|
||||
var c1 MockConn |
||||
c1.status = true |
||||
|
||||
var c2 MockConn |
||||
c2.status = false |
||||
|
||||
var c3 MockConn |
||||
c3.status = false |
||||
|
||||
var lcc ldapClusterConnector |
||||
lcc.MemberConnectors = append(lcc.MemberConnectors, c1) |
||||
lcc.activeMemberIdx = 1 |
||||
lcc.MemberConnectors = append(lcc.MemberConnectors, c2) |
||||
lcc.MemberConnectors = append(lcc.MemberConnectors, c3) |
||||
|
||||
var logger *log.Logger = log.New() |
||||
lcc.logger = logger |
||||
_, e := lcc.Refresh(ctx, s, connector.Identity{}) |
||||
assert.Equal(t, e, nil) |
||||
assert.Equal(t, lcc.activeMemberIdx, 0) |
||||
} |
||||
@ -0,0 +1,90 @@
|
||||
issuer: http://127.0.0.1:5556/dex |
||||
storage: |
||||
type: sqlite3 |
||||
config: |
||||
file: examples/dex.db |
||||
web: |
||||
http: 0.0.0.0:5556 |
||||
|
||||
connectors: |
||||
- type: ldapcluster |
||||
name: OpenLDAP |
||||
id: ldapcluster |
||||
config: |
||||
clustermembers: |
||||
- host: localhost:399 |
||||
|
||||
# No TLS for this setup. |
||||
insecureNoSSL: true |
||||
|
||||
# This would normally be a read-only user. |
||||
bindDN: cn=admin,dc=example,dc=org |
||||
bindPW: admin |
||||
|
||||
usernamePrompt: Email Address |
||||
|
||||
userSearch: |
||||
baseDN: ou=People,dc=example,dc=org |
||||
filter: "(objectClass=person)" |
||||
username: mail |
||||
# "DN" (case sensitive) is a special attribute name. It indicates that |
||||
# this value should be taken from the entity's DN not an attribute on |
||||
# the entity. |
||||
idAttr: DN |
||||
emailAttr: mail |
||||
nameAttr: cn |
||||
|
||||
groupSearch: |
||||
baseDN: ou=Groups,dc=example,dc=org |
||||
filter: "(objectClass=groupOfNames)" |
||||
|
||||
userMatchers: |
||||
# A user is a member of a group when their DN matches |
||||
# the value of a "member" attribute on the group entity. |
||||
- userAttr: DN |
||||
groupAttr: member |
||||
|
||||
# The group name should be the "cn" value. |
||||
nameAttr: cn |
||||
|
||||
- host: localhost:389 |
||||
|
||||
# No TLS for this setup. |
||||
insecureNoSSL: true |
||||
|
||||
# This would normally be a read-only user. |
||||
bindDN: cn=admin,dc=example,dc=org |
||||
bindPW: admin |
||||
|
||||
usernamePrompt: Email Address |
||||
|
||||
userSearch: |
||||
baseDN: ou=People,dc=example,dc=org |
||||
filter: "(objectClass=person)" |
||||
username: mail |
||||
# "DN" (case sensitive) is a special attribute name. It indicates that |
||||
# this value should be taken from the entity's DN not an attribute on |
||||
# the entity. |
||||
idAttr: DN |
||||
emailAttr: mail |
||||
nameAttr: cn |
||||
|
||||
groupSearch: |
||||
baseDN: ou=Groups,dc=example,dc=org |
||||
filter: "(objectClass=groupOfNames)" |
||||
|
||||
userMatchers: |
||||
# A user is a member of a group when their DN matches |
||||
# the value of a "member" attribute on the group entity. |
||||
- userAttr: DN |
||||
groupAttr: member |
||||
|
||||
# The group name should be the "cn" value. |
||||
nameAttr: cn |
||||
|
||||
staticClients: |
||||
- id: example-app |
||||
redirectURIs: |
||||
- 'http://127.0.0.1:5555/callback' |
||||
name: 'Example App' |
||||
secret: ZXhhbXBsZS1hcHAtc2VjcmV0 |
||||
Loading…
Reference in new issue