connector/ldap: add multiple user to group mapping

Add an ability to fetch user's membership from
  groups of a different type by specifying multiple
  group attribute to user attribute value matchers
  in the Dex config:

    userMatchers:
    - userAttr: uid
      groupAttr: memberUid
    - userAttr: DN
      groupAttr: member

  In other words the user's groups can be fetched now from
  ldap structure similar to the following:

    dn: cn=john,ou=People,dc=example,dc=org
    objectClass: person
    objectClass: inetOrgPerson
    sn: doe
    cn: john
    uid: johndoe
    mail: johndoe@example.com
    userpassword: bar

    dn: cn=qa,ou=Groups,ou=Portland,dc=example,dc=org
    objectClass: groupOfNames
    cn: qa
    member: cn=john,ou=People,dc=example,dc=org

    dn: cn=logger,ou=UnixGroups,ou=Portland,dc=example,dc=org
    objectClass: posixGroup
    gidNumber: 1000
    cn: logger
    memberUid: johndoe

Signed-off-by: Vitaliy Dmitriev <vi7alya@gmail.com>
This commit is contained in:
Vitaliy Dmitriev
2020-01-03 10:40:08 +01:00
parent 6318c105ec
commit f2e7823db9
5 changed files with 247 additions and 65 deletions

View File

@@ -41,16 +41,26 @@ import (
// nameAttr: name
// preferredUsernameAttr: uid
// groupSearch:
// # Would translate to the query "(&(objectClass=group)(member=<user uid>))"
// # Would translate to the separate query per user matcher pair and aggregate results into a single group list:
// # "(&(|(objectClass=posixGroup)(objectClass=groupOfNames))(memberUid=<user uid>))"
// # "(&(|(objectClass=posixGroup)(objectClass=groupOfNames))(member=<user DN>))"
// baseDN: cn=groups,dc=example,dc=com
// filter: "(objectClass=group)"
// userAttr: uid
// # Use if full DN is needed and not available as any other attribute
// # Will only work if "DN" attribute does not exist in the record
// # userAttr: DN
// groupAttr: member
// filter: "(|(objectClass=posixGroup)(objectClass=groupOfNames))"
// userMatchers:
// - userAttr: uid
// groupAttr: memberUid
// # Use if full DN is needed and not available as any other attribute
// # Will only work if "DN" attribute does not exist in the record:
// - userAttr: DN
// groupAttr: member
// nameAttr: name
//
type UserMatcher struct {
UserAttr string `json:"userAttr"`
GroupAttr string `json:"groupAttr"`
}
type Config struct {
// The host and optional port of the LDAP server. If port isn't supplied, it will be
// guessed based on the TLS configuration. 389 or 636.
@@ -124,16 +134,16 @@ type Config struct {
Scope string `json:"scope"` // Defaults to "sub"
// These two fields are use to match a user to a group.
// Array of the field pairs used to match a user to a group.
// See the "UserMatcher" struct for the exact field names
//
// It adds an additional requirement to the filter that an attribute in the group
// Each pair adds an additional requirement to the filter that an attribute in the group
// match the user's attribute value. For example that the "members" attribute of
// a group matches the "uid" of the user. The exact filter being added is:
//
// (<groupAttr>=<userAttr value>)
// (userMatchers[n].<groupAttr>=userMatchers[n].<userAttr value>)
//
UserAttr string `json:"userAttr"`
GroupAttr string `json:"groupAttr"`
UserMatchers []UserMatcher `json:"userMatchers"`
// The attribute of the group that represents its name.
NameAttr string `json:"nameAttr"`
@@ -378,11 +388,14 @@ func (c *ldapConnector) userEntry(conn *ldap.Conn, username string) (user ldap.E
Attributes: []string{
c.UserSearch.IDAttr,
c.UserSearch.EmailAttr,
c.GroupSearch.UserAttr,
// TODO(ericchiang): what if this contains duplicate values?
},
}
for _, matcher := range c.GroupSearch.UserMatchers {
req.Attributes = append(req.Attributes, matcher.UserAttr)
}
if c.UserSearch.NameAttr != "" {
req.Attributes = append(req.Attributes, c.UserSearch.NameAttr)
}
@@ -536,36 +549,38 @@ func (c *ldapConnector) groups(ctx context.Context, user ldap.Entry) ([]string,
}
var groups []*ldap.Entry
for _, attr := range getAttrs(user, c.GroupSearch.UserAttr) {
filter := fmt.Sprintf("(%s=%s)", c.GroupSearch.GroupAttr, ldap.EscapeFilter(attr))
if c.GroupSearch.Filter != "" {
filter = fmt.Sprintf("(&%s%s)", c.GroupSearch.Filter, filter)
}
req := &ldap.SearchRequest{
BaseDN: c.GroupSearch.BaseDN,
Filter: filter,
Scope: c.groupSearchScope,
Attributes: []string{c.GroupSearch.NameAttr},
}
gotGroups := false
if err := c.do(ctx, func(conn *ldap.Conn) error {
c.logger.Infof("performing ldap search %s %s %s",
req.BaseDN, scopeString(req.Scope), req.Filter)
resp, err := conn.Search(req)
if err != nil {
return fmt.Errorf("ldap: search failed: %v", err)
for _, matcher := range c.GroupSearch.UserMatchers {
for _, attr := range getAttrs(user, matcher.UserAttr) {
filter := fmt.Sprintf("(%s=%s)", matcher.GroupAttr, ldap.EscapeFilter(attr))
if c.GroupSearch.Filter != "" {
filter = fmt.Sprintf("(&%s%s)", c.GroupSearch.Filter, filter)
}
req := &ldap.SearchRequest{
BaseDN: c.GroupSearch.BaseDN,
Filter: filter,
Scope: c.groupSearchScope,
Attributes: []string{c.GroupSearch.NameAttr},
}
gotGroups := false
if err := c.do(ctx, func(conn *ldap.Conn) error {
c.logger.Infof("performing ldap search %s %s %s",
req.BaseDN, scopeString(req.Scope), req.Filter)
resp, err := conn.Search(req)
if err != nil {
return fmt.Errorf("ldap: search failed: %v", err)
}
gotGroups = len(resp.Entries) != 0
groups = append(groups, resp.Entries...)
return nil
}); err != nil {
return nil, err
}
if !gotGroups {
// TODO(ericchiang): Is this going to spam the logs?
c.logger.Errorf("ldap: groups search with filter %q returned no groups", filter)
}
gotGroups = len(resp.Entries) != 0
groups = append(groups, resp.Entries...)
return nil
}); err != nil {
return nil, err
}
if !gotGroups {
// TODO(ericchiang): Is this going to spam the logs?
c.logger.Errorf("ldap: groups search with filter %q returned no groups", filter)
}
}