Browse Source

Merge 53031bf470 into 6b9ce00e11

pull/4061/merge
Yarden Shoham 12 hours ago committed by GitHub
parent
commit
0e414f3131
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 59
      connector/ldap/ldap.go
  2. 91
      connector/ldap/ldap_test.go
  3. 22
      connector/ldap/testdata/schema.ldif

59
connector/ldap/ldap.go

@ -34,10 +34,12 @@ 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
@ -58,6 +60,27 @@ import (
// nameAttr: name
//
// UsernameAttributes represents one or more LDAP attributes to match against
// the username input. It supports unmarshaling from both a single string
// (e.g. "uid") and a list of strings (e.g. ["uid", "mail"]).
type UsernameAttributes []string
func (u *UsernameAttributes) UnmarshalJSON(data []byte) error {
var arr []string
if err := json.Unmarshal(data, &arr); err == nil {
*u = arr
return nil
}
var s string
if err := json.Unmarshal(data, &s); err != nil {
return fmt.Errorf("username must be a string or list of strings")
}
if s != "" {
*u = UsernameAttributes{s}
}
return nil
}
// UserMatcher holds information about user and group matching.
type UserMatcher struct {
UserAttr string `json:"userAttr"`
@ -110,9 +133,10 @@ 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>)".
Username string `json:"username"`
// Attribute(s) to match against the inputted username. Accepts a single string
// or a list of strings. When multiple attributes are specified, an OR filter is
// constructed: "(|(<attr1>=<username>)(<attr2>=<username>))".
Username UsernameAttributes `json:"username"`
// Can either be:
// * "sub" - search the whole sub tree
@ -242,7 +266,6 @@ func (c *Config) openConnector(logger *slog.Logger) (*ldapConnector, error) {
}{
{"host", c.Host},
{"userSearch.baseDN", c.UserSearch.BaseDN},
{"userSearch.username", c.UserSearch.Username},
}
for _, field := range requiredFields {
@ -251,6 +274,10 @@ func (c *Config) openConnector(logger *slog.Logger) (*ldapConnector, error) {
}
}
if len(c.UserSearch.Username) == 0 {
return nil, fmt.Errorf("ldap: missing required field %q", "userSearch.username")
}
var (
host string
err error
@ -298,7 +325,7 @@ func (c *Config) openConnector(logger *slog.Logger) (*ldapConnector, error) {
// TODO(nabokihms): remove it after deleting deprecated groupSearch options
c.GroupSearch.UserMatchers = userMatchers(c, logger)
return &ldapConnector{*c, userSearchScope, groupSearchScope, tlsConfig, logger}, nil
return &ldapConnector{*c, userSearchScope, groupSearchScope, tlsConfig, c.UserSearch.Username, logger}, nil
}
var (
@ -314,6 +341,8 @@ type ldapConnector struct {
tlsConfig *tls.Config
usernameAttrs []string
logger *slog.Logger
}
@ -421,7 +450,19 @@ 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)
attrFilters := make([]string, 0, len(c.usernameAttrs))
for _, attr := range c.usernameAttrs {
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)
}
@ -439,6 +480,8 @@ func (c *ldapConnector) userEntry(conn *ldap.Conn, username string) (user ldap.E
},
}
req.Attributes = append(req.Attributes, c.usernameAttrs...)
for _, matcher := range c.GroupSearch.UserMatchers {
req.Attributes = append(req.Attributes, matcher.UserAttr)
}

91
connector/ldap/ldap_test.go

@ -45,7 +45,7 @@ func TestQuery(t *testing.T) {
c.UserSearch.NameAttr = "cn"
c.UserSearch.EmailAttr = "mail"
c.UserSearch.IDAttr = "DN"
c.UserSearch.Username = "cn"
c.UserSearch.Username = UsernameAttributes{"cn"}
tests := []subtest{
{
@ -105,7 +105,7 @@ func TestQueryWithEmailSuffix(t *testing.T) {
c.UserSearch.NameAttr = "cn"
c.UserSearch.EmailSuffix = "test.example.com"
c.UserSearch.IDAttr = "DN"
c.UserSearch.Username = "cn"
c.UserSearch.Username = UsernameAttributes{"cn"}
tests := []subtest{
{
@ -141,7 +141,7 @@ func TestUserFilter(t *testing.T) {
c.UserSearch.NameAttr = "cn"
c.UserSearch.EmailAttr = "mail"
c.UserSearch.IDAttr = "DN"
c.UserSearch.Username = "cn"
c.UserSearch.Username = UsernameAttributes{"cn"}
c.UserSearch.Filter = "(ou:dn:=Seattle)"
tests := []subtest{
@ -184,13 +184,50 @@ 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 = UsernameAttributes{"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"
c.UserSearch.NameAttr = "cn"
c.UserSearch.EmailAttr = "mail"
c.UserSearch.IDAttr = "DN"
c.UserSearch.Username = "cn"
c.UserSearch.Username = UsernameAttributes{"cn"}
c.GroupSearch.BaseDN = "ou=Groups,ou=TestGroupQuery,dc=example,dc=org"
c.GroupSearch.UserMatchers = []UserMatcher{
{
@ -238,7 +275,7 @@ func TestGroupsOnUserEntity(t *testing.T) {
c.UserSearch.NameAttr = "cn"
c.UserSearch.EmailAttr = "mail"
c.UserSearch.IDAttr = "DN"
c.UserSearch.Username = "cn"
c.UserSearch.Username = UsernameAttributes{"cn"}
c.GroupSearch.BaseDN = "ou=Groups,ou=TestGroupsOnUserEntity,dc=example,dc=org"
c.GroupSearch.UserMatchers = []UserMatcher{
{
@ -284,7 +321,7 @@ func TestGroupFilter(t *testing.T) {
c.UserSearch.NameAttr = "cn"
c.UserSearch.EmailAttr = "mail"
c.UserSearch.IDAttr = "DN"
c.UserSearch.Username = "cn"
c.UserSearch.Username = UsernameAttributes{"cn"}
c.GroupSearch.BaseDN = "ou=TestGroupFilter,dc=example,dc=org"
c.GroupSearch.UserMatchers = []UserMatcher{
{
@ -333,7 +370,7 @@ func TestGroupToUserMatchers(t *testing.T) {
c.UserSearch.NameAttr = "cn"
c.UserSearch.EmailAttr = "mail"
c.UserSearch.IDAttr = "DN"
c.UserSearch.Username = "cn"
c.UserSearch.Username = UsernameAttributes{"cn"}
c.GroupSearch.BaseDN = "ou=TestGroupToUserMatchers,dc=example,dc=org"
c.GroupSearch.UserMatchers = []UserMatcher{
{
@ -389,7 +426,7 @@ func TestDeprecatedGroupToUserMatcher(t *testing.T) {
c.UserSearch.NameAttr = "cn"
c.UserSearch.EmailAttr = "mail"
c.UserSearch.IDAttr = "DN"
c.UserSearch.Username = "cn"
c.UserSearch.Username = UsernameAttributes{"cn"}
c.GroupSearch.BaseDN = "ou=TestDeprecatedGroupToUserMatcher,dc=example,dc=org"
c.GroupSearch.UserAttr = "DN"
c.GroupSearch.GroupAttr = "member"
@ -434,7 +471,7 @@ func TestStartTLS(t *testing.T) {
c.UserSearch.NameAttr = "cn"
c.UserSearch.EmailAttr = "mail"
c.UserSearch.IDAttr = "DN"
c.UserSearch.Username = "cn"
c.UserSearch.Username = UsernameAttributes{"cn"}
tests := []subtest{
{
@ -458,7 +495,7 @@ func TestInsecureSkipVerify(t *testing.T) {
c.UserSearch.NameAttr = "cn"
c.UserSearch.EmailAttr = "mail"
c.UserSearch.IDAttr = "DN"
c.UserSearch.Username = "cn"
c.UserSearch.Username = UsernameAttributes{"cn"}
tests := []subtest{
{
@ -482,7 +519,7 @@ func TestLDAPS(t *testing.T) {
c.UserSearch.NameAttr = "cn"
c.UserSearch.EmailAttr = "mail"
c.UserSearch.IDAttr = "DN"
c.UserSearch.Username = "cn"
c.UserSearch.Username = UsernameAttributes{"cn"}
tests := []subtest{
{
@ -525,13 +562,43 @@ func TestUsernamePrompt(t *testing.T) {
}
}
func TestUsernameAttributesUnmarshal(t *testing.T) {
tests := []struct {
name string
json string
want UsernameAttributes
wantErr bool
}{
{name: "single string", json: `"uid"`, want: UsernameAttributes{"uid"}},
{name: "array of strings", json: `["uid","mail"]`, want: UsernameAttributes{"uid", "mail"}},
{name: "single element array", json: `["cn"]`, want: UsernameAttributes{"cn"}},
{name: "empty string", json: `""`, want: nil},
{name: "invalid type", json: `123`, wantErr: true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var got UsernameAttributes
err := got.UnmarshalJSON([]byte(tt.json))
if (err != nil) != tt.wantErr {
t.Fatalf("UnmarshalJSON() error = %v, wantErr %v", err, tt.wantErr)
}
if !tt.wantErr {
if diff := pretty.Compare(tt.want, got); diff != "" {
t.Errorf("unexpected result: %s", diff)
}
}
})
}
}
func TestNestedGroups(t *testing.T) {
c := &Config{}
c.UserSearch.BaseDN = "ou=People,ou=TestNestedGroups,dc=example,dc=org"
c.UserSearch.NameAttr = "cn"
c.UserSearch.EmailAttr = "mail"
c.UserSearch.IDAttr = "DN"
c.UserSearch.Username = "cn"
c.UserSearch.Username = UsernameAttributes{"cn"}
c.GroupSearch.BaseDN = "ou=TestNestedGroups,dc=example,dc=org"
c.GroupSearch.UserMatchers = []UserMatcher{

22
connector/ldap/testdata/schema.ldif vendored

@ -505,3 +505,25 @@ 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=Seattle,ou=TestUsernameWithMultipleAttributes,dc=example,dc=org
objectClass: organizationalUnit
ou: Seattle
dn: ou=People,ou=Seattle,ou=TestUsernameWithMultipleAttributes,dc=example,dc=org
objectClass: organizationalUnit
ou: People
dn: cn=jane,ou=People,ou=Seattle,ou=TestUsernameWithMultipleAttributes,dc=example,dc=org
objectClass: person
objectClass: inetOrgPerson
sn: doe
cn: jane
mail: janedoe@example.com
userpassword: foo
Loading…
Cancel
Save