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:
parent
6318c105ec
commit
f2e7823db9
5 changed files with 247 additions and 65 deletions
|
@ -123,10 +123,11 @@ connectors:
|
||||||
# Optional filter to apply when searching the directory.
|
# Optional filter to apply when searching the directory.
|
||||||
filter: "(objectClass=group)"
|
filter: "(objectClass=group)"
|
||||||
|
|
||||||
# Following two fields are used to match a user to a group. It adds an additional
|
# Following list contains field pairs that are used to match a user to a group. It adds an additional
|
||||||
# requirement to the filter that an attribute in the group must match the user's
|
# requirement to the filter that an attribute in the group must match the user's
|
||||||
# attribute value.
|
# attribute value.
|
||||||
userAttr: uid
|
userMatchers:
|
||||||
|
- userAttr: uid
|
||||||
groupAttr: member
|
groupAttr: member
|
||||||
|
|
||||||
# Represents group name.
|
# Represents group name.
|
||||||
|
@ -215,7 +216,8 @@ groupSearch:
|
||||||
|
|
||||||
# The group search needs to match the "uid" attribute on
|
# The group search needs to match the "uid" attribute on
|
||||||
# the user with the "memberUid" attribute on the group.
|
# the user with the "memberUid" attribute on the group.
|
||||||
userAttr: uid
|
userMatchers:
|
||||||
|
- userAttr: uid
|
||||||
groupAttr: memberUid
|
groupAttr: memberUid
|
||||||
|
|
||||||
# Unique name of the group.
|
# Unique name of the group.
|
||||||
|
@ -242,7 +244,26 @@ groupSearch:
|
||||||
# Optional filter to apply when searching the directory.
|
# Optional filter to apply when searching the directory.
|
||||||
filter: "(objectClass=group)"
|
filter: "(objectClass=group)"
|
||||||
|
|
||||||
userAttr: DN # Use "DN" here not "uid"
|
userMatchers:
|
||||||
|
- userAttr: DN # Use "DN" here not "uid"
|
||||||
|
groupAttr: member
|
||||||
|
|
||||||
|
nameAttr: name
|
||||||
|
```
|
||||||
|
|
||||||
|
There are cases when different types (objectClass) of groups use different attributes to keep a list of members. Below is an example of group query for such case:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
groupSearch:
|
||||||
|
baseDN: cn=groups,cn=compat,dc=example,dc=com
|
||||||
|
# Optional filter to search for different group types
|
||||||
|
filter: "(|(objectClass=posixGroup)(objectClass=group))"
|
||||||
|
|
||||||
|
# Use multiple user matchers so Dex will know which attribute names should be used to search for group members
|
||||||
|
userMatchers:
|
||||||
|
- userAttr: uid
|
||||||
|
groupAttr: memberUid
|
||||||
|
- userAttr: DN
|
||||||
groupAttr: member
|
groupAttr: member
|
||||||
|
|
||||||
nameAttr: name
|
nameAttr: name
|
||||||
|
@ -275,7 +296,8 @@ connectors:
|
||||||
# Would translate to the query "(&(objectClass=group)(member=<user uid>))".
|
# Would translate to the query "(&(objectClass=group)(member=<user uid>))".
|
||||||
baseDN: cn=groups,dc=freeipa,dc=example,dc=com
|
baseDN: cn=groups,dc=freeipa,dc=example,dc=com
|
||||||
filter: "(objectClass=group)"
|
filter: "(objectClass=group)"
|
||||||
userAttr: uid
|
userMatchers:
|
||||||
|
- userAttr: uid
|
||||||
groupAttr: member
|
groupAttr: member
|
||||||
nameAttr: name
|
nameAttr: name
|
||||||
```
|
```
|
||||||
|
@ -315,7 +337,8 @@ connectors:
|
||||||
groupSearch:
|
groupSearch:
|
||||||
baseDN: cn=Users,dc=example,dc=com
|
baseDN: cn=Users,dc=example,dc=com
|
||||||
filter: "(objectClass=group)"
|
filter: "(objectClass=group)"
|
||||||
userAttr: DN
|
userMatchers:
|
||||||
|
- userAttr: DN
|
||||||
groupAttr: member
|
groupAttr: member
|
||||||
nameAttr: cn
|
nameAttr: cn
|
||||||
```
|
```
|
||||||
|
|
|
@ -41,16 +41,26 @@ import (
|
||||||
// nameAttr: name
|
// nameAttr: name
|
||||||
// preferredUsernameAttr: uid
|
// preferredUsernameAttr: uid
|
||||||
// groupSearch:
|
// 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
|
// baseDN: cn=groups,dc=example,dc=com
|
||||||
// filter: "(objectClass=group)"
|
// filter: "(|(objectClass=posixGroup)(objectClass=groupOfNames))"
|
||||||
// userAttr: uid
|
// userMatchers:
|
||||||
|
// - userAttr: uid
|
||||||
|
// groupAttr: memberUid
|
||||||
// # Use if full DN is needed and not available as any other attribute
|
// # 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
|
// # Will only work if "DN" attribute does not exist in the record:
|
||||||
// # userAttr: DN
|
// - userAttr: DN
|
||||||
// groupAttr: member
|
// groupAttr: member
|
||||||
// nameAttr: name
|
// nameAttr: name
|
||||||
//
|
//
|
||||||
|
|
||||||
|
type UserMatcher struct {
|
||||||
|
UserAttr string `json:"userAttr"`
|
||||||
|
GroupAttr string `json:"groupAttr"`
|
||||||
|
}
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
// The host and optional port of the LDAP server. If port isn't supplied, it will be
|
// 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.
|
// guessed based on the TLS configuration. 389 or 636.
|
||||||
|
@ -124,16 +134,16 @@ type Config struct {
|
||||||
|
|
||||||
Scope string `json:"scope"` // Defaults to "sub"
|
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
|
// 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:
|
// 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"`
|
UserMatchers []UserMatcher `json:"userMatchers"`
|
||||||
GroupAttr string `json:"groupAttr"`
|
|
||||||
|
|
||||||
// The attribute of the group that represents its name.
|
// The attribute of the group that represents its name.
|
||||||
NameAttr string `json:"nameAttr"`
|
NameAttr string `json:"nameAttr"`
|
||||||
|
@ -378,11 +388,14 @@ func (c *ldapConnector) userEntry(conn *ldap.Conn, username string) (user ldap.E
|
||||||
Attributes: []string{
|
Attributes: []string{
|
||||||
c.UserSearch.IDAttr,
|
c.UserSearch.IDAttr,
|
||||||
c.UserSearch.EmailAttr,
|
c.UserSearch.EmailAttr,
|
||||||
c.GroupSearch.UserAttr,
|
|
||||||
// TODO(ericchiang): what if this contains duplicate values?
|
// 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 != "" {
|
if c.UserSearch.NameAttr != "" {
|
||||||
req.Attributes = append(req.Attributes, c.UserSearch.NameAttr)
|
req.Attributes = append(req.Attributes, c.UserSearch.NameAttr)
|
||||||
}
|
}
|
||||||
|
@ -536,8 +549,9 @@ func (c *ldapConnector) groups(ctx context.Context, user ldap.Entry) ([]string,
|
||||||
}
|
}
|
||||||
|
|
||||||
var groups []*ldap.Entry
|
var groups []*ldap.Entry
|
||||||
for _, attr := range getAttrs(user, c.GroupSearch.UserAttr) {
|
for _, matcher := range c.GroupSearch.UserMatchers {
|
||||||
filter := fmt.Sprintf("(%s=%s)", c.GroupSearch.GroupAttr, ldap.EscapeFilter(attr))
|
for _, attr := range getAttrs(user, matcher.UserAttr) {
|
||||||
|
filter := fmt.Sprintf("(%s=%s)", matcher.GroupAttr, ldap.EscapeFilter(attr))
|
||||||
if c.GroupSearch.Filter != "" {
|
if c.GroupSearch.Filter != "" {
|
||||||
filter = fmt.Sprintf("(&%s%s)", c.GroupSearch.Filter, filter)
|
filter = fmt.Sprintf("(&%s%s)", c.GroupSearch.Filter, filter)
|
||||||
}
|
}
|
||||||
|
@ -568,6 +582,7 @@ func (c *ldapConnector) groups(ctx context.Context, user ldap.Entry) ([]string,
|
||||||
c.logger.Errorf("ldap: groups search with filter %q returned no groups", filter)
|
c.logger.Errorf("ldap: groups search with filter %q returned no groups", filter)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var groupNames []string
|
var groupNames []string
|
||||||
for _, group := range groups {
|
for _, group := range groups {
|
||||||
|
|
|
@ -307,8 +307,12 @@ member: cn=jane,ou=People,dc=example,dc=org
|
||||||
c.UserSearch.IDAttr = "DN"
|
c.UserSearch.IDAttr = "DN"
|
||||||
c.UserSearch.Username = "cn"
|
c.UserSearch.Username = "cn"
|
||||||
c.GroupSearch.BaseDN = "ou=Groups,dc=example,dc=org"
|
c.GroupSearch.BaseDN = "ou=Groups,dc=example,dc=org"
|
||||||
c.GroupSearch.UserAttr = "DN"
|
c.GroupSearch.UserMatchers = []UserMatcher{
|
||||||
c.GroupSearch.GroupAttr = "member"
|
{
|
||||||
|
UserAttr: "DN",
|
||||||
|
GroupAttr: "member",
|
||||||
|
},
|
||||||
|
}
|
||||||
c.GroupSearch.NameAttr = "cn"
|
c.GroupSearch.NameAttr = "cn"
|
||||||
|
|
||||||
tests := []subtest{
|
tests := []subtest{
|
||||||
|
@ -400,8 +404,12 @@ gidNumber: 1002
|
||||||
c.UserSearch.IDAttr = "DN"
|
c.UserSearch.IDAttr = "DN"
|
||||||
c.UserSearch.Username = "cn"
|
c.UserSearch.Username = "cn"
|
||||||
c.GroupSearch.BaseDN = "ou=Groups,dc=example,dc=org"
|
c.GroupSearch.BaseDN = "ou=Groups,dc=example,dc=org"
|
||||||
c.GroupSearch.UserAttr = "departmentNumber"
|
c.GroupSearch.UserMatchers = []UserMatcher{
|
||||||
c.GroupSearch.GroupAttr = "gidNumber"
|
{
|
||||||
|
UserAttr: "departmentNumber",
|
||||||
|
GroupAttr: "gidNumber",
|
||||||
|
},
|
||||||
|
}
|
||||||
c.GroupSearch.NameAttr = "cn"
|
c.GroupSearch.NameAttr = "cn"
|
||||||
tests := []subtest{
|
tests := []subtest{
|
||||||
{
|
{
|
||||||
|
@ -497,8 +505,12 @@ member: cn=jane,ou=People,dc=example,dc=org
|
||||||
c.UserSearch.IDAttr = "DN"
|
c.UserSearch.IDAttr = "DN"
|
||||||
c.UserSearch.Username = "cn"
|
c.UserSearch.Username = "cn"
|
||||||
c.GroupSearch.BaseDN = "dc=example,dc=org"
|
c.GroupSearch.BaseDN = "dc=example,dc=org"
|
||||||
c.GroupSearch.UserAttr = "DN"
|
c.GroupSearch.UserMatchers = []UserMatcher{
|
||||||
c.GroupSearch.GroupAttr = "member"
|
{
|
||||||
|
UserAttr: "DN",
|
||||||
|
GroupAttr: "member",
|
||||||
|
},
|
||||||
|
}
|
||||||
c.GroupSearch.NameAttr = "cn"
|
c.GroupSearch.NameAttr = "cn"
|
||||||
c.GroupSearch.Filter = "(ou:dn:=Seattle)" // ignore other groups
|
c.GroupSearch.Filter = "(ou:dn:=Seattle)" // ignore other groups
|
||||||
|
|
||||||
|
@ -534,6 +546,136 @@ member: cn=jane,ou=People,dc=example,dc=org
|
||||||
runTests(t, schema, connectLDAP, c, tests)
|
runTests(t, schema, connectLDAP, c, tests)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestGroupToUserMatchers(t *testing.T) {
|
||||||
|
schema := `
|
||||||
|
dn: ou=People,dc=example,dc=org
|
||||||
|
objectClass: organizationalUnit
|
||||||
|
ou: People
|
||||||
|
|
||||||
|
dn: cn=jane,ou=People,dc=example,dc=org
|
||||||
|
objectClass: person
|
||||||
|
objectClass: inetOrgPerson
|
||||||
|
sn: doe
|
||||||
|
cn: jane
|
||||||
|
uid: janedoe
|
||||||
|
mail: janedoe@example.com
|
||||||
|
userpassword: foo
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
# Group definitions.
|
||||||
|
|
||||||
|
dn: ou=Seattle,dc=example,dc=org
|
||||||
|
objectClass: organizationalUnit
|
||||||
|
ou: Seattle
|
||||||
|
|
||||||
|
dn: ou=Portland,dc=example,dc=org
|
||||||
|
objectClass: organizationalUnit
|
||||||
|
ou: Portland
|
||||||
|
|
||||||
|
dn: ou=Groups,ou=Seattle,dc=example,dc=org
|
||||||
|
objectClass: organizationalUnit
|
||||||
|
ou: Groups
|
||||||
|
|
||||||
|
dn: ou=UnixGroups,ou=Seattle,dc=example,dc=org
|
||||||
|
objectClass: organizationalUnit
|
||||||
|
ou: UnixGroups
|
||||||
|
|
||||||
|
dn: ou=Groups,ou=Portland,dc=example,dc=org
|
||||||
|
objectClass: organizationalUnit
|
||||||
|
ou: Groups
|
||||||
|
|
||||||
|
dn: ou=UnixGroups,ou=Portland,dc=example,dc=org
|
||||||
|
objectClass: organizationalUnit
|
||||||
|
ou: UnixGroups
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
dn: cn=admins,ou=Groups,ou=Seattle,dc=example,dc=org
|
||||||
|
objectClass: groupOfNames
|
||||||
|
cn: admins
|
||||||
|
member: cn=john,ou=People,dc=example,dc=org
|
||||||
|
member: cn=jane,ou=People,dc=example,dc=org
|
||||||
|
|
||||||
|
dn: cn=developers,ou=Groups,ou=Seattle,dc=example,dc=org
|
||||||
|
objectClass: groupOfNames
|
||||||
|
cn: developers
|
||||||
|
member: cn=jane,ou=People,dc=example,dc=org
|
||||||
|
|
||||||
|
dn: cn=frontend,ou=UnixGroups,ou=Seattle,dc=example,dc=org
|
||||||
|
objectClass: posixGroup
|
||||||
|
gidNumber: 1001
|
||||||
|
cn: frontend
|
||||||
|
memberUid: janedoe
|
||||||
|
`
|
||||||
|
c := &Config{}
|
||||||
|
c.UserSearch.BaseDN = "ou=People,dc=example,dc=org"
|
||||||
|
c.UserSearch.NameAttr = "cn"
|
||||||
|
c.UserSearch.EmailAttr = "mail"
|
||||||
|
c.UserSearch.IDAttr = "DN"
|
||||||
|
c.UserSearch.Username = "cn"
|
||||||
|
c.GroupSearch.BaseDN = "dc=example,dc=org"
|
||||||
|
c.GroupSearch.UserMatchers = []UserMatcher{
|
||||||
|
{
|
||||||
|
UserAttr: "DN",
|
||||||
|
GroupAttr: "member",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
UserAttr: "uid",
|
||||||
|
GroupAttr: "memberUid",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
c.GroupSearch.NameAttr = "cn"
|
||||||
|
c.GroupSearch.Filter = "(|(objectClass=posixGroup)(objectClass=groupOfNames))" // search all group types
|
||||||
|
|
||||||
|
tests := []subtest{
|
||||||
|
{
|
||||||
|
name: "validpassword",
|
||||||
|
username: "jane",
|
||||||
|
password: "foo",
|
||||||
|
groups: true,
|
||||||
|
want: connector.Identity{
|
||||||
|
UserID: "cn=jane,ou=People,dc=example,dc=org",
|
||||||
|
Username: "jane",
|
||||||
|
Email: "janedoe@example.com",
|
||||||
|
EmailVerified: true,
|
||||||
|
Groups: []string{"admins", "developers", "frontend"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "validpassword2",
|
||||||
|
username: "john",
|
||||||
|
password: "bar",
|
||||||
|
groups: true,
|
||||||
|
want: connector.Identity{
|
||||||
|
UserID: "cn=john,ou=People,dc=example,dc=org",
|
||||||
|
Username: "john",
|
||||||
|
Email: "johndoe@example.com",
|
||||||
|
EmailVerified: true,
|
||||||
|
Groups: []string{"qa", "admins", "logger"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
runTests(t, schema, connectLDAP, c, tests)
|
||||||
|
}
|
||||||
|
|
||||||
func TestStartTLS(t *testing.T) {
|
func TestStartTLS(t *testing.T) {
|
||||||
schema := `
|
schema := `
|
||||||
dn: ou=People,dc=example,dc=org
|
dn: ou=People,dc=example,dc=org
|
||||||
|
|
|
@ -41,9 +41,10 @@ connectors:
|
||||||
baseDN: cn=Users,dc=example,dc=com
|
baseDN: cn=Users,dc=example,dc=com
|
||||||
filter: "(objectClass=group)"
|
filter: "(objectClass=group)"
|
||||||
|
|
||||||
|
userMatchers:
|
||||||
# A user is a member of a group when their DN matches
|
# A user is a member of a group when their DN matches
|
||||||
# the value of a "member" attribute on the group entity.
|
# the value of a "member" attribute on the group entity.
|
||||||
userAttr: DN
|
- userAttr: DN
|
||||||
groupAttr: member
|
groupAttr: member
|
||||||
|
|
||||||
# The group name should be the "cn" value.
|
# The group name should be the "cn" value.
|
||||||
|
|
|
@ -37,9 +37,10 @@ connectors:
|
||||||
baseDN: ou=Groups,dc=example,dc=org
|
baseDN: ou=Groups,dc=example,dc=org
|
||||||
filter: "(objectClass=groupOfNames)"
|
filter: "(objectClass=groupOfNames)"
|
||||||
|
|
||||||
|
userMatchers:
|
||||||
# A user is a member of a group when their DN matches
|
# A user is a member of a group when their DN matches
|
||||||
# the value of a "member" attribute on the group entity.
|
# the value of a "member" attribute on the group entity.
|
||||||
userAttr: DN
|
- userAttr: DN
|
||||||
groupAttr: member
|
groupAttr: member
|
||||||
|
|
||||||
# The group name should be the "cn" value.
|
# The group name should be the "cn" value.
|
||||||
|
|
Reference in a new issue