Browse Source

feat(ldap): allow specifying multiple attributes on username input

In some use-cases, one would like to login using either their username or email. Administrators now don't have to choose a single field but may specify multiple fields to count as "username".

This change is backwards-compatible.

Signed-off-by: Yarden Shoham <git@yardenshoham.com>
pull/4061/head
Yarden Shoham 2 months ago
parent
commit
4185cbb986
  1. 33
      connector/ldap/ldap.go
  2. 37
      connector/ldap/ldap_test.go
  3. 18
      connector/ldap/testdata/schema.ldif

33
connector/ldap/ldap.go

@ -34,10 +34,10 @@ import (
// bindDN: uid=serviceaccount,cn=users,dc=example,dc=com
// bindPW: password
// userSearch:
// # Would translate to the query "(&(objectClass=person)(uid=<username>))"
// # Would translate to the query "(&(objectClass=person)(|(uid=<username>)(mail=<username>)))"
// baseDN: cn=users,dc=example,dc=com
// filter: "(objectClass=person)"
// username: uid
// username: uid,mail
// idAttr: uid
// emailAttr: mail
// nameAttr: name
@ -110,8 +110,8 @@ type Config struct {
// Optional filter to apply when searching the directory. For example "(objectClass=person)"
Filter string `json:"filter"`
// Attribute to match against the inputted username. This will be translated and combined
// with the other filter as "(<attr>=<username>)".
// Attributes (comma-separated) to match (OR)against the inputted username. This will be translated and combined
// with the other filter as "(|(<attr1>=<username>)(<attr2>=<username>))".
Username string `json:"username"`
// Can either be:
@ -419,7 +419,25 @@ func (c *ldapConnector) identityFromEntry(user ldap.Entry) (ident connector.Iden
}
func (c *ldapConnector) userEntry(conn *ldap.Conn, username string) (user ldap.Entry, found bool, err error) {
filter := fmt.Sprintf("(%s=%s)", c.UserSearch.Username, ldap.EscapeFilter(username))
var filter string
escapedUsername := ldap.EscapeFilter(username)
// Split username attribute by comma to support multiple search attributes
usernameAttrs := strings.Split(c.UserSearch.Username, ",")
attrFilters := make([]string, 0, len(usernameAttrs))
for _, attr := range usernameAttrs {
attr = strings.TrimSpace(attr)
if attr != "" {
attrFilters = append(attrFilters, fmt.Sprintf("(%s=%s)", attr, escapedUsername))
}
}
if len(attrFilters) == 1 {
filter = attrFilters[0] // Skip OR wrapper for single attribute
} else {
filter = fmt.Sprintf("(|%s)", strings.Join(attrFilters, ""))
}
if c.UserSearch.Filter != "" {
filter = fmt.Sprintf("(&%s%s)", c.UserSearch.Filter, filter)
}
@ -437,6 +455,11 @@ func (c *ldapConnector) userEntry(conn *ldap.Conn, username string) (user ldap.E
},
}
for _, attr := range usernameAttrs {
attr = strings.TrimSpace(attr)
req.Attributes = append(req.Attributes, attr)
}
for _, matcher := range c.GroupSearch.UserMatchers {
req.Attributes = append(req.Attributes, matcher.UserAttr)
}

37
connector/ldap/ldap_test.go

@ -184,6 +184,43 @@ func TestUserFilter(t *testing.T) {
runTests(t, connectLDAP, c, tests)
}
func TestUsernameWithMultipleAttributes(t *testing.T) {
c := &Config{}
c.UserSearch.BaseDN = "ou=TestUsernameWithMultipleAttributes,dc=example,dc=org"
c.UserSearch.NameAttr = "cn"
c.UserSearch.EmailAttr = "mail"
c.UserSearch.IDAttr = "DN"
c.UserSearch.Username = "cn,mail"
c.UserSearch.Filter = "(ou:dn:=Seattle)"
tests := []subtest{
{
name: "cn",
username: "jane",
password: "foo",
want: connector.Identity{
UserID: "cn=jane,ou=People,ou=Seattle,ou=TestUsernameWithMultipleAttributes,dc=example,dc=org",
Username: "jane",
Email: "janedoe@example.com",
EmailVerified: true,
},
},
{
name: "mail",
username: "janedoe@example.com",
password: "foo",
want: connector.Identity{
UserID: "cn=jane,ou=People,ou=Seattle,ou=TestUsernameWithMultipleAttributes,dc=example,dc=org",
Username: "jane",
Email: "janedoe@example.com",
EmailVerified: true,
},
},
}
runTests(t, connectLDAP, c, tests)
}
func TestGroupQuery(t *testing.T) {
c := &Config{}
c.UserSearch.BaseDN = "ou=People,ou=TestGroupQuery,dc=example,dc=org"

18
connector/ldap/testdata/schema.ldif vendored

@ -505,3 +505,21 @@ objectClass: groupOfNames
cn: circularGroup2
member: cn=circularGroup1,ou=Groups,ou=TestNestedGroups,dc=example,dc=org
member: cn=john,ou=People,ou=TestNestedGroups,dc=example,dc=org
########################################################################
dn: ou=TestUsernameWithMultipleAttributes,dc=example,dc=org
objectClass: organizationalUnit
ou: TestUsernameWithMultipleAttributes
dn: ou=People,ou=TestUsernameWithMultipleAttributes,dc=example,dc=org
objectClass: organizationalUnit
ou: People
dn: cn=jane,ou=People,ou=TestUsernameWithMultipleAttributes,dc=example,dc=org
objectClass: person
objectClass: inetOrgPerson
sn: doe
cn: jane
mail: janedoe@example.com
userpassword: foo
Loading…
Cancel
Save