diff --git a/ldap/client.go b/ldap/client.go index 715a7de..1c131a6 100644 --- a/ldap/client.go +++ b/ldap/client.go @@ -215,7 +215,8 @@ type Attribute struct { // the WithGroups option is specified, it will also return the user's groups // from the directory. // -// Supported options: WithUserAttributes, WithGroups, WithDialer, WithURLs, WithLowerUserAttributeKeys +// Supported options: WithUserAttributes, WithGroups, WithDialer, WithURLs, +// WithLowerUserAttributeKeys, WithEmptyAnonymousGroupSearch func (c *Client) Authenticate(ctx context.Context, username, password string, opt ...Option) (*AuthResult, error) { const op = "ldap.(Client).Authenticate" if username == "" { @@ -289,7 +290,14 @@ func (c *Client) Authenticate(ctx context.Context, username, password string, op } if c.conf.AnonymousGroupSearch { - if err := c.conn.UnauthenticatedBind(userDN); err != nil { + // Some LDAP servers will reject anonymous group searches if userDN is + // included in the query. + dn := userDN + if c.conf.AllowEmptyAnonymousGroupSearch || opts.withEmptyAnonymousGroupSearch { + dn = "" + } + + if err := c.conn.UnauthenticatedBind(dn); err != nil { return nil, fmt.Errorf("%s: group search anonymous bind failed: %w", op, err) } } diff --git a/ldap/client_exported_test.go b/ldap/client_exported_test.go index b8b5d38..f1853e1 100644 --- a/ldap/client_exported_test.go +++ b/ldap/client_exported_test.go @@ -337,6 +337,73 @@ func TestClient_Authenticate(t *testing.T) { opts: []ldap.Option{ldap.WithGroups()}, wantGroups: []string{groups[0].DN}, }, + { + name: "success-with-anon-bind-groups-empty-userdn", + username: "alice", + password: "password", + clientConfig: &ldap.ClientConfig{ + URLs: []string{fmt.Sprintf("ldaps://127.0.0.1:%d", td.Port())}, + Certificates: []string{td.Cert()}, + DiscoverDN: true, + UserDN: testdirectory.DefaultUserDN, + GroupDN: testdirectory.DefaultGroupDN, + UseTokenGroups: true, + AnonymousGroupSearch: true, + AllowEmptyAnonymousGroupSearch: true, + }, + opts: []ldap.Option{ldap.WithGroups()}, + wantGroups: []string{groups[0].DN}, + }, + { + name: "success-with-anon-bind-groups-empty-userdn-opt", + username: "alice", + password: "password", + clientConfig: &ldap.ClientConfig{ + URLs: []string{fmt.Sprintf("ldaps://127.0.0.1:%d", td.Port())}, + Certificates: []string{td.Cert()}, + DiscoverDN: true, + UserDN: testdirectory.DefaultUserDN, + GroupDN: testdirectory.DefaultGroupDN, + UseTokenGroups: true, + AnonymousGroupSearch: true, + }, + opts: []ldap.Option{ldap.WithGroups(), ldap.WithEmptyAnonymousGroupSearch()}, + wantGroups: []string{groups[0].DN}, + }, + { + name: "success-with-anon-bind-upn-domain-empty-userdn", + username: "eve", + password: "password", + clientConfig: &ldap.ClientConfig{ + URLs: []string{fmt.Sprintf("ldaps://127.0.0.1:%d", td.Port())}, + Certificates: []string{td.Cert()}, + DiscoverDN: true, + UserDN: testdirectory.DefaultUserDN, + GroupDN: testdirectory.DefaultGroupDN, + UPNDomain: "example.com", + AnonymousGroupSearch: true, + AllowEmptyAnonymousGroupSearch: true, + }, + opts: []ldap.Option{ldap.WithGroups()}, + wantGroups: []string{groups[0].DN}, + }, + { + name: "success-with-anon-bind-upn-domain-empty-userdn-opt", + username: "eve", + password: "password", + clientConfig: &ldap.ClientConfig{ + URLs: []string{fmt.Sprintf("ldaps://127.0.0.1:%d", td.Port())}, + Certificates: []string{td.Cert()}, + DiscoverDN: true, + UserDN: testdirectory.DefaultUserDN, + GroupDN: testdirectory.DefaultGroupDN, + UPNDomain: "example.com", + AnonymousGroupSearch: true, + AllowEmptyAnonymousGroupSearch: true, + }, + opts: []ldap.Option{ldap.WithGroups(), ldap.WithEmptyAnonymousGroupSearch()}, + wantGroups: []string{groups[0].DN}, + }, { name: "success-with-binddn", username: "alice", diff --git a/ldap/config.go b/ldap/config.go index b09f830..6340799 100644 --- a/ldap/config.go +++ b/ldap/config.go @@ -87,6 +87,10 @@ type ClientConfig struct { // for the initial connection test). AnonymousGroupSearch bool `json:"anonymous_group_search"` + // AllowEmptyAnonymousGroupSearches: if true it removes the userDN from + // unauthenticated group searches (optional). + AllowEmptyAnonymousGroupSearch bool `json:"allow_empty_anonymous_group_search"` + // GroupDN is the distinguished name to use as base when searching for group // membership (eg: ou=Groups,dc=example,dc=org) GroupDN string `json:"groupdn"` diff --git a/ldap/options.go b/ldap/options.go index fec5da9..94f4342 100644 --- a/ldap/options.go +++ b/ldap/options.go @@ -8,16 +8,17 @@ package ldap type Option func(interface{}) type configOptions struct { - withURLs []string - withInsecureTLS bool - withTLSMinVersion string - withTLSMaxVersion string - withCertificates []string - withClientTLSCert string - withClientTLSKey string - withGroups bool - withUserAttributes bool - withLowerUserAttributeKeys bool + withURLs []string + withInsecureTLS bool + withTLSMinVersion string + withTLSMaxVersion string + withCertificates []string + withClientTLSCert string + withClientTLSKey string + withGroups bool + withUserAttributes bool + withLowerUserAttributeKeys bool + withEmptyAnonymousGroupSearch bool } func configDefaults() configOptions { @@ -88,6 +89,16 @@ func WithLowerUserAttributeKeys() Option { } } +// WithEmptyAnonymousGroupSearch removes userDN from anonymous group searches. +func WithEmptyAnonymousGroupSearch() Option { + return func(o interface{}) { + switch v := o.(type) { + case *configOptions: + v.withEmptyAnonymousGroupSearch = true + } + } +} + func withTLSMinVersion(version string) Option { return func(o interface{}) { switch v := o.(type) {