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:
parent
bad5e60a8e
commit
c187474499
@ -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
|
||||
|
@ -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,
|
||||
}
|
||||
|
@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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),
|
||||
|
Loading…
Reference in New Issue
Block a user