Skip to content

Commit

Permalink
ldap cluster connector
Browse files Browse the repository at this point in the history
Signed-off-by: Pradeep Hiremande <[email protected]>
  • Loading branch information
phiremande committed Dec 30, 2020
1 parent 31353d2 commit 2e354b2
Show file tree
Hide file tree
Showing 4 changed files with 435 additions and 0 deletions.
218 changes: 218 additions & 0 deletions connector/ldapcluster/ldapcluster.go
Original file line number Diff line number Diff line change
@@ -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()
}
125 changes: 125 additions & 0 deletions connector/ldapcluster/ldapcluster_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
Loading

0 comments on commit 2e354b2

Please sign in to comment.