diff --git a/connector/ldap/ldap.go b/connector/ldap/ldap.go index 9fe386c6..0e3d26f0 100644 --- a/connector/ldap/ldap.go +++ b/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=))" +// # Would translate to the query "(&(objectClass=person)(|(uid=)(mail=)))" // 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 "(=)". + // Attributes (comma-separated) to match (OR)against the inputted username. This will be translated and combined + // with the other filter as "(|(=)(=))". 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) } diff --git a/connector/ldap/ldap_test.go b/connector/ldap/ldap_test.go index a9665e12..240911ae 100644 --- a/connector/ldap/ldap_test.go +++ b/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" diff --git a/connector/ldap/testdata/schema.ldif b/connector/ldap/testdata/schema.ldif index a7f1393d..73611c3f 100644 --- a/connector/ldap/testdata/schema.ldif +++ b/connector/ldap/testdata/schema.ldif @@ -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 \ No newline at end of file