Use groupSearch.userAttributeForFilter during LDAP group searches

Load the setting in the controller.
Use the setting during authentication and during refreshes.
This commit is contained in:
Ryan Richard 2023-05-25 14:25:17 -07:00
parent bad5e60a8e
commit c187474499
6 changed files with 607 additions and 90 deletions

View File

@ -1,4 +1,4 @@
// Copyright 2021-2022 the Pinniped contributors. All Rights Reserved.
// Copyright 2021-2023 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
// Package activedirectoryupstreamwatcher implements a controller which watches ActiveDirectoryIdentityProviders.
@ -203,6 +203,10 @@ func (g *activeDirectoryUpstreamGenericLDAPGroupSearch) Filter() string {
return g.groupSearch.Filter
}
func (g *activeDirectoryUpstreamGenericLDAPGroupSearch) UserAttributeForFilter() string {
return ""
}
func (g *activeDirectoryUpstreamGenericLDAPGroupSearch) GroupNameAttribute() string {
if len(g.groupSearch.Attributes.GroupName) == 0 {
return defaultActiveDirectoryGroupNameAttributeName

View File

@ -1,4 +1,4 @@
// Copyright 2021-2022 the Pinniped contributors. All Rights Reserved.
// Copyright 2021-2023 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
// Package ldapupstreamwatcher implements a controller which watches LDAPIdentityProviders.
@ -115,6 +115,10 @@ func (g *ldapUpstreamGenericLDAPGroupSearch) Filter() string {
return g.groupSearch.Filter
}
func (g *ldapUpstreamGenericLDAPGroupSearch) UserAttributeForFilter() string {
return g.groupSearch.UserAttributeForFilter
}
func (g *ldapUpstreamGenericLDAPGroupSearch) GroupNameAttribute() string {
return g.groupSearch.Attributes.GroupName
}
@ -236,10 +240,11 @@ func (c *ldapWatcherController) validateUpstream(ctx context.Context, upstream *
UIDAttribute: spec.UserSearch.Attributes.UID,
},
GroupSearch: upstreamldap.GroupSearchConfig{
Base: spec.GroupSearch.Base,
Filter: spec.GroupSearch.Filter,
GroupNameAttribute: spec.GroupSearch.Attributes.GroupName,
SkipGroupRefresh: spec.GroupSearch.SkipGroupRefresh,
Base: spec.GroupSearch.Base,
Filter: spec.GroupSearch.Filter,
UserAttributeForFilter: spec.GroupSearch.UserAttributeForFilter,
GroupNameAttribute: spec.GroupSearch.Attributes.GroupName,
SkipGroupRefresh: spec.GroupSearch.SkipGroupRefresh,
},
Dialer: c.ldapDialer,
}

View File

@ -1,4 +1,4 @@
// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved.
// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package ldapupstreamwatcher
@ -148,20 +148,25 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) {
now := metav1.NewTime(time.Now().UTC())
const (
testNamespace = "test-namespace"
testName = "test-name"
testResourceUID = "test-resource-uid"
testSecretName = "test-bind-secret"
testBindUsername = "test-bind-username"
testBindPassword = "test-bind-password"
testHost = "ldap.example.com:123"
testUserSearchBase = "test-user-search-base"
testUserSearchFilter = "test-user-search-filter"
testGroupSearchBase = "test-group-search-base"
testGroupSearchFilter = "test-group-search-filter"
testUsernameAttrName = "test-username-attr"
testGroupNameAttrName = "test-group-name-attr"
testUIDAttrName = "test-uid-attr"
testNamespace = "test-namespace"
testName = "test-name"
testResourceUID = "test-resource-uid"
testHost = "ldap.example.com:123"
testBindSecretName = "test-bind-secret"
testBindUsername = "test-bind-username"
testBindPassword = "test-bind-password"
testUserSearchBase = "test-user-search-base"
testUserSearchFilter = "test-user-search-filter"
testUserSearchUsernameAttrName = "test-username-attr"
testUserSearchUIDAttrName = "test-uid-attr"
testGroupSearchBase = "test-group-search-base"
testGroupSearchFilter = "test-group-search-filter"
testGroupSearchUserAttributeForFilter = "test-group-search-filter-user-attr-for-filter"
testGroupSearchNameAttrName = "test-group-name-attr"
)
testValidSecretData := map[string][]byte{"username": []byte(testBindUsername), "password": []byte(testBindPassword)}
@ -181,20 +186,21 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) {
Spec: v1alpha1.LDAPIdentityProviderSpec{
Host: testHost,
TLS: &v1alpha1.TLSSpec{CertificateAuthorityData: testCABundleBase64Encoded},
Bind: v1alpha1.LDAPIdentityProviderBind{SecretName: testSecretName},
Bind: v1alpha1.LDAPIdentityProviderBind{SecretName: testBindSecretName},
UserSearch: v1alpha1.LDAPIdentityProviderUserSearch{
Base: testUserSearchBase,
Filter: testUserSearchFilter,
Attributes: v1alpha1.LDAPIdentityProviderUserSearchAttributes{
Username: testUsernameAttrName,
UID: testUIDAttrName,
Username: testUserSearchUsernameAttrName,
UID: testUserSearchUIDAttrName,
},
},
GroupSearch: v1alpha1.LDAPIdentityProviderGroupSearch{
Base: testGroupSearchBase,
Filter: testGroupSearchFilter,
Base: testGroupSearchBase,
Filter: testGroupSearchFilter,
UserAttributeForFilter: testGroupSearchUserAttributeForFilter,
Attributes: v1alpha1.LDAPIdentityProviderGroupSearchAttributes{
GroupName: testGroupNameAttrName,
GroupName: testGroupSearchNameAttrName,
},
SkipGroupRefresh: false,
},
@ -217,13 +223,14 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) {
UserSearch: upstreamldap.UserSearchConfig{
Base: testUserSearchBase,
Filter: testUserSearchFilter,
UsernameAttribute: testUsernameAttrName,
UIDAttribute: testUIDAttrName,
UsernameAttribute: testUserSearchUsernameAttrName,
UIDAttribute: testUserSearchUIDAttrName,
},
GroupSearch: upstreamldap.GroupSearchConfig{
Base: testGroupSearchBase,
Filter: testGroupSearchFilter,
GroupNameAttribute: testGroupNameAttrName,
Base: testGroupSearchBase,
Filter: testGroupSearchFilter,
UserAttributeForFilter: testGroupSearchUserAttributeForFilter,
GroupNameAttribute: testGroupSearchNameAttrName,
},
}
@ -250,7 +257,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) {
Reason: "Success",
Message: fmt.Sprintf(
`successfully able to connect to "%s" and bind as user "%s" [validated with Secret "%s" at version "%s"]`,
testHost, testBindUsername, testSecretName, secretVersion),
testHost, testBindUsername, testBindSecretName, secretVersion),
ObservedGeneration: gen,
}
}
@ -282,7 +289,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) {
validBindUserSecret := func(secretVersion string) *corev1.Secret {
return &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{Name: testSecretName, Namespace: testNamespace, ResourceVersion: secretVersion},
ObjectMeta: metav1.ObjectMeta{Name: testBindSecretName, Namespace: testNamespace, ResourceVersion: secretVersion},
Type: corev1.SecretTypeBasicAuth,
Data: testValidSecretData,
}
@ -346,7 +353,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) {
Status: "False",
LastTransitionTime: now,
Reason: "SecretNotFound",
Message: fmt.Sprintf(`secret "%s" not found`, testSecretName),
Message: fmt.Sprintf(`secret "%s" not found`, testBindSecretName),
ObservedGeneration: 1234,
},
tlsConfigurationValidLoadedTrueCondition(1234),
@ -358,7 +365,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) {
name: "secret has wrong type",
inputUpstreams: []runtime.Object{validUpstream},
inputSecrets: []runtime.Object{&corev1.Secret{
ObjectMeta: metav1.ObjectMeta{Name: testSecretName, Namespace: testNamespace},
ObjectMeta: metav1.ObjectMeta{Name: testBindSecretName, Namespace: testNamespace},
Type: "some-other-type",
Data: testValidSecretData,
}},
@ -374,7 +381,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) {
Status: "False",
LastTransitionTime: now,
Reason: "SecretWrongType",
Message: fmt.Sprintf(`referenced Secret "%s" has wrong type "some-other-type" (should be "kubernetes.io/basic-auth")`, testSecretName),
Message: fmt.Sprintf(`referenced Secret "%s" has wrong type "some-other-type" (should be "kubernetes.io/basic-auth")`, testBindSecretName),
ObservedGeneration: 1234,
},
tlsConfigurationValidLoadedTrueCondition(1234),
@ -386,7 +393,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) {
name: "secret is missing key",
inputUpstreams: []runtime.Object{validUpstream},
inputSecrets: []runtime.Object{&corev1.Secret{
ObjectMeta: metav1.ObjectMeta{Name: testSecretName, Namespace: testNamespace},
ObjectMeta: metav1.ObjectMeta{Name: testBindSecretName, Namespace: testNamespace},
Type: corev1.SecretTypeBasicAuth,
}},
wantErr: controllerlib.ErrSyntheticRequeue.Error(),
@ -401,7 +408,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) {
Status: "False",
LastTransitionTime: now,
Reason: "SecretMissingKeys",
Message: fmt.Sprintf(`referenced Secret "%s" is missing required keys ["username" "password"]`, testSecretName),
Message: fmt.Sprintf(`referenced Secret "%s" is missing required keys ["username" "password"]`, testBindSecretName),
ObservedGeneration: 1234,
},
tlsConfigurationValidLoadedTrueCondition(1234),
@ -484,13 +491,14 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) {
UserSearch: upstreamldap.UserSearchConfig{
Base: testUserSearchBase,
Filter: testUserSearchFilter,
UsernameAttribute: testUsernameAttrName,
UIDAttribute: testUIDAttrName,
UsernameAttribute: testUserSearchUsernameAttrName,
UIDAttribute: testUserSearchUIDAttrName,
},
GroupSearch: upstreamldap.GroupSearchConfig{
Base: testGroupSearchBase,
Filter: testGroupSearchFilter,
GroupNameAttribute: testGroupNameAttrName,
Base: testGroupSearchBase,
Filter: testGroupSearchFilter,
UserAttributeForFilter: testGroupSearchUserAttributeForFilter,
GroupNameAttribute: testGroupSearchNameAttrName,
},
},
},
@ -548,13 +556,14 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) {
UserSearch: upstreamldap.UserSearchConfig{
Base: testUserSearchBase,
Filter: testUserSearchFilter,
UsernameAttribute: testUsernameAttrName,
UIDAttribute: testUIDAttrName,
UsernameAttribute: testUserSearchUsernameAttrName,
UIDAttribute: testUserSearchUIDAttrName,
},
GroupSearch: upstreamldap.GroupSearchConfig{
Base: testGroupSearchBase,
Filter: testGroupSearchFilter,
GroupNameAttribute: testGroupNameAttrName,
Base: testGroupSearchBase,
Filter: testGroupSearchFilter,
UserAttributeForFilter: testGroupSearchUserAttributeForFilter,
GroupNameAttribute: testGroupSearchNameAttrName,
},
},
},
@ -571,7 +580,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) {
Reason: "Success",
Message: fmt.Sprintf(
`successfully able to connect to "%s" and bind as user "%s" [validated with Secret "%s" at version "%s"]`,
"ldap.example.com", testBindUsername, testSecretName, "4242"),
"ldap.example.com", testBindUsername, testBindSecretName, "4242"),
ObservedGeneration: 1234,
},
tlsConfigurationValidLoadedTrueCondition(1234),
@ -590,7 +599,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) {
Reason: "Success",
Message: fmt.Sprintf(
`successfully able to connect to "%s" and bind as user "%s" [validated with Secret "%s" at version "%s"]`,
"ldap.example.com", testBindUsername, testSecretName, "4242"),
"ldap.example.com", testBindUsername, testBindSecretName, "4242"),
},
}},
},
@ -619,13 +628,14 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) {
UserSearch: upstreamldap.UserSearchConfig{
Base: testUserSearchBase,
Filter: testUserSearchFilter,
UsernameAttribute: testUsernameAttrName,
UIDAttribute: testUIDAttrName,
UsernameAttribute: testUserSearchUsernameAttrName,
UIDAttribute: testUserSearchUIDAttrName,
},
GroupSearch: upstreamldap.GroupSearchConfig{
Base: testGroupSearchBase,
Filter: testGroupSearchFilter,
GroupNameAttribute: testGroupNameAttrName,
Base: testGroupSearchBase,
Filter: testGroupSearchFilter,
UserAttributeForFilter: testGroupSearchUserAttributeForFilter,
GroupNameAttribute: testGroupSearchNameAttrName,
},
},
},
@ -675,13 +685,14 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) {
UserSearch: upstreamldap.UserSearchConfig{
Base: testUserSearchBase,
Filter: testUserSearchFilter,
UsernameAttribute: testUsernameAttrName,
UIDAttribute: testUIDAttrName,
UsernameAttribute: testUserSearchUsernameAttrName,
UIDAttribute: testUserSearchUIDAttrName,
},
GroupSearch: upstreamldap.GroupSearchConfig{
Base: testGroupSearchBase,
Filter: testGroupSearchFilter,
GroupNameAttribute: testGroupNameAttrName,
Base: testGroupSearchBase,
Filter: testGroupSearchFilter,
UserAttributeForFilter: testGroupSearchUserAttributeForFilter,
GroupNameAttribute: testGroupSearchNameAttrName,
},
},
},
@ -1077,14 +1088,15 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) {
UserSearch: upstreamldap.UserSearchConfig{
Base: testUserSearchBase,
Filter: testUserSearchFilter,
UsernameAttribute: testUsernameAttrName,
UIDAttribute: testUIDAttrName,
UsernameAttribute: testUserSearchUsernameAttrName,
UIDAttribute: testUserSearchUIDAttrName,
},
GroupSearch: upstreamldap.GroupSearchConfig{
Base: testGroupSearchBase,
Filter: testGroupSearchFilter,
GroupNameAttribute: testGroupNameAttrName,
SkipGroupRefresh: true,
Base: testGroupSearchBase,
Filter: testGroupSearchFilter,
UserAttributeForFilter: testGroupSearchUserAttributeForFilter,
GroupNameAttribute: testGroupSearchNameAttrName,
SkipGroupRefresh: true,
},
},
},

View File

@ -1,4 +1,4 @@
// Copyright 2021-2022 the Pinniped contributors. All Rights Reserved.
// Copyright 2021-2023 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package upstreamwatchers
@ -126,6 +126,7 @@ type UpstreamGenericLDAPUserSearch interface {
type UpstreamGenericLDAPGroupSearch interface {
Base() string
Filter() string
UserAttributeForFilter() string
GroupNameAttribute() string
}

View File

@ -1,4 +1,4 @@
// Copyright 2021-2022 the Pinniped contributors. All Rights Reserved.
// Copyright 2021-2023 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
// Package upstreamldap implements an abstraction of upstream LDAP IDP interactions.
@ -149,6 +149,10 @@ type GroupSearchConfig struct {
// Filter is the filter to use for the group search in the upstream LDAP IDP. Empty means to use `member={}`.
Filter string
// UserAttributeForFilter is the name of the user attribute whose value should be used to replace the placeholder
// in the Filter. Empty means to use 'dn'.
UserAttributeForFilter string
// GroupNameAttribute is the attribute in the LDAP group entry from which the group name should be
// retrieved. Empty means to use 'cn'.
GroupNameAttribute string
@ -166,13 +170,13 @@ type Provider struct {
var _ provider.UpstreamLDAPIdentityProviderI = &Provider{}
var _ authenticators.UserAuthenticator = &Provider{}
// Create a Provider. The config is not a pointer to ensure that a copy of the config is created,
// New creates a Provider. The config is not a pointer to ensure that a copy of the config is created,
// making the resulting Provider use an effectively read-only configuration.
func New(config ProviderConfig) *Provider {
return &Provider{c: config}
}
// A reader for the config. Returns a copy of the config to keep the underlying config read-only.
// GetConfig is a reader for the config. Returns a copy of the config to keep the underlying config read-only.
func (p *Provider) GetConfig() ProviderConfig {
return p.c
}
@ -245,7 +249,15 @@ func (p *Provider) PerformRefresh(ctx context.Context, storedRefreshAttributes p
return nil, nil
}
mappedGroupNames, err := p.searchGroupsForUserDN(conn, userDN)
var groupSearchUserAttributeForFilterValue string
if p.useGroupSearchUserAttributeForFilter() {
groupSearchUserAttributeForFilterValue, err = p.getSearchResultAttributeValue(p.c.GroupSearch.UserAttributeForFilter, userEntry, newUsername)
if err != nil {
return nil, err
}
}
mappedGroupNames, err := p.searchGroupsForUserMembership(conn, userDN, groupSearchUserAttributeForFilterValue)
if err != nil {
return nil, err
}
@ -358,7 +370,7 @@ func (p *Provider) tlsConfig() (*tls.Config, error) {
return ptls.DefaultLDAP(rootCAs), nil
}
// A name for this upstream provider.
// GetName returns a name for this upstream provider.
func (p *Provider) GetName() string {
return p.c.Name
}
@ -367,7 +379,7 @@ func (p *Provider) GetResourceUID() types.UID {
return p.c.ResourceUID
}
// Return a URL which uniquely identifies this LDAP provider, e.g. "ldaps://host.example.com:1234?base=user-search-base".
// GetURL returns a URL which uniquely identifies this LDAP provider, e.g. "ldaps://host.example.com:1234?base=user-search-base".
// This URL is not used for connecting to the provider, but rather is used for creating a globally unique user
// identifier by being combined with the user's UID, since user UIDs are only unique within one provider.
func (p *Provider) GetURL() *url.URL {
@ -412,7 +424,7 @@ func (p *Provider) DryRunAuthenticateUser(ctx context.Context, username string,
return p.authenticateUserImpl(ctx, username, grantedScopes, endUserBindFunc)
}
// Authenticate an end user and return their mapped username, groups, and UID. Implements authenticators.UserAuthenticator.
// AuthenticateUser authenticates an end user and returns their mapped username, groups, and UID. Implements authenticators.UserAuthenticator.
func (p *Provider) AuthenticateUser(ctx context.Context, username, password string, grantedScopes []string) (*authenticators.Response, bool, error) {
endUserBindFunc := func(conn Conn, foundUserDN string) error {
return conn.Bind(foundUserDN, password)
@ -463,13 +475,13 @@ func (p *Provider) authenticateUserImpl(ctx context.Context, username string, gr
return response, true, nil
}
func (p *Provider) searchGroupsForUserDN(conn Conn, userDN string) ([]string, error) {
func (p *Provider) searchGroupsForUserMembership(conn Conn, userDN string, groupSearchUserAttributeForFilterValue string) ([]string, error) {
// If we do not have group search configured, skip this search.
if len(p.c.GroupSearch.Base) == 0 {
return []string{}, nil
}
searchResult, err := conn.SearchWithPaging(p.groupSearchRequest(userDN), groupSearchPageSize)
searchResult, err := conn.SearchWithPaging(p.groupSearchRequest(userDN, groupSearchUserAttributeForFilterValue), groupSearchPageSize)
if err != nil {
return nil, fmt.Errorf(`error searching for group memberships for user with DN %q: %w`, userDN, err)
}
@ -594,7 +606,15 @@ func (p *Provider) searchAndBindUser(conn Conn, username string, grantedScopes [
var mappedGroupNames []string
if slices.Contains(grantedScopes, oidcapi.ScopeGroups) {
mappedGroupNames, err = p.searchGroupsForUserDN(conn, userEntry.DN)
var groupSearchUserAttributeForFilterValue string
if p.useGroupSearchUserAttributeForFilter() {
groupSearchUserAttributeForFilterValue, err = p.getSearchResultAttributeValue(p.c.GroupSearch.UserAttributeForFilter, userEntry, username)
if err != nil {
return nil, err
}
}
mappedGroupNames, err = p.searchGroupsForUserMembership(conn, userEntry.DN, groupSearchUserAttributeForFilterValue)
if err != nil {
return nil, err
}
@ -668,7 +688,7 @@ func (p *Provider) userSearchRequest(username string) *ldap.SearchRequest {
}
}
func (p *Provider) groupSearchRequest(userDN string) *ldap.SearchRequest {
func (p *Provider) groupSearchRequest(userDN string, groupSearchUserAttributeForFilterValue string) *ldap.SearchRequest {
// See https://ldap.com/the-ldap-search-operation for general documentation of LDAP search options.
return &ldap.SearchRequest{
BaseDN: p.c.GroupSearch.Base,
@ -677,7 +697,7 @@ func (p *Provider) groupSearchRequest(userDN string) *ldap.SearchRequest {
SizeLimit: 0, // unlimited size because we will search with paging
TimeLimit: 90,
TypesOnly: false,
Filter: p.groupSearchFilter(userDN),
Filter: p.groupSearchFilter(userDN, groupSearchUserAttributeForFilterValue),
Attributes: p.groupSearchRequestedAttributes(),
Controls: nil, // nil because ldap.SearchWithPaging() will set the appropriate controls for us
}
@ -698,6 +718,11 @@ func (p *Provider) refreshUserSearchRequest(dn string) *ldap.SearchRequest {
}
}
func (p *Provider) useGroupSearchUserAttributeForFilter() bool {
return len(p.c.GroupSearch.UserAttributeForFilter) > 0 &&
p.c.GroupSearch.UserAttributeForFilter != distinguishedNameAttributeName
}
func (p *Provider) userSearchRequestedAttributes() []string {
attributes := make([]string, 0, len(p.c.RefreshAttributeChecks)+2)
if p.c.UserSearch.UsernameAttribute != distinguishedNameAttributeName {
@ -706,6 +731,9 @@ func (p *Provider) userSearchRequestedAttributes() []string {
if p.c.UserSearch.UIDAttribute != distinguishedNameAttributeName {
attributes = append(attributes, p.c.UserSearch.UIDAttribute)
}
if p.useGroupSearchUserAttributeForFilter() {
attributes = append(attributes, p.c.GroupSearch.UserAttributeForFilter)
}
for k := range p.c.RefreshAttributeChecks {
attributes = append(attributes, k)
}
@ -733,15 +761,20 @@ func (p *Provider) userSearchFilter(username string) string {
return interpolateSearchFilter(p.c.UserSearch.Filter, safeUsername)
}
func (p *Provider) groupSearchFilter(userDN string) string {
// The DN can contain characters that are considered special characters by LDAP searches, so it should be
// escaped before being included in the search filter to prevent bad search syntax.
// E.g. for the DN `CN=My User (Admin),OU=Users,OU=my,DC=my,DC=domain` we must escape the parens.
safeUserDN := p.escapeForSearchFilter(userDN)
if len(p.c.GroupSearch.Filter) == 0 {
return fmt.Sprintf("(member=%s)", safeUserDN)
func (p *Provider) groupSearchFilter(userDN string, groupSearchUserAttributeForFilterValue string) string {
valueToInterpolate := userDN
if p.useGroupSearchUserAttributeForFilter() {
// Instead of using the DN in placeholder substitution, use the value of the specified attribute.
valueToInterpolate = groupSearchUserAttributeForFilterValue
}
return interpolateSearchFilter(p.c.GroupSearch.Filter, safeUserDN)
// The value to interpolate can contain characters that are considered special characters by LDAP searches,
// so it should be escaped before being included in the search filter to prevent bad search syntax.
// E.g. for the DN `CN=My User (Admin),OU=Users,OU=my,DC=my,DC=domain` we must escape the parens.
escapedValueToInterpolate := p.escapeForSearchFilter(valueToInterpolate)
if len(p.c.GroupSearch.Filter) == 0 {
return fmt.Sprintf("(member=%s)", escapedValueToInterpolate)
}
return interpolateSearchFilter(p.c.GroupSearch.Filter, escapedValueToInterpolate)
}
func interpolateSearchFilter(filterFormat, valueToInterpolateIntoFilter string) string {

View File

@ -56,11 +56,13 @@ const (
testUserDNWithSpecialCharsEscaped = `user DN with \2a \5c special characters \28\29`
expectedGroupSearchPageSize = uint32(250)
testGroupSearchFilterInterpolationSpec = "(some-group-filter=%s-and-more-filter=%s)"
)
var (
testUserSearchFilterInterpolated = fmt.Sprintf("(some-user-filter=%s-and-more-filter=%s)", testUpstreamUsername, testUpstreamUsername)
testGroupSearchFilterInterpolated = fmt.Sprintf("(some-group-filter=%s-and-more-filter=%s)", testUserSearchResultDNValue, testUserSearchResultDNValue)
testGroupSearchFilterInterpolated = fmt.Sprintf(testGroupSearchFilterInterpolationSpec, testUserSearchResultDNValue, testUserSearchResultDNValue)
)
func TestEndUserAuthentication(t *testing.T) {
@ -714,6 +716,236 @@ func TestEndUserAuthentication(t *testing.T) {
},
wantError: testutil.WantExactErrorString("found 0 values for attribute \"some-attribute-to-check-during-refresh\" while searching for user \"some-upstream-username\", but expected 1 result"),
},
{
name: "when the UserAttributeForFilter is set to something other than dn",
username: testUpstreamUsername,
password: testUpstreamPassword,
providerConfig: providerConfig(func(p *ProviderConfig) {
p.GroupSearch.UserAttributeForFilter = "someUserAttrName"
}),
searchMocks: func(conn *mockldapconn.MockConn) {
conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1)
conn.EXPECT().Search(expectedUserSearch(func(r *ldap.SearchRequest) {
// need to additionally ask for the attribute when performing user search
r.Attributes = []string{testUserSearchUsernameAttribute, testUserSearchUIDAttribute, "someUserAttrName"}
})).Return(&ldap.SearchResult{
Entries: []*ldap.Entry{
{
DN: testUserSearchResultDNValue,
Attributes: []*ldap.EntryAttribute{
ldap.NewEntryAttribute(testUserSearchUsernameAttribute, []string{testUserSearchResultUsernameAttributeValue}),
ldap.NewEntryAttribute(testUserSearchUIDAttribute, []string{testUserSearchResultUIDAttributeValue}),
// additionally get back the attr from the user search
ldap.NewEntryAttribute("someUserAttrName", []string{"someUserAttrValue"}),
},
},
},
}, nil).Times(1)
conn.EXPECT().SearchWithPaging(expectedGroupSearch(func(r *ldap.SearchRequest) {
// need to interpolate the attr's value into the filter instead of interpolating the default (user's dn)
r.Filter = fmt.Sprintf(testGroupSearchFilterInterpolationSpec, "someUserAttrValue", "someUserAttrValue")
}), expectedGroupSearchPageSize).
Return(exampleGroupSearchResult, nil).Times(1)
conn.EXPECT().Close().Times(1)
},
bindEndUserMocks: func(conn *mockldapconn.MockConn) {
conn.EXPECT().Bind(testUserSearchResultDNValue, testUpstreamPassword).Times(1)
},
wantAuthResponse: expectedAuthResponse(nil),
},
{
name: "when the UserAttributeForFilter is set to something other than dn but groups scope is not granted so skips validating UserAttributeForFilter attribute value",
username: testUpstreamUsername,
password: testUpstreamPassword,
grantedScopes: []string{}, // no groups scope
providerConfig: providerConfig(func(p *ProviderConfig) {
p.GroupSearch.UserAttributeForFilter = "someUserAttrName"
}),
searchMocks: func(conn *mockldapconn.MockConn) {
conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1)
conn.EXPECT().Search(expectedUserSearch(func(r *ldap.SearchRequest) {
// need to additionally ask for the attribute when performing user search
r.Attributes = []string{testUserSearchUsernameAttribute, testUserSearchUIDAttribute, "someUserAttrName"}
})).Return(exampleUserSearchResult, nil).Times(1) // result does not contain someUserAttrName, but does not matter since group search is skipped
conn.EXPECT().Close().Times(1)
},
bindEndUserMocks: func(conn *mockldapconn.MockConn) {
conn.EXPECT().Bind(testUserSearchResultDNValue, testUpstreamPassword).Times(1)
},
wantAuthResponse: expectedAuthResponse(func(r *authenticators.Response) {
info := r.User.(*user.DefaultInfo)
info.Groups = nil
}),
},
{
name: "when the UserAttributeForFilter is set to something other than dn but that attribute is not returned by the user search",
username: testUpstreamUsername,
password: testUpstreamPassword,
providerConfig: providerConfig(func(p *ProviderConfig) {
p.GroupSearch.UserAttributeForFilter = "someUserAttrName"
}),
searchMocks: func(conn *mockldapconn.MockConn) {
conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1)
conn.EXPECT().Search(expectedUserSearch(func(r *ldap.SearchRequest) {
// need to additionally ask for the attribute when performing user search
r.Attributes = []string{testUserSearchUsernameAttribute, testUserSearchUIDAttribute, "someUserAttrName"}
})).Return(exampleUserSearchResult, nil).Times(1)
conn.EXPECT().Close().Times(1)
},
wantError: testutil.WantExactErrorString("found 0 values for attribute \"someUserAttrName\" while searching for user \"some-upstream-username\", but expected 1 result"),
},
{
name: "when the UserAttributeForFilter is set to something other than dn but that attribute is returned by the user search with an empty value",
username: testUpstreamUsername,
password: testUpstreamPassword,
providerConfig: providerConfig(func(p *ProviderConfig) {
p.GroupSearch.UserAttributeForFilter = "someUserAttrName"
}),
searchMocks: func(conn *mockldapconn.MockConn) {
conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1)
conn.EXPECT().Search(expectedUserSearch(func(r *ldap.SearchRequest) {
// need to additionally ask for the attribute when performing user search
r.Attributes = []string{testUserSearchUsernameAttribute, testUserSearchUIDAttribute, "someUserAttrName"}
})).Return(&ldap.SearchResult{
Entries: []*ldap.Entry{
{
DN: testUserSearchResultDNValue,
Attributes: []*ldap.EntryAttribute{
ldap.NewEntryAttribute(testUserSearchUsernameAttribute, []string{testUserSearchResultUsernameAttributeValue}),
ldap.NewEntryAttribute(testUserSearchUIDAttribute, []string{testUserSearchResultUIDAttributeValue}),
// additionally get back the attr from the user search
ldap.NewEntryAttribute("someUserAttrName", []string{""}), // empty value!
},
},
},
}, nil).Times(1)
conn.EXPECT().Close().Times(1)
},
wantError: testutil.WantExactErrorString("found empty value for attribute \"someUserAttrName\" while searching for user \"some-upstream-username\", but expected value to be non-empty"),
},
{
name: "when the UserAttributeForFilter is set to something other than dn but that attribute is returned by the user search with multiple values",
username: testUpstreamUsername,
password: testUpstreamPassword,
providerConfig: providerConfig(func(p *ProviderConfig) {
p.GroupSearch.UserAttributeForFilter = "someUserAttrName"
}),
searchMocks: func(conn *mockldapconn.MockConn) {
conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1)
conn.EXPECT().Search(expectedUserSearch(func(r *ldap.SearchRequest) {
// need to additionally ask for the attribute when performing user search
r.Attributes = []string{testUserSearchUsernameAttribute, testUserSearchUIDAttribute, "someUserAttrName"}
})).Return(&ldap.SearchResult{
Entries: []*ldap.Entry{
{
DN: testUserSearchResultDNValue,
Attributes: []*ldap.EntryAttribute{
ldap.NewEntryAttribute(testUserSearchUsernameAttribute, []string{testUserSearchResultUsernameAttributeValue}),
ldap.NewEntryAttribute(testUserSearchUIDAttribute, []string{testUserSearchResultUIDAttributeValue}),
// additionally get back the attr from the user search
ldap.NewEntryAttribute("someUserAttrName", []string{"someUserAttrValue1", "someUserAttrValue2"}), // oops, multiple values!
},
},
},
}, nil).Times(1)
conn.EXPECT().Close().Times(1)
},
wantError: testutil.WantExactErrorString("found 2 values for attribute \"someUserAttrName\" while searching for user \"some-upstream-username\", but expected 1 result"),
},
{
name: "when the UserAttributeForFilter is set to dn, it should act the same as when it is not set",
username: testUpstreamUsername,
password: testUpstreamPassword,
providerConfig: providerConfig(func(p *ProviderConfig) {
p.GroupSearch.UserAttributeForFilter = "dn"
}),
searchMocks: func(conn *mockldapconn.MockConn) {
conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1)
conn.EXPECT().Search(expectedUserSearch(nil)).Return(exampleUserSearchResult, nil).Times(1)
conn.EXPECT().SearchWithPaging(expectedGroupSearch(nil), expectedGroupSearchPageSize).
Return(exampleGroupSearchResult, nil).Times(1)
conn.EXPECT().Close().Times(1)
},
bindEndUserMocks: func(conn *mockldapconn.MockConn) {
conn.EXPECT().Bind(testUserSearchResultDNValue, testUpstreamPassword).Times(1)
},
wantAuthResponse: expectedAuthResponse(nil),
},
{
name: "when the UserAttributeForFilter is set to something other than dn and the value of that attr contains special characters which need to be escaped for an LDAP filter",
username: testUpstreamUsername,
password: testUpstreamPassword,
providerConfig: providerConfig(func(p *ProviderConfig) {
p.GroupSearch.UserAttributeForFilter = "someUserAttrName"
}),
searchMocks: func(conn *mockldapconn.MockConn) {
conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1)
conn.EXPECT().Search(expectedUserSearch(func(r *ldap.SearchRequest) {
// need to additionally ask for the attribute when performing user search
r.Attributes = []string{testUserSearchUsernameAttribute, testUserSearchUIDAttribute, "someUserAttrName"}
})).Return(&ldap.SearchResult{
Entries: []*ldap.Entry{
{
DN: testUserSearchResultDNValue,
Attributes: []*ldap.EntryAttribute{
ldap.NewEntryAttribute(testUserSearchUsernameAttribute, []string{testUserSearchResultUsernameAttributeValue}),
ldap.NewEntryAttribute(testUserSearchUIDAttribute, []string{testUserSearchResultUIDAttributeValue}),
// additionally get back the attr from the user search
ldap.NewEntryAttribute("someUserAttrName", []string{"someUserAttrValue&(abc)"}),
},
},
},
}, nil).Times(1)
conn.EXPECT().SearchWithPaging(expectedGroupSearch(func(r *ldap.SearchRequest) {
// need to interpolate the attr's value into the filter instead of interpolating the default (user's dn)
r.Filter = fmt.Sprintf(testGroupSearchFilterInterpolationSpec, `someUserAttrValue&\28abc\29`, `someUserAttrValue&\28abc\29`)
}), expectedGroupSearchPageSize).
Return(exampleGroupSearchResult, nil).Times(1)
conn.EXPECT().Close().Times(1)
},
bindEndUserMocks: func(conn *mockldapconn.MockConn) {
conn.EXPECT().Bind(testUserSearchResultDNValue, testUpstreamPassword).Times(1)
},
wantAuthResponse: expectedAuthResponse(nil),
},
{
name: "when the UserAttributeForFilter is set to something other than dn but the group search filter is not set",
username: testUpstreamUsername,
password: testUpstreamPassword,
providerConfig: providerConfig(func(p *ProviderConfig) {
p.GroupSearch.Filter = ""
p.GroupSearch.UserAttributeForFilter = "someUserAttrName"
}),
searchMocks: func(conn *mockldapconn.MockConn) {
conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1)
conn.EXPECT().Search(expectedUserSearch(func(r *ldap.SearchRequest) {
// need to additionally ask for the attribute when performing user search
r.Attributes = []string{testUserSearchUsernameAttribute, testUserSearchUIDAttribute, "someUserAttrName"}
})).Return(&ldap.SearchResult{
Entries: []*ldap.Entry{
{
DN: testUserSearchResultDNValue,
Attributes: []*ldap.EntryAttribute{
ldap.NewEntryAttribute(testUserSearchUsernameAttribute, []string{testUserSearchResultUsernameAttributeValue}),
ldap.NewEntryAttribute(testUserSearchUIDAttribute, []string{testUserSearchResultUIDAttributeValue}),
// additionally get back the attr from the user search
ldap.NewEntryAttribute("someUserAttrName", []string{"someUserAttrValue&(abc)"}),
},
},
},
}, nil).Times(1)
conn.EXPECT().SearchWithPaging(expectedGroupSearch(func(r *ldap.SearchRequest) {
// need to interpolate the attr's value into the filter instead of interpolating the default (user's dn)
r.Filter = `(member=someUserAttrValue&\28abc\29)` // note that "member={}" is the default group search filter
}), expectedGroupSearchPageSize).
Return(exampleGroupSearchResult, nil).Times(1)
conn.EXPECT().Close().Times(1)
},
bindEndUserMocks: func(conn *mockldapconn.MockConn) {
conn.EXPECT().Bind(testUserSearchResultDNValue, testUpstreamPassword).Times(1)
},
wantAuthResponse: expectedAuthResponse(nil),
},
{
name: "when dial fails",
username: testUpstreamUsername,
@ -1486,7 +1718,8 @@ func TestUpstreamRefresh(t *testing.T) {
}),
setupMocks: func(conn *mockldapconn.MockConn) {
conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1)
conn.EXPECT().Search(expectedUserSearch(nil)).Return(happyPathUserSearchResult, nil).Times(1) // note that group search is not expected
conn.EXPECT().Search(expectedUserSearch(nil)).Return(happyPathUserSearchResult, nil).Times(1)
// note that group search is not expected
conn.EXPECT().Close().Times(1)
},
wantGroups: nil, // do not update groups
@ -1497,11 +1730,240 @@ func TestUpstreamRefresh(t *testing.T) {
setupMocks: func(conn *mockldapconn.MockConn) {
conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1)
conn.EXPECT().Search(expectedUserSearch(nil)).Return(happyPathUserSearchResult, nil).Times(1)
// note that group search is not expected
conn.EXPECT().Close().Times(1)
},
grantedScopes: []string{},
wantGroups: nil,
},
{
name: "happy path where group search is configured but skipGroupRefresh is set, when the UserAttributeForFilter is set to something other than dn, still skips group refresh, and skips validating UserAttributeForFilter attribute value",
providerConfig: providerConfig(func(p *ProviderConfig) {
p.GroupSearch.SkipGroupRefresh = true
p.GroupSearch.UserAttributeForFilter = "someUserAttrName"
}),
setupMocks: func(conn *mockldapconn.MockConn) {
conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1)
conn.EXPECT().Search(expectedUserSearch(func(r *ldap.SearchRequest) {
// need to additionally ask for the attribute when performing user search
r.Attributes = []string{testUserSearchUsernameAttribute, testUserSearchUIDAttribute, "someUserAttrName", pwdLastSetAttribute}
})).Return(happyPathUserSearchResult, nil).Times(1)
// note that group search is not expected
conn.EXPECT().Close().Times(1)
},
wantGroups: nil, // do not update groups
},
{
name: "happy path where group search is configured but groups scope isn't included, when the UserAttributeForFilter is set to something other than dn, still skips group refresh, and skips validating UserAttributeForFilter attribute value",
providerConfig: providerConfig(func(p *ProviderConfig) {
p.GroupSearch.UserAttributeForFilter = "someUserAttrName"
}),
setupMocks: func(conn *mockldapconn.MockConn) {
conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1)
conn.EXPECT().Search(expectedUserSearch(func(r *ldap.SearchRequest) {
// need to additionally ask for the attribute when performing user search
r.Attributes = []string{testUserSearchUsernameAttribute, testUserSearchUIDAttribute, "someUserAttrName", pwdLastSetAttribute}
})).Return(happyPathUserSearchResult, nil).Times(1)
// note that group search is not expected
conn.EXPECT().Close().Times(1)
},
grantedScopes: []string{},
wantGroups: nil,
},
{
name: "happy path when the UserAttributeForFilter is set to something other than dn",
providerConfig: providerConfig(func(p *ProviderConfig) {
p.GroupSearch.UserAttributeForFilter = "someUserAttrName"
}),
setupMocks: func(conn *mockldapconn.MockConn) {
conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1)
conn.EXPECT().Search(expectedUserSearch(func(r *ldap.SearchRequest) {
// need to additionally ask for the attribute when performing user search
r.Attributes = []string{testUserSearchUsernameAttribute, testUserSearchUIDAttribute, "someUserAttrName", pwdLastSetAttribute}
})).Return(&ldap.SearchResult{
Entries: []*ldap.Entry{
{
DN: testUserSearchResultDNValue,
Attributes: []*ldap.EntryAttribute{
ldap.NewEntryAttribute(testUserSearchUsernameAttribute, []string{testUserSearchResultUsernameAttributeValue}),
{
Name: testUserSearchUIDAttribute,
ByteValues: [][]byte{[]byte(testUserSearchResultUIDAttributeValue)},
},
{
Name: pwdLastSetAttribute,
Values: []string{"132801740800000000"},
ByteValues: [][]byte{[]byte("132801740800000000")},
},
// additionally get back the attr from the user search
ldap.NewEntryAttribute("someUserAttrName", []string{"someUserAttrValue"}),
},
},
},
}, nil).Times(1)
conn.EXPECT().SearchWithPaging(expectedGroupSearch(func(r *ldap.SearchRequest) {
// need to interpolate the attr's value into the filter instead of interpolating the default (user's dn)
r.Filter = fmt.Sprintf(testGroupSearchFilterInterpolationSpec, "someUserAttrValue", "someUserAttrValue")
}), expectedGroupSearchPageSize).Return(happyPathGroupSearchResult, nil).Times(1)
conn.EXPECT().Close().Times(1)
},
wantGroups: []string{testGroupSearchResultGroupNameAttributeValue1, testGroupSearchResultGroupNameAttributeValue2},
},
{
name: "happy path when the UserAttributeForFilter is set to something other than dn but the group search filter is not set",
providerConfig: providerConfig(func(p *ProviderConfig) {
p.GroupSearch.Filter = ""
p.GroupSearch.UserAttributeForFilter = "someUserAttrName"
}),
setupMocks: func(conn *mockldapconn.MockConn) {
conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1)
conn.EXPECT().Search(expectedUserSearch(func(r *ldap.SearchRequest) {
// need to additionally ask for the attribute when performing user search
r.Attributes = []string{testUserSearchUsernameAttribute, testUserSearchUIDAttribute, "someUserAttrName", pwdLastSetAttribute}
})).Return(&ldap.SearchResult{
Entries: []*ldap.Entry{
{
DN: testUserSearchResultDNValue,
Attributes: []*ldap.EntryAttribute{
ldap.NewEntryAttribute(testUserSearchUsernameAttribute, []string{testUserSearchResultUsernameAttributeValue}),
{
Name: testUserSearchUIDAttribute,
ByteValues: [][]byte{[]byte(testUserSearchResultUIDAttributeValue)},
},
{
Name: pwdLastSetAttribute,
Values: []string{"132801740800000000"},
ByteValues: [][]byte{[]byte("132801740800000000")},
},
// additionally get back the attr from the user search
ldap.NewEntryAttribute("someUserAttrName", []string{"someUserAttrValue&(abc)"}),
},
},
},
}, nil).Times(1)
conn.EXPECT().SearchWithPaging(expectedGroupSearch(func(r *ldap.SearchRequest) {
// need to interpolate the attr's value into the filter instead of interpolating the default (user's dn)
r.Filter = `(member=someUserAttrValue&\28abc\29)` // member={} is the default, and special chars are escaped
}), expectedGroupSearchPageSize).Return(happyPathGroupSearchResult, nil).Times(1)
conn.EXPECT().Close().Times(1)
},
wantGroups: []string{testGroupSearchResultGroupNameAttributeValue1, testGroupSearchResultGroupNameAttributeValue2},
},
{
name: "when the UserAttributeForFilter is set to something other than dn but that attribute is not returned by the user search",
providerConfig: providerConfig(func(p *ProviderConfig) {
p.GroupSearch.UserAttributeForFilter = "someUserAttrName"
}),
setupMocks: func(conn *mockldapconn.MockConn) {
conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1)
conn.EXPECT().Search(expectedUserSearch(func(r *ldap.SearchRequest) {
// need to additionally ask for the attribute when performing user search
r.Attributes = []string{testUserSearchUsernameAttribute, testUserSearchUIDAttribute, "someUserAttrName", pwdLastSetAttribute}
})).Return(&ldap.SearchResult{
Entries: []*ldap.Entry{
{
DN: testUserSearchResultDNValue,
Attributes: []*ldap.EntryAttribute{
ldap.NewEntryAttribute(testUserSearchUsernameAttribute, []string{testUserSearchResultUsernameAttributeValue}),
{
Name: testUserSearchUIDAttribute,
ByteValues: [][]byte{[]byte(testUserSearchResultUIDAttributeValue)},
},
{
Name: pwdLastSetAttribute,
Values: []string{"132801740800000000"},
ByteValues: [][]byte{[]byte("132801740800000000")},
},
// did not return "someUserAttrName" attribute
},
},
},
}, nil).Times(1)
conn.EXPECT().Close().Times(1)
},
wantErr: "found 0 values for attribute \"someUserAttrName\" while searching for user \"some-upstream-username-value\", but expected 1 result",
},
{
name: "when the UserAttributeForFilter is set to something other than dn but that attribute is returned by the user search with an empty value",
providerConfig: providerConfig(func(p *ProviderConfig) {
p.GroupSearch.UserAttributeForFilter = "someUserAttrName"
}),
setupMocks: func(conn *mockldapconn.MockConn) {
conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1)
conn.EXPECT().Search(expectedUserSearch(func(r *ldap.SearchRequest) {
// need to additionally ask for the attribute when performing user search
r.Attributes = []string{testUserSearchUsernameAttribute, testUserSearchUIDAttribute, "someUserAttrName", pwdLastSetAttribute}
})).Return(&ldap.SearchResult{
Entries: []*ldap.Entry{
{
DN: testUserSearchResultDNValue,
Attributes: []*ldap.EntryAttribute{
ldap.NewEntryAttribute(testUserSearchUsernameAttribute, []string{testUserSearchResultUsernameAttributeValue}),
{
Name: testUserSearchUIDAttribute,
ByteValues: [][]byte{[]byte(testUserSearchResultUIDAttributeValue)},
},
{
Name: pwdLastSetAttribute,
Values: []string{"132801740800000000"},
ByteValues: [][]byte{[]byte("132801740800000000")},
},
ldap.NewEntryAttribute("someUserAttrName", []string{""}),
},
},
},
}, nil).Times(1)
conn.EXPECT().Close().Times(1)
},
wantErr: "found empty value for attribute \"someUserAttrName\" while searching for user \"some-upstream-username-value\", but expected value to be non-empty",
},
{
name: "when the UserAttributeForFilter is set to something other than dn but that attribute is returned by the user search with a multiple values",
providerConfig: providerConfig(func(p *ProviderConfig) {
p.GroupSearch.UserAttributeForFilter = "someUserAttrName"
}),
setupMocks: func(conn *mockldapconn.MockConn) {
conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1)
conn.EXPECT().Search(expectedUserSearch(func(r *ldap.SearchRequest) {
// need to additionally ask for the attribute when performing user search
r.Attributes = []string{testUserSearchUsernameAttribute, testUserSearchUIDAttribute, "someUserAttrName", pwdLastSetAttribute}
})).Return(&ldap.SearchResult{
Entries: []*ldap.Entry{
{
DN: testUserSearchResultDNValue,
Attributes: []*ldap.EntryAttribute{
ldap.NewEntryAttribute(testUserSearchUsernameAttribute, []string{testUserSearchResultUsernameAttributeValue}),
{
Name: testUserSearchUIDAttribute,
ByteValues: [][]byte{[]byte(testUserSearchResultUIDAttributeValue)},
},
{
Name: pwdLastSetAttribute,
Values: []string{"132801740800000000"},
ByteValues: [][]byte{[]byte("132801740800000000")},
},
ldap.NewEntryAttribute("someUserAttrName", []string{"someUserAttrValue1", "someUserAttrValue2"}),
},
},
},
}, nil).Times(1)
conn.EXPECT().Close().Times(1)
},
wantErr: "found 2 values for attribute \"someUserAttrName\" while searching for user \"some-upstream-username-value\", but expected 1 result",
},
{
name: "happy path when the UserAttributeForFilter is set to dn, it should act the same as when it is not set",
providerConfig: providerConfig(func(p *ProviderConfig) {
p.GroupSearch.UserAttributeForFilter = "dn"
}),
setupMocks: func(conn *mockldapconn.MockConn) {
conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1)
conn.EXPECT().Search(expectedUserSearch(nil)).Return(happyPathUserSearchResult, nil).Times(1)
conn.EXPECT().SearchWithPaging(expectedGroupSearch(nil), expectedGroupSearchPageSize).Return(happyPathGroupSearchResult, nil).Times(1)
conn.EXPECT().Close().Times(1)
},
wantGroups: []string{testGroupSearchResultGroupNameAttributeValue1, testGroupSearchResultGroupNameAttributeValue2},
},
{
name: "error where dial fails",
providerConfig: providerConfig(nil),