Browse Source

Merge 2e354b2b9b into 13f012fb81

pull/1904/merge
phiremande 4 days ago committed by GitHub
parent
commit
ec15b3e4ad
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 218
      connector/ldapcluster/ldapcluster.go
  2. 125
      connector/ldapcluster/ldapcluster_test.go
  3. 90
      examples/ldapcluster/config-ldapcluster.yaml
  4. 2
      server/server.go

218
connector/ldapcluster/ldapcluster.go

@ -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
connector/ldapcluster/ldapcluster_test.go

@ -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)
}

90
examples/ldapcluster/config-ldapcluster.yaml

@ -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

2
server/server.go

@ -38,6 +38,7 @@ import (
"github.com/dexidp/dex/connector/google" "github.com/dexidp/dex/connector/google"
"github.com/dexidp/dex/connector/keystone" "github.com/dexidp/dex/connector/keystone"
"github.com/dexidp/dex/connector/ldap" "github.com/dexidp/dex/connector/ldap"
"github.com/dexidp/dex/connector/ldapcluster"
"github.com/dexidp/dex/connector/linkedin" "github.com/dexidp/dex/connector/linkedin"
"github.com/dexidp/dex/connector/microsoft" "github.com/dexidp/dex/connector/microsoft"
"github.com/dexidp/dex/connector/mock" "github.com/dexidp/dex/connector/mock"
@ -688,6 +689,7 @@ var ConnectorsConfig = map[string]func() ConnectorConfig{
"mockCallback": func() ConnectorConfig { return new(mock.CallbackConfig) }, "mockCallback": func() ConnectorConfig { return new(mock.CallbackConfig) },
"mockPassword": func() ConnectorConfig { return new(mock.PasswordConfig) }, "mockPassword": func() ConnectorConfig { return new(mock.PasswordConfig) },
"ldap": func() ConnectorConfig { return new(ldap.Config) }, "ldap": func() ConnectorConfig { return new(ldap.Config) },
"ldapcluster": func() ConnectorConfig { return new(ldapcluster.Config) },
"gitea": func() ConnectorConfig { return new(gitea.Config) }, "gitea": func() ConnectorConfig { return new(gitea.Config) },
"github": func() ConnectorConfig { return new(github.Config) }, "github": func() ConnectorConfig { return new(github.Config) },
"gitlab": func() ConnectorConfig { return new(gitlab.Config) }, "gitlab": func() ConnectorConfig { return new(gitlab.Config) },

Loading…
Cancel
Save