Merge pull request #975 from vmware-tanzu/upstream-ldap-group-refresh
Inline upstream ldap group refresh
This commit is contained in:
commit
c7aaa69b4b
@ -108,7 +108,7 @@ type UpstreamLDAPIdentityProviderI interface {
|
||||
authenticators.UserAuthenticator
|
||||
|
||||
// PerformRefresh performs a refresh against the upstream LDAP identity provider
|
||||
PerformRefresh(ctx context.Context, storedRefreshAttributes StoredRefreshAttributes) error
|
||||
PerformRefresh(ctx context.Context, storedRefreshAttributes StoredRefreshAttributes) (groups []string, err error)
|
||||
}
|
||||
|
||||
type StoredRefreshAttributes struct {
|
||||
|
@ -301,7 +301,7 @@ func upstreamLDAPRefresh(ctx context.Context, providerCache oidc.UpstreamIdentit
|
||||
return errorsx.WithStack(errMissingUpstreamSessionInternalError)
|
||||
}
|
||||
// run PerformRefresh
|
||||
err = p.PerformRefresh(ctx, provider.StoredRefreshAttributes{
|
||||
groups, err := p.PerformRefresh(ctx, provider.StoredRefreshAttributes{
|
||||
Username: username,
|
||||
Subject: subject,
|
||||
DN: dn,
|
||||
@ -312,6 +312,8 @@ func upstreamLDAPRefresh(ctx context.Context, providerCache oidc.UpstreamIdentit
|
||||
"Upstream refresh failed.").WithWrap(err).
|
||||
WithDebugf("provider name: %q, provider type: %q", s.ProviderName, s.ProviderType))
|
||||
}
|
||||
// Replace the old value with the new value.
|
||||
session.Fosite.Claims.Extra[oidc.DownstreamGroupsClaim] = groups
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@ -1339,6 +1339,60 @@ func TestRefreshGrant(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "happy path refresh grant when the upstream refresh returns new group memberships from LDAP, it updates groups",
|
||||
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&oidctestutil.TestUpstreamLDAPIdentityProvider{
|
||||
Name: ldapUpstreamName,
|
||||
ResourceUID: ldapUpstreamResourceUID,
|
||||
URL: ldapUpstreamURL,
|
||||
PerformRefreshGroups: []string{"new-group1", "new-group2", "new-group3"},
|
||||
}),
|
||||
authcodeExchange: authcodeExchangeInputs{
|
||||
modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access") },
|
||||
customSessionData: happyLDAPCustomSessionData,
|
||||
want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(
|
||||
happyLDAPCustomSessionData,
|
||||
),
|
||||
},
|
||||
refreshRequest: refreshRequestInputs{
|
||||
want: tokenEndpointResponseExpectedValues{
|
||||
wantStatus: http.StatusOK,
|
||||
wantSuccessBodyFields: []string{"refresh_token", "access_token", "id_token", "token_type", "expires_in", "scope"},
|
||||
wantRequestedScopes: []string{"openid", "offline_access"},
|
||||
wantGrantedScopes: []string{"openid", "offline_access"},
|
||||
wantGroups: []string{"new-group1", "new-group2", "new-group3"},
|
||||
wantUpstreamRefreshCall: happyLDAPUpstreamRefreshCall(),
|
||||
wantCustomSessionDataStored: happyLDAPCustomSessionData,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "happy path refresh grant when the upstream refresh returns empty list of group memberships from LDAP, it updates groups to an empty list",
|
||||
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&oidctestutil.TestUpstreamLDAPIdentityProvider{
|
||||
Name: ldapUpstreamName,
|
||||
ResourceUID: ldapUpstreamResourceUID,
|
||||
URL: ldapUpstreamURL,
|
||||
PerformRefreshGroups: []string{},
|
||||
}),
|
||||
authcodeExchange: authcodeExchangeInputs{
|
||||
modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access") },
|
||||
customSessionData: happyLDAPCustomSessionData,
|
||||
want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(
|
||||
happyLDAPCustomSessionData,
|
||||
),
|
||||
},
|
||||
refreshRequest: refreshRequestInputs{
|
||||
want: tokenEndpointResponseExpectedValues{
|
||||
wantStatus: http.StatusOK,
|
||||
wantSuccessBodyFields: []string{"refresh_token", "access_token", "id_token", "token_type", "expires_in", "scope"},
|
||||
wantRequestedScopes: []string{"openid", "offline_access"},
|
||||
wantGrantedScopes: []string{"openid", "offline_access"},
|
||||
wantGroups: []string{},
|
||||
wantUpstreamRefreshCall: happyLDAPUpstreamRefreshCall(),
|
||||
wantCustomSessionDataStored: happyLDAPCustomSessionData,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "error from refresh grant when the upstream refresh does not return new group memberships from the merged ID token and userinfo results by returning group claim with illegal nil value",
|
||||
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(
|
||||
@ -1967,9 +2021,10 @@ func TestRefreshGrant(t *testing.T) {
|
||||
{
|
||||
name: "upstream ldap refresh happy path",
|
||||
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&oidctestutil.TestUpstreamLDAPIdentityProvider{
|
||||
Name: ldapUpstreamName,
|
||||
ResourceUID: ldapUpstreamResourceUID,
|
||||
URL: ldapUpstreamURL,
|
||||
Name: ldapUpstreamName,
|
||||
ResourceUID: ldapUpstreamResourceUID,
|
||||
URL: ldapUpstreamURL,
|
||||
PerformRefreshGroups: goodGroups,
|
||||
}),
|
||||
authcodeExchange: authcodeExchangeInputs{
|
||||
modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access") },
|
||||
@ -1987,9 +2042,10 @@ func TestRefreshGrant(t *testing.T) {
|
||||
{
|
||||
name: "upstream active directory refresh happy path",
|
||||
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&oidctestutil.TestUpstreamLDAPIdentityProvider{
|
||||
Name: activeDirectoryUpstreamName,
|
||||
ResourceUID: activeDirectoryUpstreamResourceUID,
|
||||
URL: ldapUpstreamURL,
|
||||
Name: activeDirectoryUpstreamName,
|
||||
ResourceUID: activeDirectoryUpstreamResourceUID,
|
||||
URL: ldapUpstreamURL,
|
||||
PerformRefreshGroups: goodGroups,
|
||||
}),
|
||||
authcodeExchange: authcodeExchangeInputs{
|
||||
modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access") },
|
||||
|
@ -101,6 +101,7 @@ type TestUpstreamLDAPIdentityProvider struct {
|
||||
performRefreshCallCount int
|
||||
performRefreshArgs []*PerformRefreshArgs
|
||||
PerformRefreshErr error
|
||||
PerformRefreshGroups []string
|
||||
}
|
||||
|
||||
var _ provider.UpstreamLDAPIdentityProviderI = &TestUpstreamLDAPIdentityProvider{}
|
||||
@ -121,7 +122,7 @@ func (u *TestUpstreamLDAPIdentityProvider) GetURL() *url.URL {
|
||||
return u.URL
|
||||
}
|
||||
|
||||
func (u *TestUpstreamLDAPIdentityProvider) PerformRefresh(ctx context.Context, storedRefreshAttributes provider.StoredRefreshAttributes) error {
|
||||
func (u *TestUpstreamLDAPIdentityProvider) PerformRefresh(ctx context.Context, storedRefreshAttributes provider.StoredRefreshAttributes) ([]string, error) {
|
||||
if u.performRefreshArgs == nil {
|
||||
u.performRefreshArgs = make([]*PerformRefreshArgs, 0)
|
||||
}
|
||||
@ -133,9 +134,9 @@ func (u *TestUpstreamLDAPIdentityProvider) PerformRefresh(ctx context.Context, s
|
||||
ExpectedSubject: storedRefreshAttributes.Subject,
|
||||
})
|
||||
if u.PerformRefreshErr != nil {
|
||||
return u.PerformRefreshErr
|
||||
return nil, u.PerformRefreshErr
|
||||
}
|
||||
return nil
|
||||
return u.PerformRefreshGroups, nil
|
||||
}
|
||||
|
||||
func (u *TestUpstreamLDAPIdentityProvider) PerformRefreshCallCount() int {
|
||||
|
@ -13,12 +13,12 @@ import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/url"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-ldap/ldap/v3"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
"k8s.io/apiserver/pkg/authentication/user"
|
||||
"k8s.io/utils/trace"
|
||||
|
||||
@ -170,61 +170,11 @@ func (p *Provider) GetConfig() ProviderConfig {
|
||||
return p.c
|
||||
}
|
||||
|
||||
func (p *Provider) PerformRefresh(ctx context.Context, storedRefreshAttributes provider.StoredRefreshAttributes) error {
|
||||
func (p *Provider) PerformRefresh(ctx context.Context, storedRefreshAttributes provider.StoredRefreshAttributes) ([]string, error) {
|
||||
t := trace.FromContext(ctx).Nest("slow ldap refresh attempt", trace.Field{Key: "providerName", Value: p.GetName()})
|
||||
defer t.LogIfLong(500 * time.Millisecond) // to help users debug slow LDAP searches
|
||||
userDN := storedRefreshAttributes.DN
|
||||
|
||||
searchResult, err := p.performRefresh(ctx, userDN)
|
||||
if err != nil {
|
||||
p.traceRefreshFailure(t, err)
|
||||
return err
|
||||
}
|
||||
|
||||
// if any more or less than one entry, error.
|
||||
// we don't need to worry about logging this because we know it's a dn.
|
||||
if len(searchResult.Entries) != 1 {
|
||||
return fmt.Errorf(`searching for user %q resulted in %d search results, but expected 1 result`,
|
||||
userDN, len(searchResult.Entries),
|
||||
)
|
||||
}
|
||||
|
||||
userEntry := searchResult.Entries[0]
|
||||
if len(userEntry.DN) == 0 {
|
||||
return fmt.Errorf(`searching for user with original DN %q resulted in search result without DN`, userDN)
|
||||
}
|
||||
|
||||
newUsername, err := p.getSearchResultAttributeValue(p.c.UserSearch.UsernameAttribute, userEntry, userDN)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if newUsername != storedRefreshAttributes.Username {
|
||||
return fmt.Errorf(`searching for user %q returned a different username than the previous value. expected: %q, actual: %q`,
|
||||
userDN, storedRefreshAttributes.Username, newUsername,
|
||||
)
|
||||
}
|
||||
|
||||
newUID, err := p.getSearchResultAttributeRawValueEncoded(p.c.UserSearch.UIDAttribute, userEntry, userDN)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
newSubject := downstreamsession.DownstreamLDAPSubject(newUID, *p.GetURL())
|
||||
if newSubject != storedRefreshAttributes.Subject {
|
||||
return fmt.Errorf(`searching for user %q produced a different subject than the previous value. expected: %q, actual: %q`, userDN, storedRefreshAttributes.Subject, newSubject)
|
||||
}
|
||||
for attribute, validateFunc := range p.c.RefreshAttributeChecks {
|
||||
err = validateFunc(userEntry, storedRefreshAttributes)
|
||||
if err != nil {
|
||||
return fmt.Errorf(`validation for attribute %q failed during upstream refresh: %w`, attribute, err)
|
||||
}
|
||||
}
|
||||
// we checked that the user still exists and their information is the same, so just return.
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Provider) performRefresh(ctx context.Context, userDN string) (*ldap.SearchResult, error) {
|
||||
search := p.refreshUserSearchRequest(userDN)
|
||||
|
||||
conn, err := p.dial(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(`error dialing host %q: %w`, p.c.Host, err)
|
||||
@ -236,6 +186,60 @@ func (p *Provider) performRefresh(ctx context.Context, userDN string) (*ldap.Sea
|
||||
return nil, fmt.Errorf(`error binding as %q before user search: %w`, p.c.BindUsername, err)
|
||||
}
|
||||
|
||||
searchResult, err := p.performUserRefreshSearch(conn, userDN)
|
||||
if err != nil {
|
||||
p.traceRefreshFailure(t, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// if any more or less than one entry, error.
|
||||
// we don't need to worry about logging this because we know it's a dn.
|
||||
if len(searchResult.Entries) != 1 {
|
||||
return nil, fmt.Errorf(`searching for user %q resulted in %d search results, but expected 1 result`,
|
||||
userDN, len(searchResult.Entries),
|
||||
)
|
||||
}
|
||||
|
||||
userEntry := searchResult.Entries[0]
|
||||
if len(userEntry.DN) == 0 {
|
||||
return nil, fmt.Errorf(`searching for user with original DN %q resulted in search result without DN`, userDN)
|
||||
}
|
||||
|
||||
newUsername, err := p.getSearchResultAttributeValue(p.c.UserSearch.UsernameAttribute, userEntry, userDN)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if newUsername != storedRefreshAttributes.Username {
|
||||
return nil, fmt.Errorf(`searching for user %q returned a different username than the previous value. expected: %q, actual: %q`,
|
||||
userDN, storedRefreshAttributes.Username, newUsername,
|
||||
)
|
||||
}
|
||||
|
||||
newUID, err := p.getSearchResultAttributeRawValueEncoded(p.c.UserSearch.UIDAttribute, userEntry, userDN)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
newSubject := downstreamsession.DownstreamLDAPSubject(newUID, *p.GetURL())
|
||||
if newSubject != storedRefreshAttributes.Subject {
|
||||
return nil, fmt.Errorf(`searching for user %q produced a different subject than the previous value. expected: %q, actual: %q`, userDN, storedRefreshAttributes.Subject, newSubject)
|
||||
}
|
||||
for attribute, validateFunc := range p.c.RefreshAttributeChecks {
|
||||
err = validateFunc(userEntry, storedRefreshAttributes)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(`validation for attribute %q failed during upstream refresh: %w`, attribute, err)
|
||||
}
|
||||
}
|
||||
|
||||
mappedGroupNames, err := p.searchGroupsForUserDN(conn, userDN)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return mappedGroupNames, nil
|
||||
}
|
||||
|
||||
func (p *Provider) performUserRefreshSearch(conn Conn, userDN string) (*ldap.SearchResult, error) {
|
||||
search := p.refreshUserSearchRequest(userDN)
|
||||
|
||||
searchResult, err := conn.Search(search)
|
||||
|
||||
if err != nil {
|
||||
@ -445,6 +449,11 @@ func (p *Provider) authenticateUserImpl(ctx context.Context, username string, bi
|
||||
}
|
||||
|
||||
func (p *Provider) searchGroupsForUserDN(conn Conn, userDN 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)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(`error searching for group memberships for user with DN %q: %w`, userDN, err)
|
||||
@ -455,7 +464,7 @@ func (p *Provider) searchGroupsForUserDN(conn Conn, userDN string) ([]string, er
|
||||
groupAttributeName = distinguishedNameAttributeName
|
||||
}
|
||||
|
||||
var groups []string
|
||||
groups := []string{}
|
||||
entries:
|
||||
for _, groupEntry := range searchResult.Entries {
|
||||
if len(groupEntry.DN) == 0 {
|
||||
@ -476,8 +485,9 @@ entries:
|
||||
}
|
||||
groups = append(groups, mappedGroupName)
|
||||
}
|
||||
|
||||
return groups, nil
|
||||
// de-duplicate the list of groups by turning it into a set,
|
||||
// then turn it back into a sorted list.
|
||||
return sets.NewString(groups...).List(), nil
|
||||
}
|
||||
|
||||
func (p *Provider) validateConfig() error {
|
||||
@ -567,14 +577,10 @@ func (p *Provider) searchAndBindUser(conn Conn, username string, bindFunc func(c
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var mappedGroupNames []string
|
||||
if len(p.c.GroupSearch.Base) > 0 {
|
||||
mappedGroupNames, err = p.searchGroupsForUserDN(conn, userEntry.DN)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
mappedGroupNames, err := p.searchGroupsForUserDN(conn, userEntry.DN)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sort.Strings(mappedGroupNames)
|
||||
|
||||
mappedRefreshAttributes := make(map[string]string)
|
||||
for k := range p.c.RefreshAttributeChecks {
|
||||
|
@ -237,6 +237,33 @@ func TestEndUserAuthentication(t *testing.T) {
|
||||
},
|
||||
wantAuthResponse: expectedAuthResponse(nil),
|
||||
},
|
||||
{
|
||||
name: "when the group search has an override func",
|
||||
username: testUpstreamUsername,
|
||||
password: testUpstreamPassword,
|
||||
providerConfig: providerConfig(func(p *ProviderConfig) {
|
||||
p.GroupAttributeParsingOverrides = map[string]func(*ldap.Entry) (string, error){testGroupSearchGroupNameAttribute: func(entry *ldap.Entry) (string, error) {
|
||||
return "something-else-" + entry.DN, nil
|
||||
}}
|
||||
}),
|
||||
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(func(r *authenticators.Response) {
|
||||
r.User = &user.DefaultInfo{
|
||||
Name: testUserSearchResultUsernameAttributeValue,
|
||||
UID: base64.RawURLEncoding.EncodeToString([]byte(testUserSearchResultUIDAttributeValue)),
|
||||
Groups: []string{"something-else-some-upstream-group-dn1", "something-else-some-upstream-group-dn2"},
|
||||
}
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: "when the group search base is empty then skip the group search entirely",
|
||||
username: testUpstreamUsername,
|
||||
@ -254,7 +281,7 @@ func TestEndUserAuthentication(t *testing.T) {
|
||||
},
|
||||
wantAuthResponse: expectedAuthResponse(func(r *authenticators.Response) {
|
||||
info := r.User.(*user.DefaultInfo)
|
||||
info.Groups = nil
|
||||
info.Groups = []string{}
|
||||
}),
|
||||
},
|
||||
{
|
||||
@ -958,6 +985,24 @@ func TestEndUserAuthentication(t *testing.T) {
|
||||
},
|
||||
wantError: fmt.Sprintf(`found empty value for attribute "%s" while searching for user "%s", but expected value to be non-empty`, testUserSearchUIDAttribute, testUpstreamUsername),
|
||||
},
|
||||
{
|
||||
name: "when the group search has an override func that errors",
|
||||
username: testUpstreamUsername,
|
||||
password: testUpstreamPassword,
|
||||
providerConfig: providerConfig(func(p *ProviderConfig) {
|
||||
p.GroupAttributeParsingOverrides = map[string]func(*ldap.Entry) (string, error){testGroupSearchGroupNameAttribute: func(entry *ldap.Entry) (string, error) {
|
||||
return "", errors.New("some error")
|
||||
}}
|
||||
}),
|
||||
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)
|
||||
},
|
||||
wantError: fmt.Sprintf("error finding groups for user %s: some error", testUserSearchResultDNValue),
|
||||
},
|
||||
{
|
||||
name: "when binding as the found user returns an error",
|
||||
username: testUpstreamUsername,
|
||||
@ -1100,6 +1145,18 @@ func TestUpstreamRefresh(t *testing.T) {
|
||||
Controls: nil, // don't need paging because we set the SizeLimit so small
|
||||
}
|
||||
|
||||
expectedGroupSearch := &ldap.SearchRequest{
|
||||
BaseDN: testGroupSearchBase,
|
||||
Scope: ldap.ScopeWholeSubtree,
|
||||
DerefAliases: ldap.NeverDerefAliases,
|
||||
SizeLimit: 0, // unlimited size because we will search with paging
|
||||
TimeLimit: 90,
|
||||
TypesOnly: false,
|
||||
Filter: testGroupSearchFilterInterpolated,
|
||||
Attributes: []string{testGroupSearchGroupNameAttribute},
|
||||
Controls: nil, // nil because ldap.SearchWithPaging() will set the appropriate controls for us
|
||||
}
|
||||
|
||||
happyPathUserSearchResult := &ldap.SearchResult{
|
||||
Entries: []*ldap.Entry{
|
||||
{
|
||||
@ -1146,6 +1203,7 @@ func TestUpstreamRefresh(t *testing.T) {
|
||||
setupMocks func(conn *mockldapconn.MockConn)
|
||||
dialError error
|
||||
wantErr string
|
||||
wantGroups []string
|
||||
}{
|
||||
{
|
||||
name: "happy path where searching the dn returns a single entry",
|
||||
@ -1155,6 +1213,90 @@ func TestUpstreamRefresh(t *testing.T) {
|
||||
conn.EXPECT().Search(expectedUserSearch).Return(happyPathUserSearchResult, nil).Times(1)
|
||||
conn.EXPECT().Close().Times(1)
|
||||
},
|
||||
wantGroups: []string{},
|
||||
},
|
||||
{
|
||||
name: "happy path where group search returns groups",
|
||||
providerConfig: &ProviderConfig{
|
||||
Name: "some-provider-name",
|
||||
Host: testHost,
|
||||
CABundle: nil, // this field is only used by the production dialer, which is replaced by a mock for this test
|
||||
ConnectionProtocol: TLS,
|
||||
BindUsername: testBindUsername,
|
||||
BindPassword: testBindPassword,
|
||||
UserSearch: UserSearchConfig{
|
||||
Base: testUserSearchBase,
|
||||
UIDAttribute: testUserSearchUIDAttribute,
|
||||
UsernameAttribute: testUserSearchUsernameAttribute,
|
||||
},
|
||||
GroupSearch: GroupSearchConfig{
|
||||
Base: testGroupSearchBase,
|
||||
Filter: testGroupSearchFilter,
|
||||
GroupNameAttribute: testGroupSearchGroupNameAttribute,
|
||||
},
|
||||
RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.StoredRefreshAttributes) error{
|
||||
pwdLastSetAttribute: AttributeUnchangedSinceLogin(pwdLastSetAttribute),
|
||||
},
|
||||
},
|
||||
setupMocks: func(conn *mockldapconn.MockConn) {
|
||||
conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1)
|
||||
conn.EXPECT().Search(expectedUserSearch).Return(happyPathUserSearchResult, nil).Times(1)
|
||||
conn.EXPECT().SearchWithPaging(expectedGroupSearch, expectedGroupSearchPageSize).Return(&ldap.SearchResult{
|
||||
Entries: []*ldap.Entry{
|
||||
{
|
||||
DN: testGroupSearchResultDNValue1,
|
||||
Attributes: []*ldap.EntryAttribute{
|
||||
ldap.NewEntryAttribute(testGroupSearchGroupNameAttribute, []string{testGroupSearchResultGroupNameAttributeValue1}),
|
||||
},
|
||||
},
|
||||
{
|
||||
DN: testGroupSearchResultDNValue2,
|
||||
Attributes: []*ldap.EntryAttribute{
|
||||
ldap.NewEntryAttribute(testGroupSearchGroupNameAttribute, []string{testGroupSearchResultGroupNameAttributeValue2}),
|
||||
},
|
||||
},
|
||||
},
|
||||
Referrals: []string{}, // note that we are not following referrals at this time
|
||||
Controls: []ldap.Control{},
|
||||
}, nil).Times(1)
|
||||
conn.EXPECT().Close().Times(1)
|
||||
},
|
||||
wantGroups: []string{testGroupSearchResultGroupNameAttributeValue1, testGroupSearchResultGroupNameAttributeValue2},
|
||||
},
|
||||
{
|
||||
name: "happy path where group search returns no groups",
|
||||
providerConfig: &ProviderConfig{
|
||||
Name: "some-provider-name",
|
||||
Host: testHost,
|
||||
CABundle: nil, // this field is only used by the production dialer, which is replaced by a mock for this test
|
||||
ConnectionProtocol: TLS,
|
||||
BindUsername: testBindUsername,
|
||||
BindPassword: testBindPassword,
|
||||
UserSearch: UserSearchConfig{
|
||||
Base: testUserSearchBase,
|
||||
UIDAttribute: testUserSearchUIDAttribute,
|
||||
UsernameAttribute: testUserSearchUsernameAttribute,
|
||||
},
|
||||
GroupSearch: GroupSearchConfig{
|
||||
Base: testGroupSearchBase,
|
||||
Filter: testGroupSearchFilter,
|
||||
GroupNameAttribute: testGroupSearchGroupNameAttribute,
|
||||
},
|
||||
RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.StoredRefreshAttributes) error{
|
||||
pwdLastSetAttribute: AttributeUnchangedSinceLogin(pwdLastSetAttribute),
|
||||
},
|
||||
},
|
||||
setupMocks: func(conn *mockldapconn.MockConn) {
|
||||
conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1)
|
||||
conn.EXPECT().Search(expectedUserSearch).Return(happyPathUserSearchResult, nil).Times(1)
|
||||
conn.EXPECT().SearchWithPaging(expectedGroupSearch, expectedGroupSearchPageSize).Return(&ldap.SearchResult{
|
||||
Entries: []*ldap.Entry{},
|
||||
Referrals: []string{}, // note that we are not following referrals at this time
|
||||
Controls: []ldap.Control{},
|
||||
}, nil).Times(1)
|
||||
conn.EXPECT().Close().Times(1)
|
||||
},
|
||||
wantGroups: []string{},
|
||||
},
|
||||
{
|
||||
name: "error where dial fails",
|
||||
@ -1421,6 +1563,37 @@ func TestUpstreamRefresh(t *testing.T) {
|
||||
},
|
||||
wantErr: "validation for attribute \"pwdLastSet\" failed during upstream refresh: value for attribute \"pwdLastSet\" has changed since initial value at login",
|
||||
},
|
||||
{
|
||||
name: "group search returns an error",
|
||||
providerConfig: &ProviderConfig{
|
||||
Name: "some-provider-name",
|
||||
Host: testHost,
|
||||
CABundle: nil, // this field is only used by the production dialer, which is replaced by a mock for this test
|
||||
ConnectionProtocol: TLS,
|
||||
BindUsername: testBindUsername,
|
||||
BindPassword: testBindPassword,
|
||||
UserSearch: UserSearchConfig{
|
||||
Base: testUserSearchBase,
|
||||
UIDAttribute: testUserSearchUIDAttribute,
|
||||
UsernameAttribute: testUserSearchUsernameAttribute,
|
||||
},
|
||||
GroupSearch: GroupSearchConfig{
|
||||
Base: testGroupSearchBase,
|
||||
Filter: testGroupSearchFilter,
|
||||
GroupNameAttribute: testGroupSearchGroupNameAttribute,
|
||||
},
|
||||
RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.StoredRefreshAttributes) error{
|
||||
pwdLastSetAttribute: AttributeUnchangedSinceLogin(pwdLastSetAttribute),
|
||||
},
|
||||
},
|
||||
setupMocks: func(conn *mockldapconn.MockConn) {
|
||||
conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1)
|
||||
conn.EXPECT().Search(expectedUserSearch).Return(happyPathUserSearchResult, nil).Times(1)
|
||||
conn.EXPECT().SearchWithPaging(expectedGroupSearch, expectedGroupSearchPageSize).Return(nil, errors.New("some search error")).Times(1)
|
||||
conn.EXPECT().Close().Times(1)
|
||||
},
|
||||
wantErr: "error searching for group memberships for user with DN \"some-upstream-user-dn\": some search error",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
@ -1435,9 +1608,9 @@ func TestUpstreamRefresh(t *testing.T) {
|
||||
}
|
||||
|
||||
dialWasAttempted := false
|
||||
providerConfig.Dialer = LDAPDialerFunc(func(ctx context.Context, addr endpointaddr.HostPort) (Conn, error) {
|
||||
tt.providerConfig.Dialer = LDAPDialerFunc(func(ctx context.Context, addr endpointaddr.HostPort) (Conn, error) {
|
||||
dialWasAttempted = true
|
||||
require.Equal(t, providerConfig.Host, addr.Endpoint())
|
||||
require.Equal(t, tt.providerConfig.Host, addr.Endpoint())
|
||||
if tt.dialError != nil {
|
||||
return nil, tt.dialError
|
||||
}
|
||||
@ -1446,9 +1619,9 @@ func TestUpstreamRefresh(t *testing.T) {
|
||||
})
|
||||
|
||||
initialPwdLastSetEncoded := base64.RawURLEncoding.EncodeToString([]byte("132801740800000000"))
|
||||
ldapProvider := New(*providerConfig)
|
||||
ldapProvider := New(*tt.providerConfig)
|
||||
subject := "ldaps://ldap.example.com:8443?base=some-upstream-user-base-dn&sub=c29tZS11cHN0cmVhbS11aWQtdmFsdWU"
|
||||
err := ldapProvider.PerformRefresh(context.Background(), provider.StoredRefreshAttributes{
|
||||
groups, err := ldapProvider.PerformRefresh(context.Background(), provider.StoredRefreshAttributes{
|
||||
Username: testUserSearchResultUsernameAttributeValue,
|
||||
Subject: subject,
|
||||
DN: testUserSearchResultDNValue,
|
||||
@ -1461,6 +1634,7 @@ func TestUpstreamRefresh(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
require.Equal(t, true, dialWasAttempted)
|
||||
require.Equal(t, tt.wantGroups, groups)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -244,7 +244,7 @@ func TestLDAPSearch_Parallel(t *testing.T) {
|
||||
p.GroupSearch.Base = ""
|
||||
})),
|
||||
wantAuthResponse: &authenticators.Response{
|
||||
User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: nil},
|
||||
User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: []string{}},
|
||||
DN: "cn=pinny,ou=users,dc=pinniped,dc=dev",
|
||||
ExtraRefreshAttributes: map[string]string{},
|
||||
},
|
||||
@ -257,7 +257,7 @@ func TestLDAPSearch_Parallel(t *testing.T) {
|
||||
p.GroupSearch.Base = "ou=users,dc=pinniped,dc=dev" // there are no groups under this part of the tree
|
||||
})),
|
||||
wantAuthResponse: &authenticators.Response{
|
||||
User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: nil},
|
||||
User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: []string{}},
|
||||
DN: "cn=pinny,ou=users,dc=pinniped,dc=dev",
|
||||
ExtraRefreshAttributes: map[string]string{},
|
||||
},
|
||||
@ -302,7 +302,7 @@ func TestLDAPSearch_Parallel(t *testing.T) {
|
||||
p.GroupSearch.GroupNameAttribute = "objectClass" // silly example, but still a meaningful test
|
||||
})),
|
||||
wantAuthResponse: &authenticators.Response{
|
||||
User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: []string{"groupOfNames", "groupOfNames"}},
|
||||
User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: []string{"groupOfNames"}},
|
||||
DN: "cn=pinny,ou=users,dc=pinniped,dc=dev",
|
||||
ExtraRefreshAttributes: map[string]string{},
|
||||
},
|
||||
@ -328,7 +328,7 @@ func TestLDAPSearch_Parallel(t *testing.T) {
|
||||
p.GroupSearch.Filter = "foobar={}" // foobar is not a valid attribute name for this LDAP server's schema
|
||||
})),
|
||||
wantAuthResponse: &authenticators.Response{
|
||||
User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: nil},
|
||||
User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: []string{}},
|
||||
DN: "cn=pinny,ou=users,dc=pinniped,dc=dev",
|
||||
ExtraRefreshAttributes: map[string]string{},
|
||||
},
|
||||
|
@ -51,21 +51,25 @@ func TestSupervisorLogin_Browser(t *testing.T) {
|
||||
env := testlib.IntegrationEnv(t)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
maybeSkip func(t *testing.T)
|
||||
createTestUser func(t *testing.T) (string, string)
|
||||
deleteTestUser func(t *testing.T, username string)
|
||||
requestAuthorization func(t *testing.T, downstreamAuthorizeURL, downstreamCallbackURL, username, password string, httpClient *http.Client)
|
||||
createIDP func(t *testing.T) string
|
||||
wantDownstreamIDTokenSubjectToMatch string
|
||||
wantDownstreamIDTokenUsernameToMatch func(username string) string
|
||||
wantDownstreamIDTokenGroups []string
|
||||
wantErrorDescription string
|
||||
wantErrorType string
|
||||
name string
|
||||
maybeSkip func(t *testing.T)
|
||||
createTestUser func(t *testing.T) (string, string)
|
||||
deleteTestUser func(t *testing.T, username string)
|
||||
requestAuthorization func(t *testing.T, downstreamAuthorizeURL, downstreamCallbackURL, username, password string, httpClient *http.Client)
|
||||
createIDP func(t *testing.T) string
|
||||
wantDownstreamIDTokenSubjectToMatch string
|
||||
wantDownstreamIDTokenUsernameToMatch func(username string) string
|
||||
wantDownstreamIDTokenGroups []string
|
||||
wantDownstreamIDTokenGroupsAfterRefresh []string
|
||||
wantErrorDescription string
|
||||
wantErrorType string
|
||||
|
||||
// Either revoke the user's session on the upstream provider, or manipulate the user's session
|
||||
// data in such a way that it should cause the next upstream refresh attempt to fail.
|
||||
breakRefreshSessionData func(t *testing.T, sessionData *psession.PinnipedSession, idpName, username string)
|
||||
// Edit the refresh session data between the initial login and the refresh, which is expected to
|
||||
// succeed.
|
||||
editRefreshSessionDataWithoutBreaking func(t *testing.T, sessionData *psession.PinnipedSession, idpName, username string)
|
||||
}{
|
||||
{
|
||||
name: "oidc with default username and groups claim settings",
|
||||
@ -128,6 +132,14 @@ func TestSupervisorLogin_Browser(t *testing.T) {
|
||||
wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta(env.SupervisorUpstreamOIDC.Issuer+"?sub=") + ".+",
|
||||
wantDownstreamIDTokenUsernameToMatch: func(_ string) string { return "^" + regexp.QuoteMeta(env.SupervisorUpstreamOIDC.Username) + "$" },
|
||||
wantDownstreamIDTokenGroups: env.SupervisorUpstreamOIDC.ExpectedGroups,
|
||||
editRefreshSessionDataWithoutBreaking: func(t *testing.T, sessionData *psession.PinnipedSession, _, _ string) {
|
||||
// even if we update this group to the wrong thing, we expect that it will return to the correct
|
||||
// value after we refresh.
|
||||
// However if there are no expected groups then they will not update, so we should skip this.
|
||||
if len(env.SupervisorUpstreamOIDC.ExpectedGroups) > 0 {
|
||||
sessionData.Fosite.Claims.Extra["groups"] = []string{"some-wrong-group", "some-other-group"}
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "oidc without refresh token",
|
||||
@ -272,6 +284,11 @@ func TestSupervisorLogin_Browser(t *testing.T) {
|
||||
false,
|
||||
)
|
||||
},
|
||||
editRefreshSessionDataWithoutBreaking: func(t *testing.T, sessionData *psession.PinnipedSession, _, _ string) {
|
||||
// even if we update this group to the wrong thing, we expect that it will return to the correct
|
||||
// value after we refresh.
|
||||
sessionData.Fosite.Claims.Extra["groups"] = []string{"some-wrong-group", "some-other-group"}
|
||||
},
|
||||
breakRefreshSessionData: func(t *testing.T, pinnipedSession *psession.PinnipedSession, _, _ string) {
|
||||
customSessionData := pinnipedSession.Custom
|
||||
require.Equal(t, psession.ProviderTypeLDAP, customSessionData.ProviderType)
|
||||
@ -291,6 +308,94 @@ func TestSupervisorLogin_Browser(t *testing.T) {
|
||||
},
|
||||
wantDownstreamIDTokenGroups: env.SupervisorUpstreamLDAP.TestUserDirectGroupsDNs,
|
||||
},
|
||||
{
|
||||
name: "ldap with email as username and group search base that doesn't return anything, and using an LDAP provider which supports TLS",
|
||||
maybeSkip: func(t *testing.T) {
|
||||
t.Helper()
|
||||
if len(env.ToolsNamespace) == 0 && !env.HasCapability(testlib.CanReachInternetLDAPPorts) {
|
||||
t.Skip("LDAP integration test requires connectivity to an LDAP server")
|
||||
}
|
||||
if env.SupervisorUpstreamLDAP.UserSearchBase == env.SupervisorUpstreamLDAP.GroupSearchBase {
|
||||
// This test relies on using the user search base as the group search base, to simulate
|
||||
// searching for groups and not finding any.
|
||||
// If the users and groups are stored in the same place, then we will get groups
|
||||
// back, so this test wouldn't make sense.
|
||||
t.Skip("must have a different user search base than group search base")
|
||||
}
|
||||
},
|
||||
createIDP: func(t *testing.T) string {
|
||||
t.Helper()
|
||||
secret := testlib.CreateTestSecret(t, env.SupervisorNamespace, "ldap-service-account", v1.SecretTypeBasicAuth,
|
||||
map[string]string{
|
||||
v1.BasicAuthUsernameKey: env.SupervisorUpstreamLDAP.BindUsername,
|
||||
v1.BasicAuthPasswordKey: env.SupervisorUpstreamLDAP.BindPassword,
|
||||
},
|
||||
)
|
||||
ldapIDP := testlib.CreateTestLDAPIdentityProvider(t, idpv1alpha1.LDAPIdentityProviderSpec{
|
||||
Host: env.SupervisorUpstreamLDAP.Host,
|
||||
TLS: &idpv1alpha1.TLSSpec{
|
||||
CertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte(env.SupervisorUpstreamLDAP.CABundle)),
|
||||
},
|
||||
Bind: idpv1alpha1.LDAPIdentityProviderBind{
|
||||
SecretName: secret.Name,
|
||||
},
|
||||
UserSearch: idpv1alpha1.LDAPIdentityProviderUserSearch{
|
||||
Base: env.SupervisorUpstreamLDAP.UserSearchBase,
|
||||
Filter: "",
|
||||
Attributes: idpv1alpha1.LDAPIdentityProviderUserSearchAttributes{
|
||||
Username: env.SupervisorUpstreamLDAP.TestUserMailAttributeName,
|
||||
UID: env.SupervisorUpstreamLDAP.TestUserUniqueIDAttributeName,
|
||||
},
|
||||
},
|
||||
GroupSearch: idpv1alpha1.LDAPIdentityProviderGroupSearch{
|
||||
Base: env.SupervisorUpstreamLDAP.UserSearchBase, // groups not stored at the user search base
|
||||
Filter: "",
|
||||
Attributes: idpv1alpha1.LDAPIdentityProviderGroupSearchAttributes{
|
||||
GroupName: "dn",
|
||||
},
|
||||
},
|
||||
}, idpv1alpha1.LDAPPhaseReady)
|
||||
expectedMsg := fmt.Sprintf(
|
||||
`successfully able to connect to "%s" and bind as user "%s" [validated with Secret "%s" at version "%s"]`,
|
||||
env.SupervisorUpstreamLDAP.Host, env.SupervisorUpstreamLDAP.BindUsername,
|
||||
secret.Name, secret.ResourceVersion,
|
||||
)
|
||||
requireSuccessfulLDAPIdentityProviderConditions(t, ldapIDP, expectedMsg)
|
||||
return ldapIDP.Name
|
||||
},
|
||||
requestAuthorization: func(t *testing.T, downstreamAuthorizeURL, _, _, _ string, httpClient *http.Client) {
|
||||
requestAuthorizationUsingCLIPasswordFlow(t,
|
||||
downstreamAuthorizeURL,
|
||||
env.SupervisorUpstreamLDAP.TestUserMailAttributeValue, // username to present to server during login
|
||||
env.SupervisorUpstreamLDAP.TestUserPassword, // password to present to server during login
|
||||
httpClient,
|
||||
false,
|
||||
)
|
||||
},
|
||||
editRefreshSessionDataWithoutBreaking: func(t *testing.T, sessionData *psession.PinnipedSession, _, _ string) {
|
||||
// even if we update this group to the wrong thing, we expect that it will return to the correct
|
||||
// value after we refresh.
|
||||
sessionData.Fosite.Claims.Extra["groups"] = []string{"some-wrong-group", "some-other-group"}
|
||||
},
|
||||
breakRefreshSessionData: func(t *testing.T, pinnipedSession *psession.PinnipedSession, _, _ string) {
|
||||
customSessionData := pinnipedSession.Custom
|
||||
require.Equal(t, psession.ProviderTypeLDAP, customSessionData.ProviderType)
|
||||
require.NotEmpty(t, customSessionData.LDAP.UserDN)
|
||||
fositeSessionData := pinnipedSession.Fosite
|
||||
fositeSessionData.Claims.Subject = "not-right"
|
||||
},
|
||||
// the ID token Subject should be the Host URL plus the value pulled from the requested UserSearch.Attributes.UID attribute
|
||||
wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta(
|
||||
"ldaps://"+env.SupervisorUpstreamLDAP.Host+
|
||||
"?base="+url.QueryEscape(env.SupervisorUpstreamLDAP.UserSearchBase)+
|
||||
"&sub="+base64.RawURLEncoding.EncodeToString([]byte(env.SupervisorUpstreamLDAP.TestUserUniqueIDAttributeValue)),
|
||||
) + "$",
|
||||
// the ID token Username should have been pulled from the requested UserSearch.Attributes.Username attribute
|
||||
wantDownstreamIDTokenUsernameToMatch: func(_ string) string {
|
||||
return "^" + regexp.QuoteMeta(env.SupervisorUpstreamLDAP.TestUserMailAttributeValue) + "$"
|
||||
},
|
||||
wantDownstreamIDTokenGroups: []string{},
|
||||
},
|
||||
{
|
||||
name: "ldap with CN as username and group names as CNs and using an LDAP provider which only supports StartTLS", // try another variation of configuration options
|
||||
maybeSkip: func(t *testing.T) {
|
||||
@ -1241,7 +1346,6 @@ func TestSupervisorLogin_Browser(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
||||
defer cancel()
|
||||
|
||||
// Create the LDAPIdentityProvider using GenerateName to get a random name.
|
||||
upstreams := client.IDPV1alpha1().LDAPIdentityProviders(env.SupervisorNamespace)
|
||||
ldapIDP, err := upstreams.Get(ctx, idpName, metav1.GetOptions{})
|
||||
require.NoError(t, err)
|
||||
@ -1263,6 +1367,87 @@ func TestSupervisorLogin_Browser(t *testing.T) {
|
||||
},
|
||||
wantDownstreamIDTokenGroups: env.SupervisorUpstreamLDAP.TestUserDirectGroupsDNs,
|
||||
},
|
||||
{
|
||||
name: "ldap refresh updates groups to be empty after deleting the group search base",
|
||||
maybeSkip: func(t *testing.T) {
|
||||
t.Helper()
|
||||
if len(env.ToolsNamespace) == 0 && !env.HasCapability(testlib.CanReachInternetLDAPPorts) {
|
||||
t.Skip("LDAP integration test requires connectivity to an LDAP server")
|
||||
}
|
||||
},
|
||||
createIDP: func(t *testing.T) string {
|
||||
t.Helper()
|
||||
|
||||
secret := testlib.CreateTestSecret(t, env.SupervisorNamespace, "ldap-service-account", v1.SecretTypeBasicAuth,
|
||||
map[string]string{
|
||||
v1.BasicAuthUsernameKey: env.SupervisorUpstreamLDAP.BindUsername,
|
||||
v1.BasicAuthPasswordKey: env.SupervisorUpstreamLDAP.BindPassword,
|
||||
},
|
||||
)
|
||||
secretName := secret.Name
|
||||
ldapIDP := testlib.CreateTestLDAPIdentityProvider(t, idpv1alpha1.LDAPIdentityProviderSpec{
|
||||
Host: env.SupervisorUpstreamLDAP.Host,
|
||||
TLS: &idpv1alpha1.TLSSpec{
|
||||
CertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte(env.SupervisorUpstreamLDAP.CABundle)),
|
||||
},
|
||||
Bind: idpv1alpha1.LDAPIdentityProviderBind{
|
||||
SecretName: secretName,
|
||||
},
|
||||
UserSearch: idpv1alpha1.LDAPIdentityProviderUserSearch{
|
||||
Base: env.SupervisorUpstreamLDAP.UserSearchBase,
|
||||
Filter: "",
|
||||
Attributes: idpv1alpha1.LDAPIdentityProviderUserSearchAttributes{
|
||||
Username: env.SupervisorUpstreamLDAP.TestUserMailAttributeName,
|
||||
UID: env.SupervisorUpstreamLDAP.TestUserUniqueIDAttributeName,
|
||||
},
|
||||
},
|
||||
GroupSearch: idpv1alpha1.LDAPIdentityProviderGroupSearch{
|
||||
Base: env.SupervisorUpstreamLDAP.GroupSearchBase,
|
||||
Filter: "",
|
||||
Attributes: idpv1alpha1.LDAPIdentityProviderGroupSearchAttributes{
|
||||
GroupName: "dn",
|
||||
},
|
||||
},
|
||||
}, idpv1alpha1.LDAPPhaseReady)
|
||||
return ldapIDP.Name
|
||||
},
|
||||
requestAuthorization: func(t *testing.T, downstreamAuthorizeURL, _, _, _ string, httpClient *http.Client) {
|
||||
requestAuthorizationUsingCLIPasswordFlow(t,
|
||||
downstreamAuthorizeURL,
|
||||
env.SupervisorUpstreamLDAP.TestUserMailAttributeValue, // username to present to server during login
|
||||
env.SupervisorUpstreamLDAP.TestUserPassword, // password to present to server during login
|
||||
httpClient,
|
||||
false,
|
||||
)
|
||||
},
|
||||
editRefreshSessionDataWithoutBreaking: func(t *testing.T, pinnipedSession *psession.PinnipedSession, idpName, _ string) {
|
||||
// get the idp, update the config.
|
||||
client := testlib.NewSupervisorClientset(t)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
||||
defer cancel()
|
||||
|
||||
upstreams := client.IDPV1alpha1().LDAPIdentityProviders(env.SupervisorNamespace)
|
||||
ldapIDP, err := upstreams.Get(ctx, idpName, metav1.GetOptions{})
|
||||
require.NoError(t, err)
|
||||
ldapIDP.Spec.GroupSearch.Base = ""
|
||||
|
||||
_, err = upstreams.Update(ctx, ldapIDP, metav1.UpdateOptions{})
|
||||
require.NoError(t, err)
|
||||
time.Sleep(10 * time.Second) // wait for controllers to pick up the change
|
||||
},
|
||||
// the ID token Subject should be the Host URL plus the value pulled from the requested UserSearch.Attributes.UID attribute
|
||||
wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta(
|
||||
"ldaps://"+env.SupervisorUpstreamLDAP.Host+
|
||||
"?base="+url.QueryEscape(env.SupervisorUpstreamLDAP.UserSearchBase)+
|
||||
"&sub="+base64.RawURLEncoding.EncodeToString([]byte(env.SupervisorUpstreamLDAP.TestUserUniqueIDAttributeValue)),
|
||||
) + "$",
|
||||
// the ID token Username should have been pulled from the requested UserSearch.Attributes.Username attribute
|
||||
wantDownstreamIDTokenUsernameToMatch: func(_ string) string {
|
||||
return "^" + regexp.QuoteMeta(env.SupervisorUpstreamLDAP.TestUserMailAttributeValue) + "$"
|
||||
},
|
||||
wantDownstreamIDTokenGroups: env.SupervisorUpstreamLDAP.TestUserDirectGroupsDNs,
|
||||
wantDownstreamIDTokenGroupsAfterRefresh: []string{},
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
tt := test
|
||||
@ -1272,12 +1457,14 @@ func TestSupervisorLogin_Browser(t *testing.T) {
|
||||
testSupervisorLogin(t,
|
||||
tt.createIDP,
|
||||
tt.requestAuthorization,
|
||||
tt.editRefreshSessionDataWithoutBreaking,
|
||||
tt.breakRefreshSessionData,
|
||||
tt.createTestUser,
|
||||
tt.deleteTestUser,
|
||||
tt.wantDownstreamIDTokenSubjectToMatch,
|
||||
tt.wantDownstreamIDTokenUsernameToMatch,
|
||||
tt.wantDownstreamIDTokenGroups,
|
||||
tt.wantDownstreamIDTokenGroupsAfterRefresh,
|
||||
tt.wantErrorDescription,
|
||||
tt.wantErrorType,
|
||||
)
|
||||
@ -1405,13 +1592,15 @@ func requireEventuallySuccessfulActiveDirectoryIdentityProviderConditions(t *tes
|
||||
func testSupervisorLogin(
|
||||
t *testing.T,
|
||||
createIDP func(t *testing.T) string,
|
||||
requestAuthorization func(t *testing.T, downstreamAuthorizeURL, downstreamCallbackURL, username, password string, httpClient *http.Client),
|
||||
requestAuthorization func(t *testing.T, downstreamAuthorizeURL string, downstreamCallbackURL string, username string, password string, httpClient *http.Client),
|
||||
editRefreshSessionDataWithoutBreaking func(t *testing.T, pinnipedSession *psession.PinnipedSession, idpName, username string),
|
||||
breakRefreshSessionData func(t *testing.T, pinnipedSession *psession.PinnipedSession, idpName, username string),
|
||||
createTestUser func(t *testing.T) (string, string),
|
||||
deleteTestUser func(t *testing.T, username string),
|
||||
wantDownstreamIDTokenSubjectToMatch string,
|
||||
wantDownstreamIDTokenUsernameToMatch func(username string) string,
|
||||
wantDownstreamIDTokenGroups []string,
|
||||
wantDownstreamIDTokenGroupsAfterRefresh []string,
|
||||
wantErrorDescription string,
|
||||
wantErrorType string,
|
||||
) {
|
||||
@ -1565,6 +1754,28 @@ func testSupervisorLogin(
|
||||
// token exchange on the original token
|
||||
doTokenExchange(t, &downstreamOAuth2Config, tokenResponse, httpClient, discovery)
|
||||
|
||||
if editRefreshSessionDataWithoutBreaking != nil {
|
||||
latestRefreshToken := tokenResponse.RefreshToken
|
||||
signatureOfLatestRefreshToken := getFositeDataSignature(t, latestRefreshToken)
|
||||
|
||||
// First use the latest downstream refresh token to look up the corresponding session in the Supervisor's storage.
|
||||
kubeClient := testlib.NewKubernetesClientset(t)
|
||||
supervisorSecretsClient := kubeClient.CoreV1().Secrets(env.SupervisorNamespace)
|
||||
oauthStore := oidc.NewKubeStorage(supervisorSecretsClient, oidc.DefaultOIDCTimeoutsConfiguration())
|
||||
storedRefreshSession, err := oauthStore.GetRefreshTokenSession(ctx, signatureOfLatestRefreshToken, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Next mutate the part of the session that is used during upstream refresh.
|
||||
pinnipedSession, ok := storedRefreshSession.GetSession().(*psession.PinnipedSession)
|
||||
require.True(t, ok, "should have been able to cast session data to PinnipedSession")
|
||||
|
||||
editRefreshSessionDataWithoutBreaking(t, pinnipedSession, idpName, username)
|
||||
|
||||
// Then save the mutated Secret back to Kubernetes.
|
||||
// There is no update function, so delete and create again at the same name.
|
||||
require.NoError(t, oauthStore.DeleteRefreshTokenSession(ctx, signatureOfLatestRefreshToken))
|
||||
require.NoError(t, oauthStore.CreateRefreshTokenSession(ctx, signatureOfLatestRefreshToken, storedRefreshSession))
|
||||
}
|
||||
// Use the refresh token to get new tokens
|
||||
refreshSource := downstreamOAuth2Config.TokenSource(oidcHTTPClientContext, &oauth2.Token{RefreshToken: tokenResponse.RefreshToken})
|
||||
refreshedTokenResponse, err := refreshSource.Token()
|
||||
@ -1572,9 +1783,12 @@ func testSupervisorLogin(
|
||||
|
||||
// When refreshing, expect to get an "at_hash" claim, but no "nonce" claim.
|
||||
expectRefreshedIDTokenClaims := []string{"iss", "exp", "sub", "aud", "auth_time", "iat", "jti", "rat", "username", "groups", "at_hash"}
|
||||
if wantDownstreamIDTokenGroupsAfterRefresh == nil {
|
||||
wantDownstreamIDTokenGroupsAfterRefresh = wantDownstreamIDTokenGroups
|
||||
}
|
||||
verifyTokenResponse(t,
|
||||
refreshedTokenResponse, discovery, downstreamOAuth2Config, "",
|
||||
expectRefreshedIDTokenClaims, wantDownstreamIDTokenSubjectToMatch, wantDownstreamIDTokenUsernameToMatch(username), wantDownstreamIDTokenGroups)
|
||||
expectRefreshedIDTokenClaims, wantDownstreamIDTokenSubjectToMatch, wantDownstreamIDTokenUsernameToMatch(username), wantDownstreamIDTokenGroupsAfterRefresh)
|
||||
|
||||
require.NotEqual(t, tokenResponse.AccessToken, refreshedTokenResponse.AccessToken)
|
||||
require.NotEqual(t, tokenResponse.RefreshToken, refreshedTokenResponse.RefreshToken)
|
||||
|
Loading…
Reference in New Issue
Block a user