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