connector: add a group interface and an LDAP implementation

This commit is contained in:
Eric Chiang 2016-07-15 16:00:21 -07:00
parent af6aade6d0
commit 731dadb29d
2 changed files with 70 additions and 27 deletions

View file

@ -107,11 +107,12 @@ type LDAPConnector struct {
nameAttribute string nameAttribute string
emailAttribute string emailAttribute string
searchBeforeAuth bool searchBeforeAuth bool
searchFilter string searchFilter string
searchScope int searchScope int
searchBindDN string searchBindDN string
searchBindPw string searchBindPw string
searchGroupFilter string
bindTemplate string bindTemplate string
@ -203,19 +204,20 @@ func (cfg *LDAPConnectorConfig) Connector(ns url.URL, lf oidc.LoginFunc, tpls *t
} }
idpc := &LDAPConnector{ idpc := &LDAPConnector{
id: cfg.ID, id: cfg.ID,
namespace: ns, namespace: ns,
loginFunc: lf, loginFunc: lf,
loginTpl: tpl, loginTpl: tpl,
baseDN: cfg.BaseDN, baseDN: cfg.BaseDN,
nameAttribute: cfg.NameAttribute, nameAttribute: cfg.NameAttribute,
emailAttribute: cfg.EmailAttribute, emailAttribute: cfg.EmailAttribute,
searchBeforeAuth: cfg.SearchBeforeAuth, searchBeforeAuth: cfg.SearchBeforeAuth,
searchFilter: cfg.SearchFilter, searchFilter: cfg.SearchFilter,
searchScope: searchScope, searchGroupFilter: cfg.SearchGroupFilter,
searchBindDN: cfg.SearchBindDN, searchScope: searchScope,
searchBindPw: cfg.SearchBindPw, searchBindDN: cfg.SearchBindDN,
bindTemplate: cfg.BindTemplate, searchBindPw: cfg.SearchBindPw,
bindTemplate: cfg.BindTemplate,
ldapPool: &LDAPPool{ ldapPool: &LDAPPool{
MaxIdleConn: cfg.MaxIdleConn, MaxIdleConn: cfg.MaxIdleConn,
PoolCheckTimer: defaultPoolCheckTimer, PoolCheckTimer: defaultPoolCheckTimer,
@ -433,12 +435,47 @@ func invalidBindCredentials(err error) bool {
func (c *LDAPConnector) formatDN(template, username string) string { func (c *LDAPConnector) formatDN(template, username string) string {
result := template result := template
result = strings.Replace(result, "%u", username, -1) result = strings.Replace(result, "%u", ldap.EscapeFilter(username), -1)
result = strings.Replace(result, "%b", c.baseDN, -1) result = strings.Replace(result, "%b", c.baseDN, -1)
return result return result
} }
func (c *LDAPConnector) Groups(fullUserID string) ([]string, error) {
if !c.searchBeforeAuth {
return nil, fmt.Errorf("cannot search without service account")
}
if c.searchGroupFilter == "" {
return nil, fmt.Errorf("no group filter specified")
}
var groups []string
err := c.ldapPool.Do(func(conn *ldap.Conn) error {
if err := conn.Bind(c.searchBindDN, c.searchBindPw); err != nil {
if !invalidBindCredentials(err) {
log.Errorf("failed to connect to LDAP for search bind: %v", err)
}
return fmt.Errorf("failed to bind: %v", err)
}
req := &ldap.SearchRequest{
BaseDN: c.baseDN,
Scope: c.searchScope,
Filter: c.formatDN(c.searchGroupFilter, fullUserID),
}
resp, err := conn.Search(req)
if err != nil {
return fmt.Errorf("search failed: %v", err)
}
groups = make([]string, len(resp.Entries))
for i, entry := range resp.Entries {
groups[i] = entry.DN
}
return nil
})
return groups, err
}
func (c *LDAPConnector) Identity(username, password string) (*oidc.Identity, error) { func (c *LDAPConnector) Identity(username, password string) (*oidc.Identity, error) {
var ( var (
identity *oidc.Identity identity *oidc.Identity
@ -447,8 +484,10 @@ func (c *LDAPConnector) Identity(username, password string) (*oidc.Identity, err
if c.searchBeforeAuth { if c.searchBeforeAuth {
err = c.ldapPool.Do(func(conn *ldap.Conn) error { err = c.ldapPool.Do(func(conn *ldap.Conn) error {
if err := conn.Bind(c.searchBindDN, c.searchBindPw); err != nil { if err := conn.Bind(c.searchBindDN, c.searchBindPw); err != nil {
// Don't wrap error as it may be a specific LDAP error. if !invalidBindCredentials(err) {
return err log.Errorf("failed to connect to LDAP for search bind: %v", err)
}
return fmt.Errorf("failed to bind: %v", err)
} }
filter := c.formatDN(c.searchFilter, username) filter := c.formatDN(c.searchFilter, username)
@ -491,8 +530,10 @@ func (c *LDAPConnector) Identity(username, password string) (*oidc.Identity, err
err = c.ldapPool.Do(func(conn *ldap.Conn) error { err = c.ldapPool.Do(func(conn *ldap.Conn) error {
userBindDN := c.formatDN(c.bindTemplate, username) userBindDN := c.formatDN(c.bindTemplate, username)
if err := conn.Bind(userBindDN, password); err != nil { if err := conn.Bind(userBindDN, password); err != nil {
// Don't wrap error as it may be a specific LDAP error. if !invalidBindCredentials(err) {
return err log.Errorf("failed to connect to LDAP for search bind: %v", err)
}
return fmt.Errorf("failed to bind: %v", err)
} }
req := &ldap.SearchRequest{ req := &ldap.SearchRequest{
@ -522,11 +563,7 @@ func (c *LDAPConnector) Identity(username, password string) (*oidc.Identity, err
return nil return nil
}) })
} }
if err != nil { if err != nil {
if !invalidBindCredentials(err) {
log.Errorf("failed to connect to LDAP for search bind: %v", err)
}
return nil, err return nil, err
} }
return identity, nil return identity, nil

View file

@ -60,6 +60,12 @@ type ConnectorConfig interface {
Connector(ns url.URL, loginFunc oidc.LoginFunc, tpls *template.Template) (Connector, error) Connector(ns url.URL, loginFunc oidc.LoginFunc, tpls *template.Template) (Connector, error)
} }
// GroupsConnector is a strategy for mapping a user to a set of groups. This is optionally
// implemented by some connectors.
type GroupsConnector interface {
Groups(fullUserID string) ([]string, error)
}
type ConnectorConfigRepo interface { type ConnectorConfigRepo interface {
All() ([]ConnectorConfig, error) All() ([]ConnectorConfig, error)
GetConnectorByID(repo.Transaction, string) (ConnectorConfig, error) GetConnectorByID(repo.Transaction, string) (ConnectorConfig, error)