-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Signed-off-by: Pradeep Hiremande <[email protected]>
- Loading branch information
1 parent
31353d2
commit 2e354b2
Showing
4 changed files
with
435 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
Oops, something went wrong.