Merge pull request #877 from vmware-tanzu/upstream-ldap-refresh
Upstream ldap refresh
This commit is contained in:
commit
5a3f83f90f
@ -7,7 +7,7 @@ package authenticators
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
"k8s.io/apiserver/pkg/authentication/authenticator"
|
"k8s.io/apiserver/pkg/authentication/user"
|
||||||
)
|
)
|
||||||
|
|
||||||
// This interface is similar to the k8s token authenticator, but works with username/passwords instead
|
// This interface is similar to the k8s token authenticator, but works with username/passwords instead
|
||||||
@ -31,5 +31,10 @@ import (
|
|||||||
// See k8s.io/apiserver/pkg/authentication/authenticator/interfaces.go for the token authenticator
|
// See k8s.io/apiserver/pkg/authentication/authenticator/interfaces.go for the token authenticator
|
||||||
// interface, as well as the Response type.
|
// interface, as well as the Response type.
|
||||||
type UserAuthenticator interface {
|
type UserAuthenticator interface {
|
||||||
AuthenticateUser(ctx context.Context, username, password string) (*authenticator.Response, bool, error)
|
AuthenticateUser(ctx context.Context, username, password string) (*Response, bool, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Response struct {
|
||||||
|
User user.Info
|
||||||
|
DN string
|
||||||
}
|
}
|
||||||
|
@ -328,14 +328,22 @@ const ExpectedAuthorizeCodeSessionJSONFromFuzzing = `{
|
|||||||
"providerType": "闣ʬ橳(ý綃ʃʚƟ覣k眐4Ĉt",
|
"providerType": "闣ʬ橳(ý綃ʃʚƟ覣k眐4Ĉt",
|
||||||
"oidc": {
|
"oidc": {
|
||||||
"upstreamRefreshToken": "嵽痊w©Ź榨Q|ôɵt毇妬"
|
"upstreamRefreshToken": "嵽痊w©Ź榨Q|ôɵt毇妬"
|
||||||
|
},
|
||||||
|
"ldap": {
|
||||||
|
"userDN": "6鉢緋uƴŤȱʀļÂ?墖\u003cƬb獭潜Ʃ饾"
|
||||||
|
},
|
||||||
|
"activedirectory": {
|
||||||
|
"userDN": "|鬌R蜚蠣麹概÷驣7Ʀ澉1æɽ誮rʨ鷞"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"requestedAudience": [
|
"requestedAudience": [
|
||||||
"6鉢緋uƴŤȱʀļÂ?墖\u003cƬb獭潜Ʃ饾"
|
"ŚB碠k9"
|
||||||
],
|
],
|
||||||
"grantedAudience": [
|
"grantedAudience": [
|
||||||
"|鬌R蜚蠣麹概÷驣7Ʀ澉1æɽ誮rʨ鷞"
|
"ʘ赱",
|
||||||
|
"ď逳鞪?3)藵睋邔\u0026Ű惫蜀Ģ¡圔",
|
||||||
|
"墀jMʥ"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"version": "2"
|
"version": "2"
|
||||||
|
@ -15,9 +15,9 @@ import (
|
|||||||
"github.com/ory/fosite/token/jwt"
|
"github.com/ory/fosite/token/jwt"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"golang.org/x/oauth2"
|
"golang.org/x/oauth2"
|
||||||
"k8s.io/apiserver/pkg/authentication/authenticator"
|
|
||||||
|
|
||||||
supervisoroidc "go.pinniped.dev/generated/latest/apis/supervisor/oidc"
|
supervisoroidc "go.pinniped.dev/generated/latest/apis/supervisor/oidc"
|
||||||
|
"go.pinniped.dev/internal/authenticators"
|
||||||
"go.pinniped.dev/internal/httputil/httperr"
|
"go.pinniped.dev/internal/httputil/httperr"
|
||||||
"go.pinniped.dev/internal/httputil/securityheader"
|
"go.pinniped.dev/internal/httputil/securityheader"
|
||||||
"go.pinniped.dev/internal/oidc"
|
"go.pinniped.dev/internal/oidc"
|
||||||
@ -112,6 +112,7 @@ func handleAuthRequestForLDAPUpstream(
|
|||||||
subject := downstreamSubjectFromUpstreamLDAP(ldapUpstream, authenticateResponse)
|
subject := downstreamSubjectFromUpstreamLDAP(ldapUpstream, authenticateResponse)
|
||||||
username = authenticateResponse.User.GetName()
|
username = authenticateResponse.User.GetName()
|
||||||
groups := authenticateResponse.User.GetGroups()
|
groups := authenticateResponse.User.GetGroups()
|
||||||
|
dn := authenticateResponse.DN
|
||||||
|
|
||||||
customSessionData := &psession.CustomSessionData{
|
customSessionData := &psession.CustomSessionData{
|
||||||
ProviderUID: ldapUpstream.GetResourceUID(),
|
ProviderUID: ldapUpstream.GetResourceUID(),
|
||||||
@ -119,6 +120,17 @@ func handleAuthRequestForLDAPUpstream(
|
|||||||
ProviderType: idpType,
|
ProviderType: idpType,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if idpType == psession.ProviderTypeLDAP {
|
||||||
|
customSessionData.LDAP = &psession.LDAPSessionData{
|
||||||
|
UserDN: dn,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if idpType == psession.ProviderTypeActiveDirectory {
|
||||||
|
customSessionData.ActiveDirectory = &psession.ActiveDirectorySessionData{
|
||||||
|
UserDN: dn,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return makeDownstreamSessionAndReturnAuthcodeRedirect(r, w,
|
return makeDownstreamSessionAndReturnAuthcodeRedirect(r, w,
|
||||||
oauthHelper, authorizeRequester, subject, username, groups, customSessionData)
|
oauthHelper, authorizeRequester, subject, username, groups, customSessionData)
|
||||||
}
|
}
|
||||||
@ -470,10 +482,7 @@ func addCSRFSetCookieHeader(w http.ResponseWriter, csrfValue csrftoken.CSRFToken
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func downstreamSubjectFromUpstreamLDAP(ldapUpstream provider.UpstreamLDAPIdentityProviderI, authenticateResponse *authenticator.Response) string {
|
func downstreamSubjectFromUpstreamLDAP(ldapUpstream provider.UpstreamLDAPIdentityProviderI, authenticateResponse *authenticators.Response) string {
|
||||||
ldapURL := *ldapUpstream.GetURL()
|
ldapURL := *ldapUpstream.GetURL()
|
||||||
q := ldapURL.Query()
|
return downstreamsession.DownstreamLDAPSubject(authenticateResponse.User.GetUID(), ldapURL)
|
||||||
q.Set(oidc.IDTokenSubjectClaim, authenticateResponse.User.GetUID())
|
|
||||||
ldapURL.RawQuery = q.Encode()
|
|
||||||
return ldapURL.String()
|
|
||||||
}
|
}
|
||||||
|
@ -19,12 +19,12 @@ import (
|
|||||||
"github.com/ory/fosite"
|
"github.com/ory/fosite"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"golang.org/x/oauth2"
|
"golang.org/x/oauth2"
|
||||||
"k8s.io/apiserver/pkg/authentication/authenticator"
|
|
||||||
"k8s.io/apiserver/pkg/authentication/user"
|
"k8s.io/apiserver/pkg/authentication/user"
|
||||||
"k8s.io/client-go/kubernetes/fake"
|
"k8s.io/client-go/kubernetes/fake"
|
||||||
v1 "k8s.io/client-go/kubernetes/typed/core/v1"
|
v1 "k8s.io/client-go/kubernetes/typed/core/v1"
|
||||||
"k8s.io/utils/pointer"
|
"k8s.io/utils/pointer"
|
||||||
|
|
||||||
|
"go.pinniped.dev/internal/authenticators"
|
||||||
"go.pinniped.dev/internal/here"
|
"go.pinniped.dev/internal/here"
|
||||||
"go.pinniped.dev/internal/oidc"
|
"go.pinniped.dev/internal/oidc"
|
||||||
"go.pinniped.dev/internal/oidc/csrftoken"
|
"go.pinniped.dev/internal/oidc/csrftoken"
|
||||||
@ -267,22 +267,24 @@ func TestAuthorizationEndpoint(t *testing.T) {
|
|||||||
happyLDAPUsernameFromAuthenticator := "some-mapped-ldap-username"
|
happyLDAPUsernameFromAuthenticator := "some-mapped-ldap-username"
|
||||||
happyLDAPPassword := "some-ldap-password" //nolint:gosec
|
happyLDAPPassword := "some-ldap-password" //nolint:gosec
|
||||||
happyLDAPUID := "some-ldap-uid"
|
happyLDAPUID := "some-ldap-uid"
|
||||||
|
happyLDAPUserDN := "cn=foo,dn=bar"
|
||||||
happyLDAPGroups := []string{"group1", "group2", "group3"}
|
happyLDAPGroups := []string{"group1", "group2", "group3"}
|
||||||
|
|
||||||
parsedUpstreamLDAPURL, err := url.Parse(upstreamLDAPURL)
|
parsedUpstreamLDAPURL, err := url.Parse(upstreamLDAPURL)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
ldapAuthenticateFunc := func(ctx context.Context, username, password string) (*authenticator.Response, bool, error) {
|
ldapAuthenticateFunc := func(ctx context.Context, username, password string) (*authenticators.Response, bool, error) {
|
||||||
if username == "" || password == "" {
|
if username == "" || password == "" {
|
||||||
return nil, false, fmt.Errorf("should not have passed empty username or password to the authenticator")
|
return nil, false, fmt.Errorf("should not have passed empty username or password to the authenticator")
|
||||||
}
|
}
|
||||||
if username == happyLDAPUsername && password == happyLDAPPassword {
|
if username == happyLDAPUsername && password == happyLDAPPassword {
|
||||||
return &authenticator.Response{
|
return &authenticators.Response{
|
||||||
User: &user.DefaultInfo{
|
User: &user.DefaultInfo{
|
||||||
Name: happyLDAPUsernameFromAuthenticator,
|
Name: happyLDAPUsernameFromAuthenticator,
|
||||||
UID: happyLDAPUID,
|
UID: happyLDAPUID,
|
||||||
Groups: happyLDAPGroups,
|
Groups: happyLDAPGroups,
|
||||||
},
|
},
|
||||||
|
DN: happyLDAPUserDN,
|
||||||
}, true, nil
|
}, true, nil
|
||||||
}
|
}
|
||||||
return nil, false, nil
|
return nil, false, nil
|
||||||
@ -305,7 +307,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
|
|||||||
erroringUpstreamLDAPIdentityProvider := oidctestutil.TestUpstreamLDAPIdentityProvider{
|
erroringUpstreamLDAPIdentityProvider := oidctestutil.TestUpstreamLDAPIdentityProvider{
|
||||||
Name: ldapUpstreamName,
|
Name: ldapUpstreamName,
|
||||||
ResourceUID: ldapUpstreamResourceUID,
|
ResourceUID: ldapUpstreamResourceUID,
|
||||||
AuthenticateFunc: func(ctx context.Context, username, password string) (*authenticator.Response, bool, error) {
|
AuthenticateFunc: func(ctx context.Context, username, password string) (*authenticators.Response, bool, error) {
|
||||||
return nil, false, fmt.Errorf("some ldap upstream auth error")
|
return nil, false, fmt.Errorf("some ldap upstream auth error")
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@ -438,6 +440,10 @@ func TestAuthorizationEndpoint(t *testing.T) {
|
|||||||
ProviderName: activeDirectoryUpstreamName,
|
ProviderName: activeDirectoryUpstreamName,
|
||||||
ProviderType: psession.ProviderTypeActiveDirectory,
|
ProviderType: psession.ProviderTypeActiveDirectory,
|
||||||
OIDC: nil,
|
OIDC: nil,
|
||||||
|
LDAP: nil,
|
||||||
|
ActiveDirectory: &psession.ActiveDirectorySessionData{
|
||||||
|
UserDN: happyLDAPUserDN,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
expectedHappyLDAPUpstreamCustomSession := &psession.CustomSessionData{
|
expectedHappyLDAPUpstreamCustomSession := &psession.CustomSessionData{
|
||||||
@ -445,6 +451,10 @@ func TestAuthorizationEndpoint(t *testing.T) {
|
|||||||
ProviderName: ldapUpstreamName,
|
ProviderName: ldapUpstreamName,
|
||||||
ProviderType: psession.ProviderTypeLDAP,
|
ProviderType: psession.ProviderTypeLDAP,
|
||||||
OIDC: nil,
|
OIDC: nil,
|
||||||
|
LDAP: &psession.LDAPSessionData{
|
||||||
|
UserDN: happyLDAPUserDN,
|
||||||
|
},
|
||||||
|
ActiveDirectory: nil,
|
||||||
}
|
}
|
||||||
|
|
||||||
expectedHappyOIDCPasswordGrantCustomSession := &psession.CustomSessionData{
|
expectedHappyOIDCPasswordGrantCustomSession := &psession.CustomSessionData{
|
||||||
|
@ -169,6 +169,13 @@ func extractStringClaimValue(claimName string, upstreamIDPName string, idTokenCl
|
|||||||
return valueAsString, nil
|
return valueAsString, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func DownstreamLDAPSubject(uid string, ldapURL url.URL) string {
|
||||||
|
q := ldapURL.Query()
|
||||||
|
q.Set(oidc.IDTokenSubjectClaim, uid)
|
||||||
|
ldapURL.RawQuery = q.Encode()
|
||||||
|
return ldapURL.String()
|
||||||
|
}
|
||||||
|
|
||||||
func downstreamSubjectFromUpstreamOIDC(upstreamIssuerAsString string, upstreamSubject string) string {
|
func downstreamSubjectFromUpstreamOIDC(upstreamIssuerAsString string, upstreamSubject string) string {
|
||||||
return fmt.Sprintf("%s?%s=%s", upstreamIssuerAsString, oidc.IDTokenSubjectClaim, url.QueryEscape(upstreamSubject))
|
return fmt.Sprintf("%s?%s=%s", upstreamIssuerAsString, oidc.IDTokenSubjectClaim, url.QueryEscape(upstreamSubject))
|
||||||
}
|
}
|
||||||
|
@ -88,6 +88,9 @@ type UpstreamLDAPIdentityProviderI interface {
|
|||||||
|
|
||||||
// UserAuthenticator adds an interface method for performing user authentication against the upstream LDAP provider.
|
// UserAuthenticator adds an interface method for performing user authentication against the upstream LDAP provider.
|
||||||
authenticators.UserAuthenticator
|
authenticators.UserAuthenticator
|
||||||
|
|
||||||
|
// PerformRefresh performs a refresh against the upstream LDAP identity provider
|
||||||
|
PerformRefresh(ctx context.Context, userDN, expectedUsername, expectedSubject string) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type DynamicUpstreamIDPProvider interface {
|
type DynamicUpstreamIDPProvider interface {
|
||||||
|
@ -75,6 +75,12 @@ func NewHandler(
|
|||||||
|
|
||||||
func upstreamRefresh(ctx context.Context, accessRequest fosite.AccessRequester, providerCache oidc.UpstreamIdentityProvidersLister) error {
|
func upstreamRefresh(ctx context.Context, accessRequest fosite.AccessRequester, providerCache oidc.UpstreamIdentityProvidersLister) error {
|
||||||
session := accessRequest.GetSession().(*psession.PinnipedSession)
|
session := accessRequest.GetSession().(*psession.PinnipedSession)
|
||||||
|
downstreamUsername, err := getDownstreamUsernameFromPinnipedSession(session)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
downstreamSubject := session.Fosite.Claims.Subject
|
||||||
|
|
||||||
customSessionData := session.Custom
|
customSessionData := session.Custom
|
||||||
if customSessionData == nil {
|
if customSessionData == nil {
|
||||||
return errorsx.WithStack(errMissingUpstreamSessionInternalError)
|
return errorsx.WithStack(errMissingUpstreamSessionInternalError)
|
||||||
@ -89,14 +95,12 @@ func upstreamRefresh(ctx context.Context, accessRequest fosite.AccessRequester,
|
|||||||
case psession.ProviderTypeOIDC:
|
case psession.ProviderTypeOIDC:
|
||||||
return upstreamOIDCRefresh(ctx, customSessionData, providerCache)
|
return upstreamOIDCRefresh(ctx, customSessionData, providerCache)
|
||||||
case psession.ProviderTypeLDAP:
|
case psession.ProviderTypeLDAP:
|
||||||
// upstream refresh not yet implemented for LDAP, so do nothing
|
return upstreamLDAPRefresh(ctx, customSessionData, providerCache, downstreamUsername, downstreamSubject)
|
||||||
case psession.ProviderTypeActiveDirectory:
|
case psession.ProviderTypeActiveDirectory:
|
||||||
// upstream refresh not yet implemented for AD, so do nothing
|
return upstreamLDAPRefresh(ctx, customSessionData, providerCache, downstreamUsername, downstreamSubject)
|
||||||
default:
|
default:
|
||||||
return errorsx.WithStack(errMissingUpstreamSessionInternalError)
|
return errorsx.WithStack(errMissingUpstreamSessionInternalError)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func upstreamOIDCRefresh(ctx context.Context, s *psession.CustomSessionData, providerCache oidc.UpstreamIdentityProvidersLister) error {
|
func upstreamOIDCRefresh(ctx context.Context, s *psession.CustomSessionData, providerCache oidc.UpstreamIdentityProvidersLister) error {
|
||||||
@ -114,9 +118,9 @@ func upstreamOIDCRefresh(ctx context.Context, s *psession.CustomSessionData, pro
|
|||||||
|
|
||||||
refreshedTokens, err := p.PerformRefresh(ctx, s.OIDC.UpstreamRefreshToken)
|
refreshedTokens, err := p.PerformRefresh(ctx, s.OIDC.UpstreamRefreshToken)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errorsx.WithStack(errUpstreamRefreshError.WithHintf(
|
return errorsx.WithStack(errUpstreamRefreshError.WithHint(
|
||||||
"Upstream refresh failed using provider %q of type %q.",
|
"Upstream refresh failed.",
|
||||||
s.ProviderName, s.ProviderType).WithWrap(err))
|
).WithWrap(err).WithDebugf("provider name: %q, provider type: %q", s.ProviderName, s.ProviderType))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Upstream refresh may or may not return a new ID token. From the spec:
|
// Upstream refresh may or may not return a new ID token. From the spec:
|
||||||
@ -129,8 +133,7 @@ func upstreamOIDCRefresh(ctx context.Context, s *psession.CustomSessionData, pro
|
|||||||
_, err = p.ValidateToken(ctx, refreshedTokens, "")
|
_, err = p.ValidateToken(ctx, refreshedTokens, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errorsx.WithStack(errUpstreamRefreshError.WithHintf(
|
return errorsx.WithStack(errUpstreamRefreshError.WithHintf(
|
||||||
"Upstream refresh returned an invalid ID token using provider %q of type %q.",
|
"Upstream refresh returned an invalid ID token.").WithWrap(err).WithDebugf("provider name: %q, provider type: %q", s.ProviderName, s.ProviderType))
|
||||||
s.ProviderName, s.ProviderType).WithWrap(err))
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
plog.Debug("upstream refresh request did not return a new ID token",
|
plog.Debug("upstream refresh request did not return a new ID token",
|
||||||
@ -163,5 +166,72 @@ func findOIDCProviderByNameAndValidateUID(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil, errorsx.WithStack(errUpstreamRefreshError.
|
return nil, errorsx.WithStack(errUpstreamRefreshError.
|
||||||
WithHintf("Provider %q of type %q from upstream session data was not found.", s.ProviderName, s.ProviderType))
|
WithHint("Provider from upstream session data was not found.").WithDebugf("provider name: %q, provider type: %q", s.ProviderName, s.ProviderType))
|
||||||
|
}
|
||||||
|
|
||||||
|
func upstreamLDAPRefresh(ctx context.Context, s *psession.CustomSessionData, providerCache oidc.UpstreamIdentityProvidersLister, username string, subject string) error {
|
||||||
|
// if you have neither a valid ldap session config nor a valid active directory session config
|
||||||
|
validLDAP := s.ProviderType == psession.ProviderTypeLDAP && s.LDAP != nil && s.LDAP.UserDN != ""
|
||||||
|
validAD := s.ProviderType == psession.ProviderTypeActiveDirectory && s.ActiveDirectory != nil && s.ActiveDirectory.UserDN != ""
|
||||||
|
if !(validLDAP || validAD) {
|
||||||
|
return errorsx.WithStack(errMissingUpstreamSessionInternalError)
|
||||||
|
}
|
||||||
|
|
||||||
|
// get ldap/ad provider out of cache
|
||||||
|
p, dn, err := findLDAPProviderByNameAndValidateUID(s, providerCache)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// run PerformRefresh
|
||||||
|
err = p.PerformRefresh(ctx, dn, username, subject)
|
||||||
|
if err != nil {
|
||||||
|
return errorsx.WithStack(errUpstreamRefreshError.WithHint(
|
||||||
|
"Upstream refresh failed.").WithWrap(err).WithDebugf("provider name: %q, provider type: %q", s.ProviderName, s.ProviderType))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func findLDAPProviderByNameAndValidateUID(
|
||||||
|
s *psession.CustomSessionData,
|
||||||
|
providerCache oidc.UpstreamIdentityProvidersLister,
|
||||||
|
) (provider.UpstreamLDAPIdentityProviderI, string, error) {
|
||||||
|
var providers []provider.UpstreamLDAPIdentityProviderI
|
||||||
|
var dn string
|
||||||
|
if s.ProviderType == psession.ProviderTypeLDAP {
|
||||||
|
providers = providerCache.GetLDAPIdentityProviders()
|
||||||
|
dn = s.LDAP.UserDN
|
||||||
|
} else if s.ProviderType == psession.ProviderTypeActiveDirectory {
|
||||||
|
providers = providerCache.GetActiveDirectoryIdentityProviders()
|
||||||
|
dn = s.ActiveDirectory.UserDN
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, p := range providers {
|
||||||
|
if p.GetName() == s.ProviderName {
|
||||||
|
if p.GetResourceUID() != s.ProviderUID {
|
||||||
|
return nil, "", errorsx.WithStack(errUpstreamRefreshError.WithHint(
|
||||||
|
"Provider from upstream session data has changed its resource UID since authentication.").WithDebugf("provider name: %q, provider type: %q", s.ProviderName, s.ProviderType))
|
||||||
|
}
|
||||||
|
return p, dn, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, "", errorsx.WithStack(errUpstreamRefreshError.
|
||||||
|
WithHint("Provider from upstream session data was not found.").WithDebugf("provider name: %q, provider type: %q", s.ProviderName, s.ProviderType))
|
||||||
|
}
|
||||||
|
|
||||||
|
func getDownstreamUsernameFromPinnipedSession(session *psession.PinnipedSession) (string, error) {
|
||||||
|
extra := session.Fosite.Claims.Extra
|
||||||
|
if extra == nil {
|
||||||
|
return "", errorsx.WithStack(errMissingUpstreamSessionInternalError)
|
||||||
|
}
|
||||||
|
downstreamUsernameInterface := extra["username"]
|
||||||
|
if downstreamUsernameInterface == nil {
|
||||||
|
return "", errorsx.WithStack(errMissingUpstreamSessionInternalError)
|
||||||
|
}
|
||||||
|
downstreamUsername, ok := downstreamUsernameInterface.(string)
|
||||||
|
if !ok || len(downstreamUsername) == 0 {
|
||||||
|
return "", errorsx.WithStack(errMissingUpstreamSessionInternalError)
|
||||||
|
}
|
||||||
|
return downstreamUsername, nil
|
||||||
}
|
}
|
||||||
|
@ -232,7 +232,7 @@ type tokenEndpointResponseExpectedValues struct {
|
|||||||
wantErrorResponseBody string
|
wantErrorResponseBody string
|
||||||
wantRequestedScopes []string
|
wantRequestedScopes []string
|
||||||
wantGrantedScopes []string
|
wantGrantedScopes []string
|
||||||
wantUpstreamOIDCRefreshCall *expectedUpstreamRefresh
|
wantUpstreamRefreshCall *expectedUpstreamRefresh
|
||||||
wantUpstreamOIDCValidateTokenCall *expectedUpstreamValidateTokens
|
wantUpstreamOIDCValidateTokenCall *expectedUpstreamValidateTokens
|
||||||
wantCustomSessionDataStored *psession.CustomSessionData
|
wantCustomSessionDataStored *psession.CustomSessionData
|
||||||
}
|
}
|
||||||
@ -879,8 +879,20 @@ func TestRefreshGrant(t *testing.T) {
|
|||||||
oidcUpstreamInitialRefreshToken = "initial-upstream-refresh-token"
|
oidcUpstreamInitialRefreshToken = "initial-upstream-refresh-token"
|
||||||
oidcUpstreamRefreshedIDToken = "fake-refreshed-id-token"
|
oidcUpstreamRefreshedIDToken = "fake-refreshed-id-token"
|
||||||
oidcUpstreamRefreshedRefreshToken = "fake-refreshed-refresh-token"
|
oidcUpstreamRefreshedRefreshToken = "fake-refreshed-refresh-token"
|
||||||
|
|
||||||
|
ldapUpstreamName = "some-ldap-idp"
|
||||||
|
ldapUpstreamResourceUID = "ldap-resource-uid"
|
||||||
|
ldapUpstreamType = "ldap"
|
||||||
|
ldapUpstreamDN = "some-ldap-user-dn"
|
||||||
|
|
||||||
|
activeDirectoryUpstreamName = "some-ad-idp"
|
||||||
|
activeDirectoryUpstreamResourceUID = "ad-resource-uid"
|
||||||
|
activeDirectoryUpstreamType = "activedirectory"
|
||||||
|
activeDirectoryUpstreamDN = "some-ad-user-dn"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
ldapUpstreamURL, _ := url.Parse("some-url")
|
||||||
|
|
||||||
// The below values are funcs so every test can have its own copy of the objects, to avoid data races
|
// The below values are funcs so every test can have its own copy of the objects, to avoid data races
|
||||||
// in these parallel tests.
|
// in these parallel tests.
|
||||||
|
|
||||||
@ -907,7 +919,7 @@ func TestRefreshGrant(t *testing.T) {
|
|||||||
return sessionData
|
return sessionData
|
||||||
}
|
}
|
||||||
|
|
||||||
happyUpstreamRefreshCall := func() *expectedUpstreamRefresh {
|
happyOIDCUpstreamRefreshCall := func() *expectedUpstreamRefresh {
|
||||||
return &expectedUpstreamRefresh{
|
return &expectedUpstreamRefresh{
|
||||||
performedByUpstreamName: oidcUpstreamName,
|
performedByUpstreamName: oidcUpstreamName,
|
||||||
args: &oidctestutil.PerformRefreshArgs{
|
args: &oidctestutil.PerformRefreshArgs{
|
||||||
@ -917,6 +929,30 @@ func TestRefreshGrant(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
happyLDAPUpstreamRefreshCall := func() *expectedUpstreamRefresh {
|
||||||
|
return &expectedUpstreamRefresh{
|
||||||
|
performedByUpstreamName: ldapUpstreamName,
|
||||||
|
args: &oidctestutil.PerformRefreshArgs{
|
||||||
|
Ctx: nil,
|
||||||
|
DN: ldapUpstreamDN,
|
||||||
|
ExpectedSubject: goodSubject,
|
||||||
|
ExpectedUsername: goodUsername,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
happyActiveDirectoryUpstreamRefreshCall := func() *expectedUpstreamRefresh {
|
||||||
|
return &expectedUpstreamRefresh{
|
||||||
|
performedByUpstreamName: activeDirectoryUpstreamName,
|
||||||
|
args: &oidctestutil.PerformRefreshArgs{
|
||||||
|
Ctx: nil,
|
||||||
|
DN: activeDirectoryUpstreamDN,
|
||||||
|
ExpectedSubject: goodSubject,
|
||||||
|
ExpectedUsername: goodUsername,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
happyUpstreamValidateTokenCall := func(expectedTokens *oauth2.Token) *expectedUpstreamValidateTokens {
|
happyUpstreamValidateTokenCall := func(expectedTokens *oauth2.Token) *expectedUpstreamValidateTokens {
|
||||||
return &expectedUpstreamValidateTokens{
|
return &expectedUpstreamValidateTokens{
|
||||||
performedByUpstreamName: oidcUpstreamName,
|
performedByUpstreamName: oidcUpstreamName,
|
||||||
@ -944,7 +980,7 @@ func TestRefreshGrant(t *testing.T) {
|
|||||||
// same as the same values as the authcode exchange case.
|
// same as the same values as the authcode exchange case.
|
||||||
want := happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(wantCustomSessionDataStored)
|
want := happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(wantCustomSessionDataStored)
|
||||||
// Should always try to perform an upstream refresh.
|
// Should always try to perform an upstream refresh.
|
||||||
want.wantUpstreamOIDCRefreshCall = happyUpstreamRefreshCall()
|
want.wantUpstreamRefreshCall = happyOIDCUpstreamRefreshCall()
|
||||||
// Should only try to ValidateToken when there was an id token returned by the upstream refresh.
|
// Should only try to ValidateToken when there was an id token returned by the upstream refresh.
|
||||||
if expectToValidateToken != nil {
|
if expectToValidateToken != nil {
|
||||||
want.wantUpstreamOIDCValidateTokenCall = happyUpstreamValidateTokenCall(expectToValidateToken)
|
want.wantUpstreamOIDCValidateTokenCall = happyUpstreamValidateTokenCall(expectToValidateToken)
|
||||||
@ -952,6 +988,18 @@ func TestRefreshGrant(t *testing.T) {
|
|||||||
return want
|
return want
|
||||||
}
|
}
|
||||||
|
|
||||||
|
happyRefreshTokenResponseForLDAP := func(wantCustomSessionDataStored *psession.CustomSessionData) tokenEndpointResponseExpectedValues {
|
||||||
|
want := happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(wantCustomSessionDataStored)
|
||||||
|
want.wantUpstreamRefreshCall = happyLDAPUpstreamRefreshCall()
|
||||||
|
return want
|
||||||
|
}
|
||||||
|
|
||||||
|
happyRefreshTokenResponseForActiveDirectory := func(wantCustomSessionDataStored *psession.CustomSessionData) tokenEndpointResponseExpectedValues {
|
||||||
|
want := happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(wantCustomSessionDataStored)
|
||||||
|
want.wantUpstreamRefreshCall = happyActiveDirectoryUpstreamRefreshCall()
|
||||||
|
return want
|
||||||
|
}
|
||||||
|
|
||||||
refreshedUpstreamTokensWithRefreshTokenWithoutIDToken := func() *oauth2.Token {
|
refreshedUpstreamTokensWithRefreshTokenWithoutIDToken := func() *oauth2.Token {
|
||||||
return &oauth2.Token{
|
return &oauth2.Token{
|
||||||
AccessToken: "fake-refreshed-access-token",
|
AccessToken: "fake-refreshed-access-token",
|
||||||
@ -972,11 +1020,28 @@ func TestRefreshGrant(t *testing.T) {
|
|||||||
return tokens
|
return tokens
|
||||||
}
|
}
|
||||||
|
|
||||||
|
happyActiveDirectoryCustomSessionData := &psession.CustomSessionData{
|
||||||
|
ProviderUID: activeDirectoryUpstreamResourceUID,
|
||||||
|
ProviderName: activeDirectoryUpstreamName,
|
||||||
|
ProviderType: activeDirectoryUpstreamType,
|
||||||
|
ActiveDirectory: &psession.ActiveDirectorySessionData{
|
||||||
|
UserDN: activeDirectoryUpstreamDN,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
happyLDAPCustomSessionData := &psession.CustomSessionData{
|
||||||
|
ProviderUID: ldapUpstreamResourceUID,
|
||||||
|
ProviderName: ldapUpstreamName,
|
||||||
|
ProviderType: ldapUpstreamType,
|
||||||
|
LDAP: &psession.LDAPSessionData{
|
||||||
|
UserDN: ldapUpstreamDN,
|
||||||
|
},
|
||||||
|
}
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
idps *oidctestutil.UpstreamIDPListerBuilder
|
idps *oidctestutil.UpstreamIDPListerBuilder
|
||||||
authcodeExchange authcodeExchangeInputs
|
authcodeExchange authcodeExchangeInputs
|
||||||
refreshRequest refreshRequestInputs
|
refreshRequest refreshRequestInputs
|
||||||
|
modifyRefreshTokenStorage func(t *testing.T, oauthStore *oidc.KubeStorage, refreshToken string)
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "happy path refresh grant with openid scope granted (id token returned)",
|
name: "happy path refresh grant with openid scope granted (id token returned)",
|
||||||
@ -1015,7 +1080,7 @@ func TestRefreshGrant(t *testing.T) {
|
|||||||
wantSuccessBodyFields: []string{"refresh_token", "access_token", "token_type", "expires_in", "scope"},
|
wantSuccessBodyFields: []string{"refresh_token", "access_token", "token_type", "expires_in", "scope"},
|
||||||
wantRequestedScopes: []string{"offline_access"},
|
wantRequestedScopes: []string{"offline_access"},
|
||||||
wantGrantedScopes: []string{"offline_access"},
|
wantGrantedScopes: []string{"offline_access"},
|
||||||
wantUpstreamOIDCRefreshCall: happyUpstreamRefreshCall(),
|
wantUpstreamRefreshCall: happyOIDCUpstreamRefreshCall(),
|
||||||
wantUpstreamOIDCValidateTokenCall: happyUpstreamValidateTokenCall(refreshedUpstreamTokensWithIDAndRefreshTokens()),
|
wantUpstreamOIDCValidateTokenCall: happyUpstreamValidateTokenCall(refreshedUpstreamTokensWithIDAndRefreshTokens()),
|
||||||
wantCustomSessionDataStored: upstreamOIDCCustomSessionDataWithNewRefreshToken(oidcUpstreamRefreshedRefreshToken),
|
wantCustomSessionDataStored: upstreamOIDCCustomSessionDataWithNewRefreshToken(oidcUpstreamRefreshedRefreshToken),
|
||||||
},
|
},
|
||||||
@ -1096,7 +1161,7 @@ func TestRefreshGrant(t *testing.T) {
|
|||||||
wantSuccessBodyFields: []string{"id_token", "refresh_token", "access_token", "token_type", "expires_in", "scope"},
|
wantSuccessBodyFields: []string{"id_token", "refresh_token", "access_token", "token_type", "expires_in", "scope"},
|
||||||
wantRequestedScopes: []string{"openid", "offline_access", "pinniped:request-audience"},
|
wantRequestedScopes: []string{"openid", "offline_access", "pinniped:request-audience"},
|
||||||
wantGrantedScopes: []string{"openid", "offline_access", "pinniped:request-audience"},
|
wantGrantedScopes: []string{"openid", "offline_access", "pinniped:request-audience"},
|
||||||
wantUpstreamOIDCRefreshCall: happyUpstreamRefreshCall(),
|
wantUpstreamRefreshCall: happyOIDCUpstreamRefreshCall(),
|
||||||
wantUpstreamOIDCValidateTokenCall: happyUpstreamValidateTokenCall(refreshedUpstreamTokensWithIDAndRefreshTokens()),
|
wantUpstreamOIDCValidateTokenCall: happyUpstreamValidateTokenCall(refreshedUpstreamTokensWithIDAndRefreshTokens()),
|
||||||
wantCustomSessionDataStored: upstreamOIDCCustomSessionDataWithNewRefreshToken(oidcUpstreamRefreshedRefreshToken),
|
wantCustomSessionDataStored: upstreamOIDCCustomSessionDataWithNewRefreshToken(oidcUpstreamRefreshedRefreshToken),
|
||||||
},
|
},
|
||||||
@ -1400,7 +1465,7 @@ func TestRefreshGrant(t *testing.T) {
|
|||||||
wantErrorResponseBody: here.Doc(`
|
wantErrorResponseBody: here.Doc(`
|
||||||
{
|
{
|
||||||
"error": "error",
|
"error": "error",
|
||||||
"error_description": "Error during upstream refresh. Provider 'this-name-will-not-be-found' of type 'oidc' from upstream session data was not found."
|
"error_description": "Error during upstream refresh. Provider from upstream session data was not found."
|
||||||
}
|
}
|
||||||
`),
|
`),
|
||||||
},
|
},
|
||||||
@ -1449,12 +1514,12 @@ func TestRefreshGrant(t *testing.T) {
|
|||||||
},
|
},
|
||||||
refreshRequest: refreshRequestInputs{
|
refreshRequest: refreshRequestInputs{
|
||||||
want: tokenEndpointResponseExpectedValues{
|
want: tokenEndpointResponseExpectedValues{
|
||||||
wantUpstreamOIDCRefreshCall: happyUpstreamRefreshCall(),
|
wantUpstreamRefreshCall: happyOIDCUpstreamRefreshCall(),
|
||||||
wantStatus: http.StatusUnauthorized,
|
wantStatus: http.StatusUnauthorized,
|
||||||
wantErrorResponseBody: here.Doc(`
|
wantErrorResponseBody: here.Doc(`
|
||||||
{
|
{
|
||||||
"error": "error",
|
"error": "error",
|
||||||
"error_description": "Error during upstream refresh. Upstream refresh failed using provider 'some-oidc-idp' of type 'oidc'."
|
"error_description": "Error during upstream refresh. Upstream refresh failed."
|
||||||
}
|
}
|
||||||
`),
|
`),
|
||||||
},
|
},
|
||||||
@ -1474,13 +1539,520 @@ func TestRefreshGrant(t *testing.T) {
|
|||||||
},
|
},
|
||||||
refreshRequest: refreshRequestInputs{
|
refreshRequest: refreshRequestInputs{
|
||||||
want: tokenEndpointResponseExpectedValues{
|
want: tokenEndpointResponseExpectedValues{
|
||||||
wantUpstreamOIDCRefreshCall: happyUpstreamRefreshCall(),
|
wantUpstreamRefreshCall: happyOIDCUpstreamRefreshCall(),
|
||||||
wantUpstreamOIDCValidateTokenCall: happyUpstreamValidateTokenCall(refreshedUpstreamTokensWithIDAndRefreshTokens()),
|
wantUpstreamOIDCValidateTokenCall: happyUpstreamValidateTokenCall(refreshedUpstreamTokensWithIDAndRefreshTokens()),
|
||||||
wantStatus: http.StatusUnauthorized,
|
wantStatus: http.StatusUnauthorized,
|
||||||
wantErrorResponseBody: here.Doc(`
|
wantErrorResponseBody: here.Doc(`
|
||||||
{
|
{
|
||||||
"error": "error",
|
"error": "error",
|
||||||
"error_description": "Error during upstream refresh. Upstream refresh returned an invalid ID token using provider 'some-oidc-idp' of type 'oidc'."
|
"error_description": "Error during upstream refresh. Upstream refresh returned an invalid ID token."
|
||||||
|
}
|
||||||
|
`),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "upstream ldap refresh happy path",
|
||||||
|
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&oidctestutil.TestUpstreamLDAPIdentityProvider{
|
||||||
|
Name: ldapUpstreamName,
|
||||||
|
ResourceUID: ldapUpstreamResourceUID,
|
||||||
|
URL: ldapUpstreamURL,
|
||||||
|
}),
|
||||||
|
authcodeExchange: authcodeExchangeInputs{
|
||||||
|
modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access") },
|
||||||
|
customSessionData: happyLDAPCustomSessionData,
|
||||||
|
want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(
|
||||||
|
happyLDAPCustomSessionData,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
refreshRequest: refreshRequestInputs{
|
||||||
|
want: happyRefreshTokenResponseForLDAP(
|
||||||
|
happyLDAPCustomSessionData,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "upstream active directory refresh happy path",
|
||||||
|
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&oidctestutil.TestUpstreamLDAPIdentityProvider{
|
||||||
|
Name: activeDirectoryUpstreamName,
|
||||||
|
ResourceUID: activeDirectoryUpstreamResourceUID,
|
||||||
|
URL: ldapUpstreamURL,
|
||||||
|
}),
|
||||||
|
authcodeExchange: authcodeExchangeInputs{
|
||||||
|
modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access") },
|
||||||
|
customSessionData: happyActiveDirectoryCustomSessionData,
|
||||||
|
want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(
|
||||||
|
happyActiveDirectoryCustomSessionData,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
refreshRequest: refreshRequestInputs{
|
||||||
|
want: happyRefreshTokenResponseForActiveDirectory(
|
||||||
|
happyActiveDirectoryCustomSessionData,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "upstream ldap refresh when the LDAP session data is nil",
|
||||||
|
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&oidctestutil.TestUpstreamLDAPIdentityProvider{
|
||||||
|
Name: ldapUpstreamName,
|
||||||
|
ResourceUID: ldapUpstreamResourceUID,
|
||||||
|
URL: ldapUpstreamURL,
|
||||||
|
}),
|
||||||
|
authcodeExchange: authcodeExchangeInputs{
|
||||||
|
modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access") },
|
||||||
|
customSessionData: &psession.CustomSessionData{
|
||||||
|
ProviderUID: ldapUpstreamResourceUID,
|
||||||
|
ProviderName: ldapUpstreamName,
|
||||||
|
ProviderType: ldapUpstreamType,
|
||||||
|
LDAP: nil,
|
||||||
|
},
|
||||||
|
want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(
|
||||||
|
&psession.CustomSessionData{
|
||||||
|
ProviderUID: ldapUpstreamResourceUID,
|
||||||
|
ProviderName: ldapUpstreamName,
|
||||||
|
ProviderType: ldapUpstreamType,
|
||||||
|
LDAP: nil,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
},
|
||||||
|
refreshRequest: refreshRequestInputs{
|
||||||
|
want: tokenEndpointResponseExpectedValues{
|
||||||
|
wantStatus: http.StatusInternalServerError,
|
||||||
|
wantErrorResponseBody: here.Doc(`
|
||||||
|
{
|
||||||
|
"error": "error",
|
||||||
|
"error_description": "There was an internal server error. Required upstream data not found in session."
|
||||||
|
}
|
||||||
|
`),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "upstream active directory refresh when the ad session data is nil",
|
||||||
|
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&oidctestutil.TestUpstreamLDAPIdentityProvider{
|
||||||
|
Name: activeDirectoryUpstreamName,
|
||||||
|
ResourceUID: activeDirectoryUpstreamResourceUID,
|
||||||
|
URL: ldapUpstreamURL,
|
||||||
|
}),
|
||||||
|
authcodeExchange: authcodeExchangeInputs{
|
||||||
|
modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access") },
|
||||||
|
customSessionData: &psession.CustomSessionData{
|
||||||
|
ProviderUID: activeDirectoryUpstreamResourceUID,
|
||||||
|
ProviderName: activeDirectoryUpstreamName,
|
||||||
|
ProviderType: activeDirectoryUpstreamType,
|
||||||
|
ActiveDirectory: nil,
|
||||||
|
},
|
||||||
|
want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(
|
||||||
|
&psession.CustomSessionData{
|
||||||
|
ProviderUID: activeDirectoryUpstreamResourceUID,
|
||||||
|
ProviderName: activeDirectoryUpstreamName,
|
||||||
|
ProviderType: activeDirectoryUpstreamType,
|
||||||
|
ActiveDirectory: nil,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
},
|
||||||
|
refreshRequest: refreshRequestInputs{
|
||||||
|
want: tokenEndpointResponseExpectedValues{
|
||||||
|
wantStatus: http.StatusInternalServerError,
|
||||||
|
wantErrorResponseBody: here.Doc(`
|
||||||
|
{
|
||||||
|
"error": "error",
|
||||||
|
"error_description": "There was an internal server error. Required upstream data not found in session."
|
||||||
|
}
|
||||||
|
`),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "upstream ldap refresh when the LDAP session data does not contain dn",
|
||||||
|
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&oidctestutil.TestUpstreamLDAPIdentityProvider{
|
||||||
|
Name: ldapUpstreamName,
|
||||||
|
ResourceUID: ldapUpstreamResourceUID,
|
||||||
|
URL: ldapUpstreamURL,
|
||||||
|
}),
|
||||||
|
authcodeExchange: authcodeExchangeInputs{
|
||||||
|
modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access") },
|
||||||
|
customSessionData: &psession.CustomSessionData{
|
||||||
|
ProviderUID: ldapUpstreamResourceUID,
|
||||||
|
ProviderName: ldapUpstreamName,
|
||||||
|
ProviderType: ldapUpstreamType,
|
||||||
|
LDAP: &psession.LDAPSessionData{
|
||||||
|
UserDN: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(
|
||||||
|
&psession.CustomSessionData{
|
||||||
|
ProviderUID: ldapUpstreamResourceUID,
|
||||||
|
ProviderName: ldapUpstreamName,
|
||||||
|
ProviderType: ldapUpstreamType,
|
||||||
|
LDAP: &psession.LDAPSessionData{
|
||||||
|
UserDN: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
},
|
||||||
|
refreshRequest: refreshRequestInputs{
|
||||||
|
want: tokenEndpointResponseExpectedValues{
|
||||||
|
wantStatus: http.StatusInternalServerError,
|
||||||
|
wantErrorResponseBody: here.Doc(`
|
||||||
|
{
|
||||||
|
"error": "error",
|
||||||
|
"error_description": "There was an internal server error. Required upstream data not found in session."
|
||||||
|
}
|
||||||
|
`),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "upstream active directory refresh when the active directory session data does not contain dn",
|
||||||
|
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&oidctestutil.TestUpstreamLDAPIdentityProvider{
|
||||||
|
Name: activeDirectoryUpstreamName,
|
||||||
|
ResourceUID: activeDirectoryUpstreamResourceUID,
|
||||||
|
URL: ldapUpstreamURL,
|
||||||
|
}),
|
||||||
|
authcodeExchange: authcodeExchangeInputs{
|
||||||
|
modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access") },
|
||||||
|
customSessionData: &psession.CustomSessionData{
|
||||||
|
ProviderUID: ldapUpstreamResourceUID,
|
||||||
|
ProviderName: ldapUpstreamName,
|
||||||
|
ProviderType: ldapUpstreamType,
|
||||||
|
ActiveDirectory: &psession.ActiveDirectorySessionData{
|
||||||
|
UserDN: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(
|
||||||
|
&psession.CustomSessionData{
|
||||||
|
ProviderUID: ldapUpstreamResourceUID,
|
||||||
|
ProviderName: ldapUpstreamName,
|
||||||
|
ProviderType: ldapUpstreamType,
|
||||||
|
ActiveDirectory: &psession.ActiveDirectorySessionData{
|
||||||
|
UserDN: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
},
|
||||||
|
refreshRequest: refreshRequestInputs{
|
||||||
|
want: tokenEndpointResponseExpectedValues{
|
||||||
|
wantStatus: http.StatusInternalServerError,
|
||||||
|
wantErrorResponseBody: here.Doc(`
|
||||||
|
{
|
||||||
|
"error": "error",
|
||||||
|
"error_description": "There was an internal server error. Required upstream data not found in session."
|
||||||
|
}
|
||||||
|
`),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "upstream ldap refresh returns an error",
|
||||||
|
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&oidctestutil.TestUpstreamLDAPIdentityProvider{
|
||||||
|
Name: ldapUpstreamName,
|
||||||
|
ResourceUID: ldapUpstreamResourceUID,
|
||||||
|
URL: ldapUpstreamURL,
|
||||||
|
PerformRefreshErr: errors.New("Some error performing upstream refresh"),
|
||||||
|
}),
|
||||||
|
authcodeExchange: authcodeExchangeInputs{
|
||||||
|
modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access") },
|
||||||
|
customSessionData: happyLDAPCustomSessionData,
|
||||||
|
want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(
|
||||||
|
happyLDAPCustomSessionData,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
refreshRequest: refreshRequestInputs{
|
||||||
|
want: tokenEndpointResponseExpectedValues{
|
||||||
|
wantUpstreamRefreshCall: happyLDAPUpstreamRefreshCall(),
|
||||||
|
wantStatus: http.StatusUnauthorized,
|
||||||
|
wantErrorResponseBody: here.Doc(`
|
||||||
|
{
|
||||||
|
"error": "error",
|
||||||
|
"error_description": "Error during upstream refresh. Upstream refresh failed."
|
||||||
|
}
|
||||||
|
`),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "upstream active directory refresh returns an error",
|
||||||
|
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&oidctestutil.TestUpstreamLDAPIdentityProvider{
|
||||||
|
Name: activeDirectoryUpstreamName,
|
||||||
|
ResourceUID: activeDirectoryUpstreamResourceUID,
|
||||||
|
URL: ldapUpstreamURL,
|
||||||
|
PerformRefreshErr: errors.New("Some error performing upstream refresh"),
|
||||||
|
}),
|
||||||
|
authcodeExchange: authcodeExchangeInputs{
|
||||||
|
modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access") },
|
||||||
|
customSessionData: happyActiveDirectoryCustomSessionData,
|
||||||
|
want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(
|
||||||
|
happyActiveDirectoryCustomSessionData,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
refreshRequest: refreshRequestInputs{
|
||||||
|
want: tokenEndpointResponseExpectedValues{
|
||||||
|
wantUpstreamRefreshCall: happyActiveDirectoryUpstreamRefreshCall(),
|
||||||
|
wantStatus: http.StatusUnauthorized,
|
||||||
|
wantErrorResponseBody: here.Doc(`
|
||||||
|
{
|
||||||
|
"error": "error",
|
||||||
|
"error_description": "Error during upstream refresh. Upstream refresh failed."
|
||||||
|
}
|
||||||
|
`),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "upstream ldap idp not found",
|
||||||
|
idps: oidctestutil.NewUpstreamIDPListerBuilder(),
|
||||||
|
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.StatusUnauthorized,
|
||||||
|
wantErrorResponseBody: here.Doc(`
|
||||||
|
{
|
||||||
|
"error": "error",
|
||||||
|
"error_description": "Error during upstream refresh. Provider from upstream session data was not found."
|
||||||
|
}
|
||||||
|
`),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "upstream active directory idp not found",
|
||||||
|
idps: oidctestutil.NewUpstreamIDPListerBuilder(),
|
||||||
|
authcodeExchange: authcodeExchangeInputs{
|
||||||
|
modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access") },
|
||||||
|
customSessionData: happyActiveDirectoryCustomSessionData,
|
||||||
|
want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(
|
||||||
|
happyActiveDirectoryCustomSessionData,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
refreshRequest: refreshRequestInputs{
|
||||||
|
want: tokenEndpointResponseExpectedValues{
|
||||||
|
wantStatus: http.StatusUnauthorized,
|
||||||
|
wantErrorResponseBody: here.Doc(`
|
||||||
|
{
|
||||||
|
"error": "error",
|
||||||
|
"error_description": "Error during upstream refresh. Provider from upstream session data was not found."
|
||||||
|
}
|
||||||
|
`),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "fosite session is empty",
|
||||||
|
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&oidctestutil.TestUpstreamLDAPIdentityProvider{
|
||||||
|
Name: ldapUpstreamName,
|
||||||
|
ResourceUID: ldapUpstreamResourceUID,
|
||||||
|
URL: ldapUpstreamURL,
|
||||||
|
}),
|
||||||
|
authcodeExchange: authcodeExchangeInputs{
|
||||||
|
modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access") },
|
||||||
|
customSessionData: happyLDAPCustomSessionData,
|
||||||
|
want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(
|
||||||
|
happyLDAPCustomSessionData,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
modifyRefreshTokenStorage: func(t *testing.T, oauthStore *oidc.KubeStorage, refreshToken string) {
|
||||||
|
refreshTokenSignature := getFositeDataSignature(t, refreshToken)
|
||||||
|
firstRequester, err := oauthStore.GetRefreshTokenSession(context.Background(), refreshTokenSignature, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
session := firstRequester.GetSession().(*psession.PinnipedSession)
|
||||||
|
session.Fosite = &openid.DefaultSession{}
|
||||||
|
err = oauthStore.DeleteRefreshTokenSession(context.Background(), refreshTokenSignature)
|
||||||
|
require.NoError(t, err)
|
||||||
|
err = oauthStore.CreateRefreshTokenSession(context.Background(), refreshTokenSignature, firstRequester)
|
||||||
|
require.NoError(t, err)
|
||||||
|
},
|
||||||
|
refreshRequest: refreshRequestInputs{
|
||||||
|
want: tokenEndpointResponseExpectedValues{
|
||||||
|
wantStatus: http.StatusInternalServerError,
|
||||||
|
wantErrorResponseBody: here.Doc(`
|
||||||
|
{
|
||||||
|
"error": "error",
|
||||||
|
"error_description": "There was an internal server error. Required upstream data not found in session."
|
||||||
|
}
|
||||||
|
`),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "username not found in extra field",
|
||||||
|
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&oidctestutil.TestUpstreamLDAPIdentityProvider{
|
||||||
|
Name: ldapUpstreamName,
|
||||||
|
ResourceUID: ldapUpstreamResourceUID,
|
||||||
|
URL: ldapUpstreamURL,
|
||||||
|
}),
|
||||||
|
authcodeExchange: authcodeExchangeInputs{
|
||||||
|
modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access") },
|
||||||
|
customSessionData: happyLDAPCustomSessionData,
|
||||||
|
//fositeSessionData: &openid.DefaultSession{},
|
||||||
|
want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(
|
||||||
|
happyLDAPCustomSessionData,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
modifyRefreshTokenStorage: func(t *testing.T, oauthStore *oidc.KubeStorage, refreshToken string) {
|
||||||
|
refreshTokenSignature := getFositeDataSignature(t, refreshToken)
|
||||||
|
firstRequester, err := oauthStore.GetRefreshTokenSession(context.Background(), refreshTokenSignature, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
session := firstRequester.GetSession().(*psession.PinnipedSession)
|
||||||
|
session.Fosite = &openid.DefaultSession{
|
||||||
|
Claims: &jwt.IDTokenClaims{
|
||||||
|
Extra: map[string]interface{}{},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
err = oauthStore.DeleteRefreshTokenSession(context.Background(), refreshTokenSignature)
|
||||||
|
require.NoError(t, err)
|
||||||
|
err = oauthStore.CreateRefreshTokenSession(context.Background(), refreshTokenSignature, firstRequester)
|
||||||
|
require.NoError(t, err)
|
||||||
|
},
|
||||||
|
refreshRequest: refreshRequestInputs{
|
||||||
|
want: tokenEndpointResponseExpectedValues{
|
||||||
|
wantStatus: http.StatusInternalServerError,
|
||||||
|
wantErrorResponseBody: here.Doc(`
|
||||||
|
{
|
||||||
|
"error": "error",
|
||||||
|
"error_description": "There was an internal server error. Required upstream data not found in session."
|
||||||
|
}
|
||||||
|
`),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "username in extra is not a string",
|
||||||
|
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&oidctestutil.TestUpstreamLDAPIdentityProvider{
|
||||||
|
Name: ldapUpstreamName,
|
||||||
|
ResourceUID: ldapUpstreamResourceUID,
|
||||||
|
URL: ldapUpstreamURL,
|
||||||
|
}),
|
||||||
|
authcodeExchange: authcodeExchangeInputs{
|
||||||
|
modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access") },
|
||||||
|
customSessionData: happyLDAPCustomSessionData,
|
||||||
|
//fositeSessionData: &openid.DefaultSession{},
|
||||||
|
want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(
|
||||||
|
happyLDAPCustomSessionData,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
modifyRefreshTokenStorage: func(t *testing.T, oauthStore *oidc.KubeStorage, refreshToken string) {
|
||||||
|
refreshTokenSignature := getFositeDataSignature(t, refreshToken)
|
||||||
|
firstRequester, err := oauthStore.GetRefreshTokenSession(context.Background(), refreshTokenSignature, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
session := firstRequester.GetSession().(*psession.PinnipedSession)
|
||||||
|
session.Fosite = &openid.DefaultSession{
|
||||||
|
Claims: &jwt.IDTokenClaims{
|
||||||
|
Extra: map[string]interface{}{"username": 123},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
err = oauthStore.DeleteRefreshTokenSession(context.Background(), refreshTokenSignature)
|
||||||
|
require.NoError(t, err)
|
||||||
|
err = oauthStore.CreateRefreshTokenSession(context.Background(), refreshTokenSignature, firstRequester)
|
||||||
|
require.NoError(t, err)
|
||||||
|
},
|
||||||
|
refreshRequest: refreshRequestInputs{
|
||||||
|
want: tokenEndpointResponseExpectedValues{
|
||||||
|
wantStatus: http.StatusInternalServerError,
|
||||||
|
wantErrorResponseBody: here.Doc(`
|
||||||
|
{
|
||||||
|
"error": "error",
|
||||||
|
"error_description": "There was an internal server error. Required upstream data not found in session."
|
||||||
|
}
|
||||||
|
`),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "username in extra is an empty string",
|
||||||
|
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&oidctestutil.TestUpstreamLDAPIdentityProvider{
|
||||||
|
Name: ldapUpstreamName,
|
||||||
|
ResourceUID: ldapUpstreamResourceUID,
|
||||||
|
URL: ldapUpstreamURL,
|
||||||
|
}),
|
||||||
|
authcodeExchange: authcodeExchangeInputs{
|
||||||
|
modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access") },
|
||||||
|
customSessionData: happyLDAPCustomSessionData,
|
||||||
|
//fositeSessionData: &openid.DefaultSession{},
|
||||||
|
want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(
|
||||||
|
happyLDAPCustomSessionData,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
modifyRefreshTokenStorage: func(t *testing.T, oauthStore *oidc.KubeStorage, refreshToken string) {
|
||||||
|
refreshTokenSignature := getFositeDataSignature(t, refreshToken)
|
||||||
|
firstRequester, err := oauthStore.GetRefreshTokenSession(context.Background(), refreshTokenSignature, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
session := firstRequester.GetSession().(*psession.PinnipedSession)
|
||||||
|
session.Fosite = &openid.DefaultSession{
|
||||||
|
Claims: &jwt.IDTokenClaims{
|
||||||
|
Extra: map[string]interface{}{"username": ""},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
err = oauthStore.DeleteRefreshTokenSession(context.Background(), refreshTokenSignature)
|
||||||
|
require.NoError(t, err)
|
||||||
|
err = oauthStore.CreateRefreshTokenSession(context.Background(), refreshTokenSignature, firstRequester)
|
||||||
|
require.NoError(t, err)
|
||||||
|
},
|
||||||
|
refreshRequest: refreshRequestInputs{
|
||||||
|
want: tokenEndpointResponseExpectedValues{
|
||||||
|
wantStatus: http.StatusInternalServerError,
|
||||||
|
wantErrorResponseBody: here.Doc(`
|
||||||
|
{
|
||||||
|
"error": "error",
|
||||||
|
"error_description": "There was an internal server error. Required upstream data not found in session."
|
||||||
|
}
|
||||||
|
`),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "when the ldap provider in the session storage is found but has the wrong resource UID during the refresh request",
|
||||||
|
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&oidctestutil.TestUpstreamLDAPIdentityProvider{
|
||||||
|
Name: ldapUpstreamName,
|
||||||
|
ResourceUID: "the-wrong-uid",
|
||||||
|
URL: ldapUpstreamURL,
|
||||||
|
}),
|
||||||
|
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.StatusUnauthorized,
|
||||||
|
wantErrorResponseBody: here.Doc(`
|
||||||
|
{
|
||||||
|
"error": "error",
|
||||||
|
"error_description": "Error during upstream refresh. Provider from upstream session data has changed its resource UID since authentication."
|
||||||
|
}
|
||||||
|
`),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "when the active directory provider in the session storage is found but has the wrong resource UID during the refresh request",
|
||||||
|
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&oidctestutil.TestUpstreamLDAPIdentityProvider{
|
||||||
|
Name: activeDirectoryUpstreamName,
|
||||||
|
ResourceUID: "the-wrong-uid",
|
||||||
|
URL: ldapUpstreamURL,
|
||||||
|
}),
|
||||||
|
authcodeExchange: authcodeExchangeInputs{
|
||||||
|
modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access") },
|
||||||
|
customSessionData: happyActiveDirectoryCustomSessionData,
|
||||||
|
want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(
|
||||||
|
happyActiveDirectoryCustomSessionData,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
refreshRequest: refreshRequestInputs{
|
||||||
|
want: tokenEndpointResponseExpectedValues{
|
||||||
|
wantStatus: http.StatusUnauthorized,
|
||||||
|
wantErrorResponseBody: here.Doc(`
|
||||||
|
{
|
||||||
|
"error": "error",
|
||||||
|
"error_description": "Error during upstream refresh. Provider from upstream session data has changed its resource UID since authentication."
|
||||||
}
|
}
|
||||||
`),
|
`),
|
||||||
},
|
},
|
||||||
@ -1493,6 +2065,8 @@ func TestRefreshGrant(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
// First exchange the authcode for tokens, including a refresh token.
|
// First exchange the authcode for tokens, including a refresh token.
|
||||||
|
// its actually fine to use this function even when simulating ldap (which uses a different flow) because it's
|
||||||
|
// just populating a secret in storage.
|
||||||
subject, rsp, authCode, jwtSigningKey, secrets, oauthStore := exchangeAuthcodeForTokens(t, test.authcodeExchange, test.idps.Build())
|
subject, rsp, authCode, jwtSigningKey, secrets, oauthStore := exchangeAuthcodeForTokens(t, test.authcodeExchange, test.idps.Build())
|
||||||
var parsedAuthcodeExchangeResponseBody map[string]interface{}
|
var parsedAuthcodeExchangeResponseBody map[string]interface{}
|
||||||
require.NoError(t, json.Unmarshal(rsp.Body.Bytes(), &parsedAuthcodeExchangeResponseBody))
|
require.NoError(t, json.Unmarshal(rsp.Body.Bytes(), &parsedAuthcodeExchangeResponseBody))
|
||||||
@ -1511,6 +2085,10 @@ func TestRefreshGrant(t *testing.T) {
|
|||||||
// Send the refresh token back and preform a refresh.
|
// Send the refresh token back and preform a refresh.
|
||||||
firstRefreshToken := parsedAuthcodeExchangeResponseBody["refresh_token"].(string)
|
firstRefreshToken := parsedAuthcodeExchangeResponseBody["refresh_token"].(string)
|
||||||
require.NotEmpty(t, firstRefreshToken)
|
require.NotEmpty(t, firstRefreshToken)
|
||||||
|
|
||||||
|
if test.modifyRefreshTokenStorage != nil {
|
||||||
|
test.modifyRefreshTokenStorage(t, oauthStore, firstRefreshToken)
|
||||||
|
}
|
||||||
reqContext := context.WithValue(context.Background(), struct{ name string }{name: "test"}, "request-context")
|
reqContext := context.WithValue(context.Background(), struct{ name string }{name: "test"}, "request-context")
|
||||||
req := httptest.NewRequest("POST", "/path/shouldn't/matter",
|
req := httptest.NewRequest("POST", "/path/shouldn't/matter",
|
||||||
happyRefreshRequestBody(firstRefreshToken).ReadCloser()).WithContext(reqContext)
|
happyRefreshRequestBody(firstRefreshToken).ReadCloser()).WithContext(reqContext)
|
||||||
@ -1525,11 +2103,11 @@ func TestRefreshGrant(t *testing.T) {
|
|||||||
t.Logf("second response body: %q", refreshResponse.Body.String())
|
t.Logf("second response body: %q", refreshResponse.Body.String())
|
||||||
|
|
||||||
// Test that we did or did not make a call to the upstream OIDC provider interface to perform a token refresh.
|
// Test that we did or did not make a call to the upstream OIDC provider interface to perform a token refresh.
|
||||||
if test.refreshRequest.want.wantUpstreamOIDCRefreshCall != nil {
|
if test.refreshRequest.want.wantUpstreamRefreshCall != nil {
|
||||||
test.refreshRequest.want.wantUpstreamOIDCRefreshCall.args.Ctx = reqContext
|
test.refreshRequest.want.wantUpstreamRefreshCall.args.Ctx = reqContext
|
||||||
test.idps.RequireExactlyOneCallToPerformRefresh(t,
|
test.idps.RequireExactlyOneCallToPerformRefresh(t,
|
||||||
test.refreshRequest.want.wantUpstreamOIDCRefreshCall.performedByUpstreamName,
|
test.refreshRequest.want.wantUpstreamRefreshCall.performedByUpstreamName,
|
||||||
test.refreshRequest.want.wantUpstreamOIDCRefreshCall.args,
|
test.refreshRequest.want.wantUpstreamRefreshCall.args,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
test.idps.RequireExactlyZeroCallsToPerformRefresh(t)
|
test.idps.RequireExactlyZeroCallsToPerformRefresh(t)
|
||||||
|
@ -45,6 +45,10 @@ type CustomSessionData struct {
|
|||||||
|
|
||||||
// Only used when ProviderType == "oidc".
|
// Only used when ProviderType == "oidc".
|
||||||
OIDC *OIDCSessionData `json:"oidc,omitempty"`
|
OIDC *OIDCSessionData `json:"oidc,omitempty"`
|
||||||
|
|
||||||
|
LDAP *LDAPSessionData `json:"ldap,omitempty"`
|
||||||
|
|
||||||
|
ActiveDirectory *ActiveDirectorySessionData `json:"activedirectory,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ProviderType string
|
type ProviderType string
|
||||||
@ -60,6 +64,16 @@ type OIDCSessionData struct {
|
|||||||
UpstreamRefreshToken string `json:"upstreamRefreshToken"`
|
UpstreamRefreshToken string `json:"upstreamRefreshToken"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// LDAPSessionData is the additional data needed by Pinniped when the upstream IDP is an LDAP provider.
|
||||||
|
type LDAPSessionData struct {
|
||||||
|
UserDN string `json:"userDN"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ActiveDirectorySessionData is the additional data needed by Pinniped when the upstream IDP is an Active Directory provider.
|
||||||
|
type ActiveDirectorySessionData struct {
|
||||||
|
UserDN string `json:"userDN"`
|
||||||
|
}
|
||||||
|
|
||||||
// NewPinnipedSession returns a new empty session.
|
// NewPinnipedSession returns a new empty session.
|
||||||
func NewPinnipedSession() *PinnipedSession {
|
func NewPinnipedSession() *PinnipedSession {
|
||||||
return &PinnipedSession{
|
return &PinnipedSession{
|
||||||
|
@ -21,10 +21,10 @@ import (
|
|||||||
"gopkg.in/square/go-jose.v2"
|
"gopkg.in/square/go-jose.v2"
|
||||||
"k8s.io/apimachinery/pkg/labels"
|
"k8s.io/apimachinery/pkg/labels"
|
||||||
"k8s.io/apimachinery/pkg/types"
|
"k8s.io/apimachinery/pkg/types"
|
||||||
"k8s.io/apiserver/pkg/authentication/authenticator"
|
|
||||||
"k8s.io/client-go/kubernetes/fake"
|
"k8s.io/client-go/kubernetes/fake"
|
||||||
v1 "k8s.io/client-go/kubernetes/typed/core/v1"
|
v1 "k8s.io/client-go/kubernetes/typed/core/v1"
|
||||||
|
|
||||||
|
"go.pinniped.dev/internal/authenticators"
|
||||||
"go.pinniped.dev/internal/crud"
|
"go.pinniped.dev/internal/crud"
|
||||||
"go.pinniped.dev/internal/fositestorage/authorizationcode"
|
"go.pinniped.dev/internal/fositestorage/authorizationcode"
|
||||||
"go.pinniped.dev/internal/fositestorage/openidconnect"
|
"go.pinniped.dev/internal/fositestorage/openidconnect"
|
||||||
@ -61,8 +61,11 @@ type PasswordCredentialsGrantAndValidateTokensArgs struct {
|
|||||||
// PerformRefreshArgs is used to spy on calls to
|
// PerformRefreshArgs is used to spy on calls to
|
||||||
// TestUpstreamOIDCIdentityProvider.PerformRefreshFunc().
|
// TestUpstreamOIDCIdentityProvider.PerformRefreshFunc().
|
||||||
type PerformRefreshArgs struct {
|
type PerformRefreshArgs struct {
|
||||||
Ctx context.Context
|
Ctx context.Context
|
||||||
RefreshToken string
|
RefreshToken string
|
||||||
|
DN string
|
||||||
|
ExpectedUsername string
|
||||||
|
ExpectedSubject string
|
||||||
}
|
}
|
||||||
|
|
||||||
// ValidateTokenArgs is used to spy on calls to
|
// ValidateTokenArgs is used to spy on calls to
|
||||||
@ -74,10 +77,13 @@ type ValidateTokenArgs struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type TestUpstreamLDAPIdentityProvider struct {
|
type TestUpstreamLDAPIdentityProvider struct {
|
||||||
Name string
|
Name string
|
||||||
ResourceUID types.UID
|
ResourceUID types.UID
|
||||||
URL *url.URL
|
URL *url.URL
|
||||||
AuthenticateFunc func(ctx context.Context, username, password string) (*authenticator.Response, bool, error)
|
AuthenticateFunc func(ctx context.Context, username, password string) (*authenticators.Response, bool, error)
|
||||||
|
performRefreshCallCount int
|
||||||
|
performRefreshArgs []*PerformRefreshArgs
|
||||||
|
PerformRefreshErr error
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ provider.UpstreamLDAPIdentityProviderI = &TestUpstreamLDAPIdentityProvider{}
|
var _ provider.UpstreamLDAPIdentityProviderI = &TestUpstreamLDAPIdentityProvider{}
|
||||||
@ -90,7 +96,7 @@ func (u *TestUpstreamLDAPIdentityProvider) GetName() string {
|
|||||||
return u.Name
|
return u.Name
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *TestUpstreamLDAPIdentityProvider) AuthenticateUser(ctx context.Context, username, password string) (*authenticator.Response, bool, error) {
|
func (u *TestUpstreamLDAPIdentityProvider) AuthenticateUser(ctx context.Context, username, password string) (*authenticators.Response, bool, error) {
|
||||||
return u.AuthenticateFunc(ctx, username, password)
|
return u.AuthenticateFunc(ctx, username, password)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -98,6 +104,34 @@ func (u *TestUpstreamLDAPIdentityProvider) GetURL() *url.URL {
|
|||||||
return u.URL
|
return u.URL
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (u *TestUpstreamLDAPIdentityProvider) PerformRefresh(ctx context.Context, userDN, expectedUsername, expectedSubject string) error {
|
||||||
|
if u.performRefreshArgs == nil {
|
||||||
|
u.performRefreshArgs = make([]*PerformRefreshArgs, 0)
|
||||||
|
}
|
||||||
|
u.performRefreshCallCount++
|
||||||
|
u.performRefreshArgs = append(u.performRefreshArgs, &PerformRefreshArgs{
|
||||||
|
Ctx: ctx,
|
||||||
|
DN: userDN,
|
||||||
|
ExpectedUsername: expectedUsername,
|
||||||
|
ExpectedSubject: expectedSubject,
|
||||||
|
})
|
||||||
|
if u.PerformRefreshErr != nil {
|
||||||
|
return u.PerformRefreshErr
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *TestUpstreamLDAPIdentityProvider) PerformRefreshCallCount() int {
|
||||||
|
return u.performRefreshCallCount
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *TestUpstreamLDAPIdentityProvider) PerformRefreshArgs(call int) *PerformRefreshArgs {
|
||||||
|
if u.performRefreshArgs == nil {
|
||||||
|
u.performRefreshArgs = make([]*PerformRefreshArgs, 0)
|
||||||
|
}
|
||||||
|
return u.performRefreshArgs[call]
|
||||||
|
}
|
||||||
|
|
||||||
type TestUpstreamOIDCIdentityProvider struct {
|
type TestUpstreamOIDCIdentityProvider struct {
|
||||||
Name string
|
Name string
|
||||||
ClientID string
|
ClientID string
|
||||||
@ -390,31 +424,54 @@ func (b *UpstreamIDPListerBuilder) RequireExactlyOneCallToPerformRefresh(
|
|||||||
t.Helper()
|
t.Helper()
|
||||||
var actualArgs *PerformRefreshArgs
|
var actualArgs *PerformRefreshArgs
|
||||||
var actualNameOfUpstreamWhichMadeCall string
|
var actualNameOfUpstreamWhichMadeCall string
|
||||||
actualCallCountAcrossAllOIDCUpstreams := 0
|
actualCallCountAcrossAllUpstreams := 0
|
||||||
for _, upstreamOIDC := range b.upstreamOIDCIdentityProviders {
|
for _, upstreamOIDC := range b.upstreamOIDCIdentityProviders {
|
||||||
callCountOnThisUpstream := upstreamOIDC.performRefreshCallCount
|
callCountOnThisUpstream := upstreamOIDC.performRefreshCallCount
|
||||||
actualCallCountAcrossAllOIDCUpstreams += callCountOnThisUpstream
|
actualCallCountAcrossAllUpstreams += callCountOnThisUpstream
|
||||||
if callCountOnThisUpstream == 1 {
|
if callCountOnThisUpstream == 1 {
|
||||||
actualNameOfUpstreamWhichMadeCall = upstreamOIDC.Name
|
actualNameOfUpstreamWhichMadeCall = upstreamOIDC.Name
|
||||||
actualArgs = upstreamOIDC.performRefreshArgs[0]
|
actualArgs = upstreamOIDC.performRefreshArgs[0]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
require.Equal(t, 1, actualCallCountAcrossAllOIDCUpstreams,
|
for _, upstreamLDAP := range b.upstreamLDAPIdentityProviders {
|
||||||
"should have been exactly one call to PerformRefresh() by all OIDC upstreams",
|
callCountOnThisUpstream := upstreamLDAP.performRefreshCallCount
|
||||||
|
actualCallCountAcrossAllUpstreams += callCountOnThisUpstream
|
||||||
|
if callCountOnThisUpstream == 1 {
|
||||||
|
actualNameOfUpstreamWhichMadeCall = upstreamLDAP.Name
|
||||||
|
actualArgs = upstreamLDAP.performRefreshArgs[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, upstreamAD := range b.upstreamActiveDirectoryIdentityProviders {
|
||||||
|
callCountOnThisUpstream := upstreamAD.performRefreshCallCount
|
||||||
|
actualCallCountAcrossAllUpstreams += callCountOnThisUpstream
|
||||||
|
if callCountOnThisUpstream == 1 {
|
||||||
|
actualNameOfUpstreamWhichMadeCall = upstreamAD.Name
|
||||||
|
actualArgs = upstreamAD.performRefreshArgs[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
require.Equal(t, 1, actualCallCountAcrossAllUpstreams,
|
||||||
|
"should have been exactly one call to PerformRefresh() by all upstreams",
|
||||||
)
|
)
|
||||||
require.Equal(t, expectedPerformedByUpstreamName, actualNameOfUpstreamWhichMadeCall,
|
require.Equal(t, expectedPerformedByUpstreamName, actualNameOfUpstreamWhichMadeCall,
|
||||||
"PerformRefresh() was called on the wrong OIDC upstream",
|
"PerformRefresh() was called on the wrong upstream",
|
||||||
)
|
)
|
||||||
require.Equal(t, expectedArgs, actualArgs)
|
require.Equal(t, expectedArgs, actualArgs)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *UpstreamIDPListerBuilder) RequireExactlyZeroCallsToPerformRefresh(t *testing.T) {
|
func (b *UpstreamIDPListerBuilder) RequireExactlyZeroCallsToPerformRefresh(t *testing.T) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
actualCallCountAcrossAllOIDCUpstreams := 0
|
actualCallCountAcrossAllUpstreams := 0
|
||||||
for _, upstreamOIDC := range b.upstreamOIDCIdentityProviders {
|
for _, upstreamOIDC := range b.upstreamOIDCIdentityProviders {
|
||||||
actualCallCountAcrossAllOIDCUpstreams += upstreamOIDC.performRefreshCallCount
|
actualCallCountAcrossAllUpstreams += upstreamOIDC.performRefreshCallCount
|
||||||
}
|
}
|
||||||
require.Equal(t, 0, actualCallCountAcrossAllOIDCUpstreams,
|
for _, upstreamLDAP := range b.upstreamLDAPIdentityProviders {
|
||||||
|
actualCallCountAcrossAllUpstreams += upstreamLDAP.performRefreshCallCount
|
||||||
|
}
|
||||||
|
for _, upstreamActiveDirectory := range b.upstreamActiveDirectoryIdentityProviders {
|
||||||
|
actualCallCountAcrossAllUpstreams += upstreamActiveDirectory.performRefreshCallCount
|
||||||
|
}
|
||||||
|
|
||||||
|
require.Equal(t, 0, actualCallCountAcrossAllUpstreams,
|
||||||
"expected exactly zero calls to PerformRefresh()",
|
"expected exactly zero calls to PerformRefresh()",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -21,12 +21,12 @@ import (
|
|||||||
"github.com/go-ldap/ldap/v3"
|
"github.com/go-ldap/ldap/v3"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"k8s.io/apimachinery/pkg/types"
|
"k8s.io/apimachinery/pkg/types"
|
||||||
"k8s.io/apiserver/pkg/authentication/authenticator"
|
|
||||||
"k8s.io/apiserver/pkg/authentication/user"
|
"k8s.io/apiserver/pkg/authentication/user"
|
||||||
"k8s.io/utils/trace"
|
"k8s.io/utils/trace"
|
||||||
|
|
||||||
"go.pinniped.dev/internal/authenticators"
|
"go.pinniped.dev/internal/authenticators"
|
||||||
"go.pinniped.dev/internal/endpointaddr"
|
"go.pinniped.dev/internal/endpointaddr"
|
||||||
|
"go.pinniped.dev/internal/oidc/downstreamsession"
|
||||||
"go.pinniped.dev/internal/oidc/provider"
|
"go.pinniped.dev/internal/oidc/provider"
|
||||||
"go.pinniped.dev/internal/plog"
|
"go.pinniped.dev/internal/plog"
|
||||||
)
|
)
|
||||||
@ -169,6 +169,73 @@ func (p *Provider) GetConfig() ProviderConfig {
|
|||||||
return p.c
|
return p.c
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *Provider) PerformRefresh(ctx context.Context, userDN, expectedUsername, expectedSubject 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
|
||||||
|
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 "%s" 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 "%s" resulted in search result without DN`, userDN)
|
||||||
|
}
|
||||||
|
|
||||||
|
newUsername, err := p.getSearchResultAttributeValue(p.c.UserSearch.UsernameAttribute, userEntry, userDN)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if newUsername != expectedUsername {
|
||||||
|
return fmt.Errorf(`searching for user "%s" returned a different username than the previous value. expected: "%s", actual: "%s"`,
|
||||||
|
userDN, expectedUsername, newUsername,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
newUID, err := p.getSearchResultAttributeRawValueEncoded(p.c.UserSearch.UIDAttribute, userEntry, userDN)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
newSubject := downstreamsession.DownstreamLDAPSubject(newUID, *p.GetURL())
|
||||||
|
if newSubject != expectedSubject {
|
||||||
|
return fmt.Errorf(`searching for user "%s" produced a different subject than the previous value. expected: "%s", actual: "%s"`, userDN, expectedSubject, newSubject)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 "%s": %w`, p.c.Host, err)
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
err = conn.Bind(p.c.BindUsername, p.c.BindPassword)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf(`error binding as "%s" before user search: %w`, p.c.BindUsername, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
searchResult, err := conn.Search(search)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf(`error searching for user "%s": %w`, userDN, err)
|
||||||
|
}
|
||||||
|
return searchResult, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (p *Provider) dial(ctx context.Context) (Conn, error) {
|
func (p *Provider) dial(ctx context.Context) (Conn, error) {
|
||||||
tlsAddr, err := endpointaddr.Parse(p.c.Host, defaultLDAPSPort)
|
tlsAddr, err := endpointaddr.Parse(p.c.Host, defaultLDAPSPort)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -310,7 +377,7 @@ func (p *Provider) TestConnection(ctx context.Context) error {
|
|||||||
// authentication for a given end user's username. It runs the same logic as AuthenticateUser except it does
|
// authentication for a given end user's username. It runs the same logic as AuthenticateUser except it does
|
||||||
// not bind as that user, so it does not test their password. It returns the same values that a real call to
|
// not bind as that user, so it does not test their password. It returns the same values that a real call to
|
||||||
// AuthenticateUser with the correct password would return.
|
// AuthenticateUser with the correct password would return.
|
||||||
func (p *Provider) DryRunAuthenticateUser(ctx context.Context, username string) (*authenticator.Response, bool, error) {
|
func (p *Provider) DryRunAuthenticateUser(ctx context.Context, username string) (*authenticators.Response, bool, error) {
|
||||||
endUserBindFunc := func(conn Conn, foundUserDN string) error {
|
endUserBindFunc := func(conn Conn, foundUserDN string) error {
|
||||||
// Act as if the end user bind always succeeds.
|
// Act as if the end user bind always succeeds.
|
||||||
return nil
|
return nil
|
||||||
@ -319,14 +386,14 @@ func (p *Provider) DryRunAuthenticateUser(ctx context.Context, username string)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Authenticate an end user and return their mapped username, groups, and UID. Implements authenticators.UserAuthenticator.
|
// Authenticate an end user and return their mapped username, groups, and UID. Implements authenticators.UserAuthenticator.
|
||||||
func (p *Provider) AuthenticateUser(ctx context.Context, username, password string) (*authenticator.Response, bool, error) {
|
func (p *Provider) AuthenticateUser(ctx context.Context, username, password string) (*authenticators.Response, bool, error) {
|
||||||
endUserBindFunc := func(conn Conn, foundUserDN string) error {
|
endUserBindFunc := func(conn Conn, foundUserDN string) error {
|
||||||
return conn.Bind(foundUserDN, password)
|
return conn.Bind(foundUserDN, password)
|
||||||
}
|
}
|
||||||
return p.authenticateUserImpl(ctx, username, endUserBindFunc)
|
return p.authenticateUserImpl(ctx, username, endUserBindFunc)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Provider) authenticateUserImpl(ctx context.Context, username string, bindFunc func(conn Conn, foundUserDN string) error) (*authenticator.Response, bool, error) {
|
func (p *Provider) authenticateUserImpl(ctx context.Context, username string, bindFunc func(conn Conn, foundUserDN string) error) (*authenticators.Response, bool, error) {
|
||||||
t := trace.FromContext(ctx).Nest("slow ldap authenticate user attempt", trace.Field{Key: "providerName", Value: p.GetName()})
|
t := trace.FromContext(ctx).Nest("slow ldap authenticate user attempt", trace.Field{Key: "providerName", Value: p.GetName()})
|
||||||
defer t.LogIfLong(500 * time.Millisecond) // to help users debug slow LDAP searches
|
defer t.LogIfLong(500 * time.Millisecond) // to help users debug slow LDAP searches
|
||||||
|
|
||||||
@ -355,24 +422,16 @@ func (p *Provider) authenticateUserImpl(ctx context.Context, username string, bi
|
|||||||
return nil, false, fmt.Errorf(`error binding as "%s" before user search: %w`, p.c.BindUsername, err)
|
return nil, false, fmt.Errorf(`error binding as "%s" before user search: %w`, p.c.BindUsername, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
mappedUsername, mappedUID, mappedGroupNames, err := p.searchAndBindUser(conn, username, bindFunc)
|
response, err := p.searchAndBindUser(conn, username, bindFunc)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
p.traceAuthFailure(t, err)
|
p.traceAuthFailure(t, err)
|
||||||
return nil, false, err
|
return nil, false, err
|
||||||
}
|
}
|
||||||
if len(mappedUsername) == 0 || len(mappedUID) == 0 {
|
if response == nil {
|
||||||
// Couldn't find the username or couldn't bind using the password.
|
|
||||||
p.traceAuthFailure(t, fmt.Errorf("bad username or password"))
|
p.traceAuthFailure(t, fmt.Errorf("bad username or password"))
|
||||||
return nil, false, nil
|
return nil, false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
response := &authenticator.Response{
|
|
||||||
User: &user.DefaultInfo{
|
|
||||||
Name: mappedUsername,
|
|
||||||
UID: mappedUID,
|
|
||||||
Groups: mappedGroupNames,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
p.traceAuthSuccess(t)
|
p.traceAuthSuccess(t)
|
||||||
return response, true, nil
|
return response, true, nil
|
||||||
}
|
}
|
||||||
@ -454,7 +513,7 @@ func (p *Provider) SearchForDefaultNamingContext(ctx context.Context) (string, e
|
|||||||
return searchBase, nil
|
return searchBase, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Provider) searchAndBindUser(conn Conn, username string, bindFunc func(conn Conn, foundUserDN string) error) (string, string, []string, error) {
|
func (p *Provider) searchAndBindUser(conn Conn, username string, bindFunc func(conn Conn, foundUserDN string) error) (*authenticators.Response, error) {
|
||||||
searchResult, err := conn.Search(p.userSearchRequest(username))
|
searchResult, err := conn.Search(p.userSearchRequest(username))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
plog.All(`error searching for user`,
|
plog.All(`error searching for user`,
|
||||||
@ -462,7 +521,7 @@ func (p *Provider) searchAndBindUser(conn Conn, username string, bindFunc func(c
|
|||||||
"username", username,
|
"username", username,
|
||||||
"err", err,
|
"err", err,
|
||||||
)
|
)
|
||||||
return "", "", nil, fmt.Errorf(`error searching for user: %w`, err)
|
return nil, fmt.Errorf(`error searching for user: %w`, err)
|
||||||
}
|
}
|
||||||
if len(searchResult.Entries) == 0 {
|
if len(searchResult.Entries) == 0 {
|
||||||
if plog.Enabled(plog.LevelAll) {
|
if plog.Enabled(plog.LevelAll) {
|
||||||
@ -473,38 +532,38 @@ func (p *Provider) searchAndBindUser(conn Conn, username string, bindFunc func(c
|
|||||||
} else {
|
} else {
|
||||||
plog.Debug("error finding user: user not found (cowardly avoiding printing username because log level is not 'all')", "upstreamName", p.GetName())
|
plog.Debug("error finding user: user not found (cowardly avoiding printing username because log level is not 'all')", "upstreamName", p.GetName())
|
||||||
}
|
}
|
||||||
return "", "", nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// At this point, we have matched at least one entry, so we can be confident that the username is not actually
|
// At this point, we have matched at least one entry, so we can be confident that the username is not actually
|
||||||
// someone's password mistakenly entered into the username field, so we can log it without concern.
|
// someone's password mistakenly entered into the username field, so we can log it without concern.
|
||||||
if len(searchResult.Entries) > 1 {
|
if len(searchResult.Entries) > 1 {
|
||||||
return "", "", nil, fmt.Errorf(`searching for user "%s" resulted in %d search results, but expected 1 result`,
|
return nil, fmt.Errorf(`searching for user "%s" resulted in %d search results, but expected 1 result`,
|
||||||
username, len(searchResult.Entries),
|
username, len(searchResult.Entries),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
userEntry := searchResult.Entries[0]
|
userEntry := searchResult.Entries[0]
|
||||||
if len(userEntry.DN) == 0 {
|
if len(userEntry.DN) == 0 {
|
||||||
return "", "", nil, fmt.Errorf(`searching for user "%s" resulted in search result without DN`, username)
|
return nil, fmt.Errorf(`searching for user "%s" resulted in search result without DN`, username)
|
||||||
}
|
}
|
||||||
|
|
||||||
mappedUsername, err := p.getSearchResultAttributeValue(p.c.UserSearch.UsernameAttribute, userEntry, username)
|
mappedUsername, err := p.getSearchResultAttributeValue(p.c.UserSearch.UsernameAttribute, userEntry, username)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", "", nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// We would like to support binary typed attributes for UIDs, so always read them as binary and encode them,
|
// We would like to support binary typed attributes for UIDs, so always read them as binary and encode them,
|
||||||
// even when the attribute may not be binary.
|
// even when the attribute may not be binary.
|
||||||
mappedUID, err := p.getSearchResultAttributeRawValueEncoded(p.c.UserSearch.UIDAttribute, userEntry, username)
|
mappedUID, err := p.getSearchResultAttributeRawValueEncoded(p.c.UserSearch.UIDAttribute, userEntry, username)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", "", nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
mappedGroupNames := []string{}
|
mappedGroupNames := []string{}
|
||||||
if len(p.c.GroupSearch.Base) > 0 {
|
if len(p.c.GroupSearch.Base) > 0 {
|
||||||
mappedGroupNames, err = p.searchGroupsForUserDN(conn, userEntry.DN)
|
mappedGroupNames, err = p.searchGroupsForUserDN(conn, userEntry.DN)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", "", nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
sort.Strings(mappedGroupNames)
|
sort.Strings(mappedGroupNames)
|
||||||
@ -516,12 +575,26 @@ func (p *Provider) searchAndBindUser(conn Conn, username string, bindFunc func(c
|
|||||||
err, "upstreamName", p.GetName(), "username", username, "dn", userEntry.DN)
|
err, "upstreamName", p.GetName(), "username", username, "dn", userEntry.DN)
|
||||||
ldapErr := &ldap.Error{}
|
ldapErr := &ldap.Error{}
|
||||||
if errors.As(err, &ldapErr) && ldapErr.ResultCode == ldap.LDAPResultInvalidCredentials {
|
if errors.As(err, &ldapErr) && ldapErr.ResultCode == ldap.LDAPResultInvalidCredentials {
|
||||||
return "", "", nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
return "", "", nil, fmt.Errorf(`error binding for user "%s" using provided password against DN "%s": %w`, username, userEntry.DN, err)
|
return nil, fmt.Errorf(`error binding for user "%s" using provided password against DN "%s": %w`, username, userEntry.DN, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return mappedUsername, mappedUID, mappedGroupNames, nil
|
if len(mappedUsername) == 0 || len(mappedUID) == 0 {
|
||||||
|
// Couldn't find the username or couldn't bind using the password.
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
response := &authenticators.Response{
|
||||||
|
User: &user.DefaultInfo{
|
||||||
|
Name: mappedUsername,
|
||||||
|
UID: mappedUID,
|
||||||
|
Groups: mappedGroupNames,
|
||||||
|
},
|
||||||
|
DN: userEntry.DN,
|
||||||
|
}
|
||||||
|
|
||||||
|
return response, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Provider) defaultNamingContextRequest() *ldap.SearchRequest {
|
func (p *Provider) defaultNamingContextRequest() *ldap.SearchRequest {
|
||||||
@ -568,6 +641,21 @@ func (p *Provider) groupSearchRequest(userDN string) *ldap.SearchRequest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *Provider) refreshUserSearchRequest(dn string) *ldap.SearchRequest {
|
||||||
|
// See https://ldap.com/the-ldap-search-operation for general documentation of LDAP search options.
|
||||||
|
return &ldap.SearchRequest{
|
||||||
|
BaseDN: dn,
|
||||||
|
Scope: ldap.ScopeBaseObject,
|
||||||
|
DerefAliases: ldap.NeverDerefAliases,
|
||||||
|
SizeLimit: 2,
|
||||||
|
TimeLimit: 90,
|
||||||
|
TypesOnly: false,
|
||||||
|
Filter: "(objectClass=*)", // we already have the dn, so the filter doesn't matter
|
||||||
|
Attributes: p.userSearchRequestedAttributes(),
|
||||||
|
Controls: nil, // this could be used to enable paging, but we're already limiting the result max size
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (p *Provider) userSearchRequestedAttributes() []string {
|
func (p *Provider) userSearchRequestedAttributes() []string {
|
||||||
attributes := []string{}
|
attributes := []string{}
|
||||||
if p.c.UserSearch.UsernameAttribute != distinguishedNameAttributeName {
|
if p.c.UserSearch.UsernameAttribute != distinguishedNameAttributeName {
|
||||||
@ -687,6 +775,12 @@ func (p *Provider) traceSearchBaseDiscoveryFailure(t *trace.Trace, err error) {
|
|||||||
trace.Field{Key: "reason", Value: err.Error()})
|
trace.Field{Key: "reason", Value: err.Error()})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *Provider) traceRefreshFailure(t *trace.Trace, err error) {
|
||||||
|
t.Step("refresh failed",
|
||||||
|
trace.Field{Key: "reason", Value: err.Error()},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
func MicrosoftUUIDFromBinary(attributeName string) func(entry *ldap.Entry) (string, error) {
|
func MicrosoftUUIDFromBinary(attributeName string) func(entry *ldap.Entry) (string, error) {
|
||||||
// validation has already been done so we can just get the attribute...
|
// validation has already been done so we can just get the attribute...
|
||||||
return func(entry *ldap.Entry) (string, error) {
|
return func(entry *ldap.Entry) (string, error) {
|
||||||
|
@ -18,9 +18,9 @@ import (
|
|||||||
"github.com/go-ldap/ldap/v3"
|
"github.com/go-ldap/ldap/v3"
|
||||||
"github.com/golang/mock/gomock"
|
"github.com/golang/mock/gomock"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"k8s.io/apiserver/pkg/authentication/authenticator"
|
|
||||||
"k8s.io/apiserver/pkg/authentication/user"
|
"k8s.io/apiserver/pkg/authentication/user"
|
||||||
|
|
||||||
|
"go.pinniped.dev/internal/authenticators"
|
||||||
"go.pinniped.dev/internal/certauthority"
|
"go.pinniped.dev/internal/certauthority"
|
||||||
"go.pinniped.dev/internal/endpointaddr"
|
"go.pinniped.dev/internal/endpointaddr"
|
||||||
"go.pinniped.dev/internal/mocks/mockldapconn"
|
"go.pinniped.dev/internal/mocks/mockldapconn"
|
||||||
@ -151,7 +151,7 @@ func TestEndUserAuthentication(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// The auth response which matches the exampleUserSearchResult and exampleGroupSearchResult.
|
// The auth response which matches the exampleUserSearchResult and exampleGroupSearchResult.
|
||||||
expectedAuthResponse := func(editFunc func(r *user.DefaultInfo)) *authenticator.Response {
|
expectedAuthResponse := func(editFunc func(r *user.DefaultInfo)) *authenticators.Response {
|
||||||
u := &user.DefaultInfo{
|
u := &user.DefaultInfo{
|
||||||
Name: testUserSearchResultUsernameAttributeValue,
|
Name: testUserSearchResultUsernameAttributeValue,
|
||||||
UID: base64.RawURLEncoding.EncodeToString([]byte(testUserSearchResultUIDAttributeValue)),
|
UID: base64.RawURLEncoding.EncodeToString([]byte(testUserSearchResultUIDAttributeValue)),
|
||||||
@ -160,7 +160,7 @@ func TestEndUserAuthentication(t *testing.T) {
|
|||||||
if editFunc != nil {
|
if editFunc != nil {
|
||||||
editFunc(u)
|
editFunc(u)
|
||||||
}
|
}
|
||||||
return &authenticator.Response{User: u}
|
return &authenticators.Response{User: u, DN: testUserSearchResultDNValue}
|
||||||
}
|
}
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
@ -173,7 +173,7 @@ func TestEndUserAuthentication(t *testing.T) {
|
|||||||
dialError error
|
dialError error
|
||||||
wantError string
|
wantError string
|
||||||
wantToSkipDial bool
|
wantToSkipDial bool
|
||||||
wantAuthResponse *authenticator.Response
|
wantAuthResponse *authenticators.Response
|
||||||
wantUnauthenticated bool
|
wantUnauthenticated bool
|
||||||
skipDryRunAuthenticateUser bool // tests about when the end user bind fails don't make sense for DryRunAuthenticateUser()
|
skipDryRunAuthenticateUser bool // tests about when the end user bind fails don't make sense for DryRunAuthenticateUser()
|
||||||
}{
|
}{
|
||||||
@ -498,12 +498,13 @@ func TestEndUserAuthentication(t *testing.T) {
|
|||||||
bindEndUserMocks: func(conn *mockldapconn.MockConn) {
|
bindEndUserMocks: func(conn *mockldapconn.MockConn) {
|
||||||
conn.EXPECT().Bind(testUserSearchResultDNValue, testUpstreamPassword).Times(1)
|
conn.EXPECT().Bind(testUserSearchResultDNValue, testUpstreamPassword).Times(1)
|
||||||
},
|
},
|
||||||
wantAuthResponse: &authenticator.Response{
|
wantAuthResponse: &authenticators.Response{
|
||||||
User: &user.DefaultInfo{
|
User: &user.DefaultInfo{
|
||||||
Name: testUserSearchResultUsernameAttributeValue,
|
Name: testUserSearchResultUsernameAttributeValue,
|
||||||
UID: base64.RawURLEncoding.EncodeToString([]byte(testUserSearchResultUIDAttributeValue)),
|
UID: base64.RawURLEncoding.EncodeToString([]byte(testUserSearchResultUIDAttributeValue)),
|
||||||
Groups: []string{"a", "b", "c"},
|
Groups: []string{"a", "b", "c"},
|
||||||
},
|
},
|
||||||
|
DN: testUserSearchResultDNValue,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -1212,6 +1213,340 @@ func TestEndUserAuthentication(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestUpstreamRefresh(t *testing.T) {
|
||||||
|
expectedUserSearch := &ldap.SearchRequest{
|
||||||
|
BaseDN: testUserSearchResultDNValue,
|
||||||
|
Scope: ldap.ScopeBaseObject,
|
||||||
|
DerefAliases: ldap.NeverDerefAliases,
|
||||||
|
SizeLimit: 2,
|
||||||
|
TimeLimit: 90,
|
||||||
|
TypesOnly: false,
|
||||||
|
Filter: "(objectClass=*)",
|
||||||
|
Attributes: []string{testUserSearchUsernameAttribute, testUserSearchUIDAttribute},
|
||||||
|
Controls: nil, // don't need paging because we set the SizeLimit so small
|
||||||
|
}
|
||||||
|
|
||||||
|
happyPathUserSearchResult := &ldap.SearchResult{
|
||||||
|
Entries: []*ldap.Entry{
|
||||||
|
{
|
||||||
|
DN: testUserSearchResultDNValue,
|
||||||
|
Attributes: []*ldap.EntryAttribute{
|
||||||
|
{
|
||||||
|
Name: testUserSearchUsernameAttribute,
|
||||||
|
Values: []string{testUserSearchResultUsernameAttributeValue},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: testUserSearchUIDAttribute,
|
||||||
|
ByteValues: [][]byte{[]byte(testUserSearchResultUIDAttributeValue)},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
providerConfig *ProviderConfig
|
||||||
|
setupMocks func(conn *mockldapconn.MockConn)
|
||||||
|
dialError error
|
||||||
|
wantErr string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "happy path where searching the dn returns a single entry",
|
||||||
|
providerConfig: providerConfig,
|
||||||
|
setupMocks: func(conn *mockldapconn.MockConn) {
|
||||||
|
conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1)
|
||||||
|
conn.EXPECT().Search(expectedUserSearch).Return(happyPathUserSearchResult, nil).Times(1)
|
||||||
|
conn.EXPECT().Close().Times(1)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "error where dial fails",
|
||||||
|
providerConfig: providerConfig,
|
||||||
|
dialError: errors.New("some dial error"),
|
||||||
|
wantErr: "error dialing host \"ldap.example.com:8443\": some dial error",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "error binding",
|
||||||
|
providerConfig: providerConfig,
|
||||||
|
setupMocks: func(conn *mockldapconn.MockConn) {
|
||||||
|
conn.EXPECT().Bind(testBindUsername, testBindPassword).Return(errors.New("some bind error")).Times(1)
|
||||||
|
conn.EXPECT().Close().Times(1)
|
||||||
|
},
|
||||||
|
wantErr: "error binding as \"cn=some-bind-username,dc=pinniped,dc=dev\" before user search: some bind error",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "search result returns no entries",
|
||||||
|
providerConfig: providerConfig,
|
||||||
|
setupMocks: func(conn *mockldapconn.MockConn) {
|
||||||
|
conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1)
|
||||||
|
conn.EXPECT().Search(expectedUserSearch).Return(&ldap.SearchResult{
|
||||||
|
Entries: []*ldap.Entry{},
|
||||||
|
}, nil).Times(1)
|
||||||
|
conn.EXPECT().Close().Times(1)
|
||||||
|
},
|
||||||
|
wantErr: "searching for user \"some-upstream-user-dn\" resulted in 0 search results, but expected 1 result",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "error searching",
|
||||||
|
providerConfig: providerConfig,
|
||||||
|
setupMocks: func(conn *mockldapconn.MockConn) {
|
||||||
|
conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1)
|
||||||
|
conn.EXPECT().Search(expectedUserSearch).Return(nil, errors.New("some search error"))
|
||||||
|
conn.EXPECT().Close().Times(1)
|
||||||
|
},
|
||||||
|
wantErr: "error searching for user \"some-upstream-user-dn\": some search error",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "search result returns more than one entry",
|
||||||
|
providerConfig: providerConfig,
|
||||||
|
setupMocks: func(conn *mockldapconn.MockConn) {
|
||||||
|
conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1)
|
||||||
|
conn.EXPECT().Search(expectedUserSearch).Return(&ldap.SearchResult{
|
||||||
|
Entries: []*ldap.Entry{
|
||||||
|
{
|
||||||
|
DN: testUserSearchResultDNValue,
|
||||||
|
Attributes: []*ldap.EntryAttribute{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
DN: "doesn't-matter",
|
||||||
|
Attributes: []*ldap.EntryAttribute{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, nil).Times(1)
|
||||||
|
conn.EXPECT().Close().Times(1)
|
||||||
|
},
|
||||||
|
wantErr: "searching for user \"some-upstream-user-dn\" resulted in 2 search results, but expected 1 result",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "search result has wrong uid",
|
||||||
|
providerConfig: providerConfig,
|
||||||
|
setupMocks: func(conn *mockldapconn.MockConn) {
|
||||||
|
conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1)
|
||||||
|
conn.EXPECT().Search(expectedUserSearch).Return(&ldap.SearchResult{
|
||||||
|
Entries: []*ldap.Entry{
|
||||||
|
{
|
||||||
|
DN: testUserSearchResultDNValue,
|
||||||
|
Attributes: []*ldap.EntryAttribute{
|
||||||
|
{
|
||||||
|
Name: testUserSearchUsernameAttribute,
|
||||||
|
Values: []string{testUserSearchResultUsernameAttributeValue},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: testUserSearchUIDAttribute,
|
||||||
|
ByteValues: [][]byte{[]byte("wrong-uid")},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, nil).Times(1)
|
||||||
|
conn.EXPECT().Close().Times(1)
|
||||||
|
},
|
||||||
|
wantErr: "searching for user \"some-upstream-user-dn\" produced a different subject than the previous value. expected: \"ldaps://ldap.example.com:8443?base=some-upstream-user-base-dn&sub=c29tZS11cHN0cmVhbS11aWQtdmFsdWU\", actual: \"ldaps://ldap.example.com:8443?base=some-upstream-user-base-dn&sub=d3JvbmctdWlk\"",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "search result has wrong username",
|
||||||
|
providerConfig: providerConfig,
|
||||||
|
setupMocks: func(conn *mockldapconn.MockConn) {
|
||||||
|
conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1)
|
||||||
|
conn.EXPECT().Search(expectedUserSearch).Return(&ldap.SearchResult{
|
||||||
|
Entries: []*ldap.Entry{
|
||||||
|
{
|
||||||
|
DN: testUserSearchResultDNValue,
|
||||||
|
Attributes: []*ldap.EntryAttribute{
|
||||||
|
{
|
||||||
|
Name: testUserSearchUsernameAttribute,
|
||||||
|
Values: []string{"wrong-username"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, nil).Times(1)
|
||||||
|
conn.EXPECT().Close().Times(1)
|
||||||
|
},
|
||||||
|
wantErr: "searching for user \"some-upstream-user-dn\" returned a different username than the previous value. expected: \"some-upstream-username-value\", actual: \"wrong-username\"",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "search result has no dn",
|
||||||
|
providerConfig: providerConfig,
|
||||||
|
setupMocks: func(conn *mockldapconn.MockConn) {
|
||||||
|
conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1)
|
||||||
|
conn.EXPECT().Search(expectedUserSearch).Return(&ldap.SearchResult{
|
||||||
|
Entries: []*ldap.Entry{
|
||||||
|
{
|
||||||
|
Attributes: []*ldap.EntryAttribute{
|
||||||
|
{
|
||||||
|
Name: testUserSearchUsernameAttribute,
|
||||||
|
Values: []string{testUserSearchResultUsernameAttributeValue},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: testUserSearchUIDAttribute,
|
||||||
|
ByteValues: [][]byte{[]byte(testUserSearchResultUIDAttributeValue)},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, nil).Times(1)
|
||||||
|
conn.EXPECT().Close().Times(1)
|
||||||
|
},
|
||||||
|
wantErr: "searching for user with original DN \"some-upstream-user-dn\" resulted in search result without DN",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "search result has 0 values for username attribute",
|
||||||
|
providerConfig: providerConfig,
|
||||||
|
setupMocks: func(conn *mockldapconn.MockConn) {
|
||||||
|
conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1)
|
||||||
|
conn.EXPECT().Search(expectedUserSearch).Return(&ldap.SearchResult{
|
||||||
|
Entries: []*ldap.Entry{
|
||||||
|
{
|
||||||
|
DN: testUserSearchResultDNValue,
|
||||||
|
Attributes: []*ldap.EntryAttribute{
|
||||||
|
{
|
||||||
|
Name: testUserSearchUsernameAttribute,
|
||||||
|
Values: []string{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: testUserSearchUIDAttribute,
|
||||||
|
ByteValues: [][]byte{[]byte(testUserSearchResultUIDAttributeValue)},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, nil).Times(1)
|
||||||
|
conn.EXPECT().Close().Times(1)
|
||||||
|
},
|
||||||
|
wantErr: "found 0 values for attribute \"some-upstream-username-attribute\" while searching for user \"some-upstream-user-dn\", but expected 1 result",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "search result has more than one value for username attribute",
|
||||||
|
providerConfig: providerConfig,
|
||||||
|
setupMocks: func(conn *mockldapconn.MockConn) {
|
||||||
|
conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1)
|
||||||
|
conn.EXPECT().Search(expectedUserSearch).Return(&ldap.SearchResult{
|
||||||
|
Entries: []*ldap.Entry{
|
||||||
|
{
|
||||||
|
DN: testUserSearchResultDNValue,
|
||||||
|
Attributes: []*ldap.EntryAttribute{
|
||||||
|
{
|
||||||
|
Name: testUserSearchUsernameAttribute,
|
||||||
|
Values: []string{testUserSearchResultUsernameAttributeValue, "something-else"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: testUserSearchUIDAttribute,
|
||||||
|
ByteValues: [][]byte{[]byte(testUserSearchResultUIDAttributeValue)},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, nil).Times(1)
|
||||||
|
conn.EXPECT().Close().Times(1)
|
||||||
|
},
|
||||||
|
wantErr: "found 2 values for attribute \"some-upstream-username-attribute\" while searching for user \"some-upstream-user-dn\", but expected 1 result",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "search result has 0 values for uid attribute",
|
||||||
|
providerConfig: providerConfig,
|
||||||
|
setupMocks: func(conn *mockldapconn.MockConn) {
|
||||||
|
conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1)
|
||||||
|
conn.EXPECT().Search(expectedUserSearch).Return(&ldap.SearchResult{
|
||||||
|
Entries: []*ldap.Entry{
|
||||||
|
{
|
||||||
|
DN: testUserSearchResultDNValue,
|
||||||
|
Attributes: []*ldap.EntryAttribute{
|
||||||
|
{
|
||||||
|
Name: testUserSearchUsernameAttribute,
|
||||||
|
Values: []string{testUserSearchResultUsernameAttributeValue},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: testUserSearchUIDAttribute,
|
||||||
|
ByteValues: [][]byte{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, nil).Times(1)
|
||||||
|
conn.EXPECT().Close().Times(1)
|
||||||
|
},
|
||||||
|
wantErr: "found 0 values for attribute \"some-upstream-uid-attribute\" while searching for user \"some-upstream-user-dn\", but expected 1 result",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "search result has 2 values for uid attribute",
|
||||||
|
providerConfig: providerConfig,
|
||||||
|
setupMocks: func(conn *mockldapconn.MockConn) {
|
||||||
|
conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1)
|
||||||
|
conn.EXPECT().Search(expectedUserSearch).Return(&ldap.SearchResult{
|
||||||
|
Entries: []*ldap.Entry{
|
||||||
|
{
|
||||||
|
DN: testUserSearchResultDNValue,
|
||||||
|
Attributes: []*ldap.EntryAttribute{
|
||||||
|
{
|
||||||
|
Name: testUserSearchUsernameAttribute,
|
||||||
|
Values: []string{testUserSearchResultUsernameAttributeValue},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: testUserSearchUIDAttribute,
|
||||||
|
ByteValues: [][]byte{[]byte(testUserSearchResultUIDAttributeValue), []byte("other-uid-value")},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, nil).Times(1)
|
||||||
|
conn.EXPECT().Close().Times(1)
|
||||||
|
},
|
||||||
|
wantErr: "found 2 values for attribute \"some-upstream-uid-attribute\" while searching for user \"some-upstream-user-dn\", but expected 1 result",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
tt := tt
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
ctrl := gomock.NewController(t)
|
||||||
|
t.Cleanup(ctrl.Finish)
|
||||||
|
|
||||||
|
conn := mockldapconn.NewMockConn(ctrl)
|
||||||
|
if tt.setupMocks != nil {
|
||||||
|
tt.setupMocks(conn)
|
||||||
|
}
|
||||||
|
|
||||||
|
dialWasAttempted := false
|
||||||
|
providerConfig.Dialer = LDAPDialerFunc(func(ctx context.Context, addr endpointaddr.HostPort) (Conn, error) {
|
||||||
|
dialWasAttempted = true
|
||||||
|
require.Equal(t, providerConfig.Host, addr.Endpoint())
|
||||||
|
if tt.dialError != nil {
|
||||||
|
return nil, tt.dialError
|
||||||
|
}
|
||||||
|
|
||||||
|
return conn, nil
|
||||||
|
})
|
||||||
|
|
||||||
|
provider := New(*providerConfig)
|
||||||
|
subject := "ldaps://ldap.example.com:8443?base=some-upstream-user-base-dn&sub=c29tZS11cHN0cmVhbS11aWQtdmFsdWU"
|
||||||
|
err := provider.PerformRefresh(context.Background(), testUserSearchResultDNValue, testUserSearchResultUsernameAttributeValue, subject)
|
||||||
|
if tt.wantErr != "" {
|
||||||
|
require.Error(t, err)
|
||||||
|
require.Equal(t, tt.wantErr, err.Error())
|
||||||
|
} else {
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
require.Equal(t, true, dialWasAttempted)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestTestConnection(t *testing.T) {
|
func TestTestConnection(t *testing.T) {
|
||||||
providerConfig := func(editFunc func(p *ProviderConfig)) *ProviderConfig {
|
providerConfig := func(editFunc func(p *ProviderConfig)) *ProviderConfig {
|
||||||
config := &ProviderConfig{
|
config := &ProviderConfig{
|
||||||
|
@ -17,9 +17,9 @@ import (
|
|||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"k8s.io/apiserver/pkg/authentication/authenticator"
|
|
||||||
"k8s.io/apiserver/pkg/authentication/user"
|
"k8s.io/apiserver/pkg/authentication/user"
|
||||||
|
|
||||||
|
"go.pinniped.dev/internal/authenticators"
|
||||||
"go.pinniped.dev/internal/upstreamldap"
|
"go.pinniped.dev/internal/upstreamldap"
|
||||||
"go.pinniped.dev/test/testlib"
|
"go.pinniped.dev/test/testlib"
|
||||||
)
|
)
|
||||||
@ -74,7 +74,7 @@ func TestLDAPSearch_Parallel(t *testing.T) {
|
|||||||
password string
|
password string
|
||||||
provider *upstreamldap.Provider
|
provider *upstreamldap.Provider
|
||||||
wantError string
|
wantError string
|
||||||
wantAuthResponse *authenticator.Response
|
wantAuthResponse *authenticators.Response
|
||||||
wantUnauthenticated bool
|
wantUnauthenticated bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
@ -82,8 +82,8 @@ func TestLDAPSearch_Parallel(t *testing.T) {
|
|||||||
username: "pinny",
|
username: "pinny",
|
||||||
password: pinnyPassword,
|
password: pinnyPassword,
|
||||||
provider: upstreamldap.New(*providerConfig(nil)),
|
provider: upstreamldap.New(*providerConfig(nil)),
|
||||||
wantAuthResponse: &authenticator.Response{
|
wantAuthResponse: &authenticators.Response{
|
||||||
User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: []string{"ball-game-players", "seals"}},
|
User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: []string{"ball-game-players", "seals"}}, DN: "cn=pinny,ou=users,dc=pinniped,dc=dev",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -94,8 +94,8 @@ func TestLDAPSearch_Parallel(t *testing.T) {
|
|||||||
p.Host = "127.0.0.1:" + ldapLocalhostPort
|
p.Host = "127.0.0.1:" + ldapLocalhostPort
|
||||||
p.ConnectionProtocol = upstreamldap.StartTLS
|
p.ConnectionProtocol = upstreamldap.StartTLS
|
||||||
})),
|
})),
|
||||||
wantAuthResponse: &authenticator.Response{
|
wantAuthResponse: &authenticators.Response{
|
||||||
User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: []string{"ball-game-players", "seals"}},
|
User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: []string{"ball-game-players", "seals"}}, DN: "cn=pinny,ou=users,dc=pinniped,dc=dev",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -103,8 +103,8 @@ func TestLDAPSearch_Parallel(t *testing.T) {
|
|||||||
username: "pinny",
|
username: "pinny",
|
||||||
password: pinnyPassword,
|
password: pinnyPassword,
|
||||||
provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) { p.UserSearch.Base = "dc=pinniped,dc=dev" })),
|
provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) { p.UserSearch.Base = "dc=pinniped,dc=dev" })),
|
||||||
wantAuthResponse: &authenticator.Response{
|
wantAuthResponse: &authenticators.Response{
|
||||||
User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: []string{"ball-game-players", "seals"}},
|
User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: []string{"ball-game-players", "seals"}}, DN: "cn=pinny,ou=users,dc=pinniped,dc=dev",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -112,8 +112,8 @@ func TestLDAPSearch_Parallel(t *testing.T) {
|
|||||||
username: "pinny",
|
username: "pinny",
|
||||||
password: pinnyPassword,
|
password: pinnyPassword,
|
||||||
provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) { p.UserSearch.Filter = "(cn={})" })),
|
provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) { p.UserSearch.Filter = "(cn={})" })),
|
||||||
wantAuthResponse: &authenticator.Response{
|
wantAuthResponse: &authenticators.Response{
|
||||||
User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: []string{"ball-game-players", "seals"}},
|
User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: []string{"ball-game-players", "seals"}}, DN: "cn=pinny,ou=users,dc=pinniped,dc=dev",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -124,8 +124,8 @@ func TestLDAPSearch_Parallel(t *testing.T) {
|
|||||||
p.UserSearch.UsernameAttribute = "dn"
|
p.UserSearch.UsernameAttribute = "dn"
|
||||||
p.UserSearch.Filter = "cn={}"
|
p.UserSearch.Filter = "cn={}"
|
||||||
})),
|
})),
|
||||||
wantAuthResponse: &authenticator.Response{
|
wantAuthResponse: &authenticators.Response{
|
||||||
User: &user.DefaultInfo{Name: "cn=pinny,ou=users,dc=pinniped,dc=dev", UID: b64("1000"), Groups: []string{"ball-game-players", "seals"}},
|
User: &user.DefaultInfo{Name: "cn=pinny,ou=users,dc=pinniped,dc=dev", UID: b64("1000"), Groups: []string{"ball-game-players", "seals"}}, DN: "cn=pinny,ou=users,dc=pinniped,dc=dev",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -135,8 +135,8 @@ func TestLDAPSearch_Parallel(t *testing.T) {
|
|||||||
provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) {
|
provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) {
|
||||||
p.UserSearch.Filter = "(|(cn={})(mail={}))"
|
p.UserSearch.Filter = "(|(cn={})(mail={}))"
|
||||||
})),
|
})),
|
||||||
wantAuthResponse: &authenticator.Response{
|
wantAuthResponse: &authenticators.Response{
|
||||||
User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: []string{"ball-game-players", "seals"}},
|
User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: []string{"ball-game-players", "seals"}}, DN: "cn=pinny,ou=users,dc=pinniped,dc=dev",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -146,8 +146,8 @@ func TestLDAPSearch_Parallel(t *testing.T) {
|
|||||||
provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) {
|
provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) {
|
||||||
p.UserSearch.Filter = "(|(cn={})(mail={}))"
|
p.UserSearch.Filter = "(|(cn={})(mail={}))"
|
||||||
})),
|
})),
|
||||||
wantAuthResponse: &authenticator.Response{
|
wantAuthResponse: &authenticators.Response{
|
||||||
User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: []string{"ball-game-players", "seals"}},
|
User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: []string{"ball-game-players", "seals"}}, DN: "cn=pinny,ou=users,dc=pinniped,dc=dev",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -155,8 +155,8 @@ func TestLDAPSearch_Parallel(t *testing.T) {
|
|||||||
username: "pinny",
|
username: "pinny",
|
||||||
password: pinnyPassword,
|
password: pinnyPassword,
|
||||||
provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) { p.UserSearch.UIDAttribute = "dn" })),
|
provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) { p.UserSearch.UIDAttribute = "dn" })),
|
||||||
wantAuthResponse: &authenticator.Response{
|
wantAuthResponse: &authenticators.Response{
|
||||||
User: &user.DefaultInfo{Name: "pinny", UID: b64("cn=pinny,ou=users,dc=pinniped,dc=dev"), Groups: []string{"ball-game-players", "seals"}},
|
User: &user.DefaultInfo{Name: "pinny", UID: b64("cn=pinny,ou=users,dc=pinniped,dc=dev"), Groups: []string{"ball-game-players", "seals"}}, DN: "cn=pinny,ou=users,dc=pinniped,dc=dev",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -164,8 +164,8 @@ func TestLDAPSearch_Parallel(t *testing.T) {
|
|||||||
username: "pinny",
|
username: "pinny",
|
||||||
password: pinnyPassword,
|
password: pinnyPassword,
|
||||||
provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) { p.UserSearch.UIDAttribute = "sn" })),
|
provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) { p.UserSearch.UIDAttribute = "sn" })),
|
||||||
wantAuthResponse: &authenticator.Response{
|
wantAuthResponse: &authenticators.Response{
|
||||||
User: &user.DefaultInfo{Name: "pinny", UID: b64("Seal"), Groups: []string{"ball-game-players", "seals"}},
|
User: &user.DefaultInfo{Name: "pinny", UID: b64("Seal"), Groups: []string{"ball-game-players", "seals"}}, DN: "cn=pinny,ou=users,dc=pinniped,dc=dev",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -173,8 +173,8 @@ func TestLDAPSearch_Parallel(t *testing.T) {
|
|||||||
username: "seAl", // note that this is not case-sensitive! sn=Seal. The server decides which fields are compared case-sensitive.
|
username: "seAl", // note that this is not case-sensitive! sn=Seal. The server decides which fields are compared case-sensitive.
|
||||||
password: pinnyPassword,
|
password: pinnyPassword,
|
||||||
provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) { p.UserSearch.UsernameAttribute = "sn" })),
|
provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) { p.UserSearch.UsernameAttribute = "sn" })),
|
||||||
wantAuthResponse: &authenticator.Response{
|
wantAuthResponse: &authenticators.Response{
|
||||||
User: &user.DefaultInfo{Name: "Seal", UID: b64("1000"), Groups: []string{"ball-game-players", "seals"}}, // note that the final answer has case preserved from the entry
|
User: &user.DefaultInfo{Name: "Seal", UID: b64("1000"), Groups: []string{"ball-game-players", "seals"}}, DN: "cn=pinny,ou=users,dc=pinniped,dc=dev", // note that the final answer has case preserved from the entry
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -186,8 +186,8 @@ func TestLDAPSearch_Parallel(t *testing.T) {
|
|||||||
p.UserSearch.UsernameAttribute = "givenName"
|
p.UserSearch.UsernameAttribute = "givenName"
|
||||||
p.UserSearch.UIDAttribute = "givenName"
|
p.UserSearch.UIDAttribute = "givenName"
|
||||||
})),
|
})),
|
||||||
wantAuthResponse: &authenticator.Response{
|
wantAuthResponse: &authenticators.Response{
|
||||||
User: &user.DefaultInfo{Name: "Pinny the 🦭", UID: b64("Pinny the 🦭"), Groups: []string{"ball-game-players", "seals"}},
|
User: &user.DefaultInfo{Name: "Pinny the 🦭", UID: b64("Pinny the 🦭"), Groups: []string{"ball-game-players", "seals"}}, DN: "cn=pinny,ou=users,dc=pinniped,dc=dev",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -198,8 +198,8 @@ func TestLDAPSearch_Parallel(t *testing.T) {
|
|||||||
p.UserSearch.Filter = "givenName={}"
|
p.UserSearch.Filter = "givenName={}"
|
||||||
p.UserSearch.UsernameAttribute = "cn"
|
p.UserSearch.UsernameAttribute = "cn"
|
||||||
})),
|
})),
|
||||||
wantAuthResponse: &authenticator.Response{
|
wantAuthResponse: &authenticators.Response{
|
||||||
User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: []string{"ball-game-players", "seals"}},
|
User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: []string{"ball-game-players", "seals"}}, DN: "cn=pinny,ou=users,dc=pinniped,dc=dev",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -219,8 +219,8 @@ func TestLDAPSearch_Parallel(t *testing.T) {
|
|||||||
provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) {
|
provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) {
|
||||||
p.GroupSearch.Base = ""
|
p.GroupSearch.Base = ""
|
||||||
})),
|
})),
|
||||||
wantAuthResponse: &authenticator.Response{
|
wantAuthResponse: &authenticators.Response{
|
||||||
User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: []string{}},
|
User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: []string{}}, DN: "cn=pinny,ou=users,dc=pinniped,dc=dev",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -230,8 +230,8 @@ func TestLDAPSearch_Parallel(t *testing.T) {
|
|||||||
provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) {
|
provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) {
|
||||||
p.GroupSearch.Base = "ou=users,dc=pinniped,dc=dev" // there are no groups under this part of the tree
|
p.GroupSearch.Base = "ou=users,dc=pinniped,dc=dev" // there are no groups under this part of the tree
|
||||||
})),
|
})),
|
||||||
wantAuthResponse: &authenticator.Response{
|
wantAuthResponse: &authenticators.Response{
|
||||||
User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: []string{}},
|
User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: []string{}}, DN: "cn=pinny,ou=users,dc=pinniped,dc=dev",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -241,11 +241,11 @@ func TestLDAPSearch_Parallel(t *testing.T) {
|
|||||||
provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) {
|
provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) {
|
||||||
p.GroupSearch.GroupNameAttribute = "dn"
|
p.GroupSearch.GroupNameAttribute = "dn"
|
||||||
})),
|
})),
|
||||||
wantAuthResponse: &authenticator.Response{
|
wantAuthResponse: &authenticators.Response{
|
||||||
User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: []string{
|
User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: []string{
|
||||||
"cn=ball-game-players,ou=beach-groups,ou=groups,dc=pinniped,dc=dev",
|
"cn=ball-game-players,ou=beach-groups,ou=groups,dc=pinniped,dc=dev",
|
||||||
"cn=seals,ou=groups,dc=pinniped,dc=dev",
|
"cn=seals,ou=groups,dc=pinniped,dc=dev",
|
||||||
}},
|
}}, DN: "cn=pinny,ou=users,dc=pinniped,dc=dev",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -255,11 +255,11 @@ func TestLDAPSearch_Parallel(t *testing.T) {
|
|||||||
provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) {
|
provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) {
|
||||||
p.GroupSearch.GroupNameAttribute = ""
|
p.GroupSearch.GroupNameAttribute = ""
|
||||||
})),
|
})),
|
||||||
wantAuthResponse: &authenticator.Response{
|
wantAuthResponse: &authenticators.Response{
|
||||||
User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: []string{
|
User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: []string{
|
||||||
"cn=ball-game-players,ou=beach-groups,ou=groups,dc=pinniped,dc=dev",
|
"cn=ball-game-players,ou=beach-groups,ou=groups,dc=pinniped,dc=dev",
|
||||||
"cn=seals,ou=groups,dc=pinniped,dc=dev",
|
"cn=seals,ou=groups,dc=pinniped,dc=dev",
|
||||||
}},
|
}}, DN: "cn=pinny,ou=users,dc=pinniped,dc=dev",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -269,8 +269,8 @@ func TestLDAPSearch_Parallel(t *testing.T) {
|
|||||||
provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) {
|
provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) {
|
||||||
p.GroupSearch.GroupNameAttribute = "objectClass" // silly example, but still a meaningful test
|
p.GroupSearch.GroupNameAttribute = "objectClass" // silly example, but still a meaningful test
|
||||||
})),
|
})),
|
||||||
wantAuthResponse: &authenticator.Response{
|
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", "groupOfNames"}}, DN: "cn=pinny,ou=users,dc=pinniped,dc=dev",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -280,8 +280,8 @@ func TestLDAPSearch_Parallel(t *testing.T) {
|
|||||||
provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) {
|
provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) {
|
||||||
p.GroupSearch.Filter = "(&(&(objectClass=groupOfNames)(member={}))(cn=seals))"
|
p.GroupSearch.Filter = "(&(&(objectClass=groupOfNames)(member={}))(cn=seals))"
|
||||||
})),
|
})),
|
||||||
wantAuthResponse: &authenticator.Response{
|
wantAuthResponse: &authenticators.Response{
|
||||||
User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: []string{"seals"}},
|
User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: []string{"seals"}}, DN: "cn=pinny,ou=users,dc=pinniped,dc=dev",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -291,8 +291,8 @@ func TestLDAPSearch_Parallel(t *testing.T) {
|
|||||||
provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) {
|
provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) {
|
||||||
p.GroupSearch.Filter = "foobar={}" // foobar is not a valid attribute name for this LDAP server's schema
|
p.GroupSearch.Filter = "foobar={}" // foobar is not a valid attribute name for this LDAP server's schema
|
||||||
})),
|
})),
|
||||||
wantAuthResponse: &authenticator.Response{
|
wantAuthResponse: &authenticators.Response{
|
||||||
User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: []string{}},
|
User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: []string{}}, DN: "cn=pinny,ou=users,dc=pinniped,dc=dev",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -670,14 +670,15 @@ func TestSimultaneousLDAPRequestsOnSingleProvider(t *testing.T) {
|
|||||||
// Record failures but allow the test to keep running so that all the background goroutines have a chance to try.
|
// Record failures but allow the test to keep running so that all the background goroutines have a chance to try.
|
||||||
assert.NoError(t, result.err)
|
assert.NoError(t, result.err)
|
||||||
assert.True(t, result.authenticated, "expected the user to be authenticated, but they were not")
|
assert.True(t, result.authenticated, "expected the user to be authenticated, but they were not")
|
||||||
assert.Equal(t, &authenticator.Response{
|
assert.Equal(t, &authenticators.Response{
|
||||||
User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: []string{"ball-game-players", "seals"}},
|
User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: []string{"ball-game-players", "seals"}},
|
||||||
|
DN: "cn=pinny,ou=users,dc=pinniped,dc=dev",
|
||||||
}, result.response)
|
}, result.response)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type authUserResult struct {
|
type authUserResult struct {
|
||||||
response *authenticator.Response
|
response *authenticators.Response
|
||||||
authenticated bool
|
authenticated bool
|
||||||
err error
|
err error
|
||||||
}
|
}
|
||||||
|
@ -44,7 +44,7 @@ func TestSupervisorLogin(t *testing.T) {
|
|||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
maybeSkip func(t *testing.T)
|
maybeSkip func(t *testing.T)
|
||||||
createIDP func(t *testing.T)
|
createIDP func(t *testing.T) string
|
||||||
requestAuthorization func(t *testing.T, downstreamAuthorizeURL, downstreamCallbackURL string, httpClient *http.Client)
|
requestAuthorization func(t *testing.T, downstreamAuthorizeURL, downstreamCallbackURL string, httpClient *http.Client)
|
||||||
wantDownstreamIDTokenSubjectToMatch string
|
wantDownstreamIDTokenSubjectToMatch string
|
||||||
wantDownstreamIDTokenUsernameToMatch string
|
wantDownstreamIDTokenUsernameToMatch string
|
||||||
@ -55,16 +55,16 @@ func TestSupervisorLogin(t *testing.T) {
|
|||||||
// We don't necessarily have any way to revoke the user's session on the upstream provider,
|
// We don't necessarily have any way to revoke the user's session on the upstream provider,
|
||||||
// so to cause the upstream refresh to fail we can cheat by manipulating the user's session
|
// so to cause the upstream refresh to fail we can cheat by manipulating the user's session
|
||||||
// data in such a way that it should cause the next upstream refresh attempt to fail.
|
// data in such a way that it should cause the next upstream refresh attempt to fail.
|
||||||
breakRefreshSessionData func(t *testing.T, customSessionData *psession.CustomSessionData)
|
breakRefreshSessionData func(t *testing.T, sessionData *psession.PinnipedSession, idpName string)
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "oidc with default username and groups claim settings",
|
name: "oidc with default username and groups claim settings",
|
||||||
maybeSkip: func(t *testing.T) {
|
maybeSkip: func(t *testing.T) {
|
||||||
// never need to skip this test
|
// never need to skip this test
|
||||||
},
|
},
|
||||||
createIDP: func(t *testing.T) {
|
createIDP: func(t *testing.T) string {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
testlib.CreateTestOIDCIdentityProvider(t, idpv1alpha1.OIDCIdentityProviderSpec{
|
oidcIDP := testlib.CreateTestOIDCIdentityProvider(t, idpv1alpha1.OIDCIdentityProviderSpec{
|
||||||
Issuer: env.SupervisorUpstreamOIDC.Issuer,
|
Issuer: env.SupervisorUpstreamOIDC.Issuer,
|
||||||
TLS: &idpv1alpha1.TLSSpec{
|
TLS: &idpv1alpha1.TLSSpec{
|
||||||
CertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte(env.SupervisorUpstreamOIDC.CABundle)),
|
CertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte(env.SupervisorUpstreamOIDC.CABundle)),
|
||||||
@ -73,9 +73,11 @@ func TestSupervisorLogin(t *testing.T) {
|
|||||||
SecretName: testlib.CreateClientCredsSecret(t, env.SupervisorUpstreamOIDC.ClientID, env.SupervisorUpstreamOIDC.ClientSecret).Name,
|
SecretName: testlib.CreateClientCredsSecret(t, env.SupervisorUpstreamOIDC.ClientID, env.SupervisorUpstreamOIDC.ClientSecret).Name,
|
||||||
},
|
},
|
||||||
}, idpv1alpha1.PhaseReady)
|
}, idpv1alpha1.PhaseReady)
|
||||||
|
return oidcIDP.Name
|
||||||
},
|
},
|
||||||
requestAuthorization: requestAuthorizationUsingBrowserAuthcodeFlow,
|
requestAuthorization: requestAuthorizationUsingBrowserAuthcodeFlow,
|
||||||
breakRefreshSessionData: func(t *testing.T, customSessionData *psession.CustomSessionData) {
|
breakRefreshSessionData: func(t *testing.T, pinnipedSession *psession.PinnipedSession, _ string) {
|
||||||
|
customSessionData := pinnipedSession.Custom
|
||||||
require.Equal(t, psession.ProviderTypeOIDC, customSessionData.ProviderType)
|
require.Equal(t, psession.ProviderTypeOIDC, customSessionData.ProviderType)
|
||||||
require.NotEmpty(t, customSessionData.OIDC.UpstreamRefreshToken)
|
require.NotEmpty(t, customSessionData.OIDC.UpstreamRefreshToken)
|
||||||
customSessionData.OIDC.UpstreamRefreshToken = "invalid-updated-refresh-token"
|
customSessionData.OIDC.UpstreamRefreshToken = "invalid-updated-refresh-token"
|
||||||
@ -90,9 +92,9 @@ func TestSupervisorLogin(t *testing.T) {
|
|||||||
maybeSkip: func(t *testing.T) {
|
maybeSkip: func(t *testing.T) {
|
||||||
// never need to skip this test
|
// never need to skip this test
|
||||||
},
|
},
|
||||||
createIDP: func(t *testing.T) {
|
createIDP: func(t *testing.T) string {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
testlib.CreateTestOIDCIdentityProvider(t, idpv1alpha1.OIDCIdentityProviderSpec{
|
oidcIDP := testlib.CreateTestOIDCIdentityProvider(t, idpv1alpha1.OIDCIdentityProviderSpec{
|
||||||
Issuer: env.SupervisorUpstreamOIDC.Issuer,
|
Issuer: env.SupervisorUpstreamOIDC.Issuer,
|
||||||
TLS: &idpv1alpha1.TLSSpec{
|
TLS: &idpv1alpha1.TLSSpec{
|
||||||
CertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte(env.SupervisorUpstreamOIDC.CABundle)),
|
CertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte(env.SupervisorUpstreamOIDC.CABundle)),
|
||||||
@ -108,9 +110,11 @@ func TestSupervisorLogin(t *testing.T) {
|
|||||||
AdditionalScopes: env.SupervisorUpstreamOIDC.AdditionalScopes,
|
AdditionalScopes: env.SupervisorUpstreamOIDC.AdditionalScopes,
|
||||||
},
|
},
|
||||||
}, idpv1alpha1.PhaseReady)
|
}, idpv1alpha1.PhaseReady)
|
||||||
|
return oidcIDP.Name
|
||||||
},
|
},
|
||||||
requestAuthorization: requestAuthorizationUsingBrowserAuthcodeFlow,
|
requestAuthorization: requestAuthorizationUsingBrowserAuthcodeFlow,
|
||||||
breakRefreshSessionData: func(t *testing.T, customSessionData *psession.CustomSessionData) {
|
breakRefreshSessionData: func(t *testing.T, pinnipedSession *psession.PinnipedSession, _ string) {
|
||||||
|
customSessionData := pinnipedSession.Custom
|
||||||
require.Equal(t, psession.ProviderTypeOIDC, customSessionData.ProviderType)
|
require.Equal(t, psession.ProviderTypeOIDC, customSessionData.ProviderType)
|
||||||
require.NotEmpty(t, customSessionData.OIDC.UpstreamRefreshToken)
|
require.NotEmpty(t, customSessionData.OIDC.UpstreamRefreshToken)
|
||||||
customSessionData.OIDC.UpstreamRefreshToken = "invalid-updated-refresh-token"
|
customSessionData.OIDC.UpstreamRefreshToken = "invalid-updated-refresh-token"
|
||||||
@ -124,9 +128,9 @@ func TestSupervisorLogin(t *testing.T) {
|
|||||||
maybeSkip: func(t *testing.T) {
|
maybeSkip: func(t *testing.T) {
|
||||||
// never need to skip this test
|
// never need to skip this test
|
||||||
},
|
},
|
||||||
createIDP: func(t *testing.T) {
|
createIDP: func(t *testing.T) string {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
testlib.CreateTestOIDCIdentityProvider(t, idpv1alpha1.OIDCIdentityProviderSpec{
|
oidcIDP := testlib.CreateTestOIDCIdentityProvider(t, idpv1alpha1.OIDCIdentityProviderSpec{
|
||||||
Issuer: env.SupervisorUpstreamOIDC.Issuer,
|
Issuer: env.SupervisorUpstreamOIDC.Issuer,
|
||||||
TLS: &idpv1alpha1.TLSSpec{
|
TLS: &idpv1alpha1.TLSSpec{
|
||||||
CertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte(env.SupervisorUpstreamOIDC.CABundle)),
|
CertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte(env.SupervisorUpstreamOIDC.CABundle)),
|
||||||
@ -138,6 +142,7 @@ func TestSupervisorLogin(t *testing.T) {
|
|||||||
AllowPasswordGrant: true, // allow the CLI password flow for this OIDCIdentityProvider
|
AllowPasswordGrant: true, // allow the CLI password flow for this OIDCIdentityProvider
|
||||||
},
|
},
|
||||||
}, idpv1alpha1.PhaseReady)
|
}, idpv1alpha1.PhaseReady)
|
||||||
|
return oidcIDP.Name
|
||||||
},
|
},
|
||||||
requestAuthorization: func(t *testing.T, downstreamAuthorizeURL, _ string, httpClient *http.Client) {
|
requestAuthorization: func(t *testing.T, downstreamAuthorizeURL, _ string, httpClient *http.Client) {
|
||||||
requestAuthorizationUsingCLIPasswordFlow(t,
|
requestAuthorizationUsingCLIPasswordFlow(t,
|
||||||
@ -148,7 +153,8 @@ func TestSupervisorLogin(t *testing.T) {
|
|||||||
false,
|
false,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
breakRefreshSessionData: func(t *testing.T, customSessionData *psession.CustomSessionData) {
|
breakRefreshSessionData: func(t *testing.T, pinnipedSession *psession.PinnipedSession, _ string) {
|
||||||
|
customSessionData := pinnipedSession.Custom
|
||||||
require.Equal(t, psession.ProviderTypeOIDC, customSessionData.ProviderType)
|
require.Equal(t, psession.ProviderTypeOIDC, customSessionData.ProviderType)
|
||||||
require.NotEmpty(t, customSessionData.OIDC.UpstreamRefreshToken)
|
require.NotEmpty(t, customSessionData.OIDC.UpstreamRefreshToken)
|
||||||
customSessionData.OIDC.UpstreamRefreshToken = "invalid-updated-refresh-token"
|
customSessionData.OIDC.UpstreamRefreshToken = "invalid-updated-refresh-token"
|
||||||
@ -166,7 +172,7 @@ func TestSupervisorLogin(t *testing.T) {
|
|||||||
t.Skip("LDAP integration test requires connectivity to an LDAP server")
|
t.Skip("LDAP integration test requires connectivity to an LDAP server")
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
createIDP: func(t *testing.T) {
|
createIDP: func(t *testing.T) string {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
secret := testlib.CreateTestSecret(t, env.SupervisorNamespace, "ldap-service-account", v1.SecretTypeBasicAuth,
|
secret := testlib.CreateTestSecret(t, env.SupervisorNamespace, "ldap-service-account", v1.SecretTypeBasicAuth,
|
||||||
map[string]string{
|
map[string]string{
|
||||||
@ -204,6 +210,7 @@ func TestSupervisorLogin(t *testing.T) {
|
|||||||
secret.Name, secret.ResourceVersion,
|
secret.Name, secret.ResourceVersion,
|
||||||
)
|
)
|
||||||
requireSuccessfulLDAPIdentityProviderConditions(t, ldapIDP, expectedMsg)
|
requireSuccessfulLDAPIdentityProviderConditions(t, ldapIDP, expectedMsg)
|
||||||
|
return ldapIDP.Name
|
||||||
},
|
},
|
||||||
requestAuthorization: func(t *testing.T, downstreamAuthorizeURL, _ string, httpClient *http.Client) {
|
requestAuthorization: func(t *testing.T, downstreamAuthorizeURL, _ string, httpClient *http.Client) {
|
||||||
requestAuthorizationUsingCLIPasswordFlow(t,
|
requestAuthorizationUsingCLIPasswordFlow(t,
|
||||||
@ -214,7 +221,13 @@ func TestSupervisorLogin(t *testing.T) {
|
|||||||
false,
|
false,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
breakRefreshSessionData: nil, // upstream refresh not yet implemented for this IDP type
|
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
|
// the ID token Subject should be the Host URL plus the value pulled from the requested UserSearch.Attributes.UID attribute
|
||||||
wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta(
|
wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta(
|
||||||
"ldaps://"+env.SupervisorUpstreamLDAP.Host+
|
"ldaps://"+env.SupervisorUpstreamLDAP.Host+
|
||||||
@ -233,7 +246,7 @@ func TestSupervisorLogin(t *testing.T) {
|
|||||||
t.Skip("LDAP integration test requires connectivity to an LDAP server")
|
t.Skip("LDAP integration test requires connectivity to an LDAP server")
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
createIDP: func(t *testing.T) {
|
createIDP: func(t *testing.T) string {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
secret := testlib.CreateTestSecret(t, env.SupervisorNamespace, "ldap-service-account", v1.SecretTypeBasicAuth,
|
secret := testlib.CreateTestSecret(t, env.SupervisorNamespace, "ldap-service-account", v1.SecretTypeBasicAuth,
|
||||||
map[string]string{
|
map[string]string{
|
||||||
@ -271,6 +284,7 @@ func TestSupervisorLogin(t *testing.T) {
|
|||||||
secret.Name, secret.ResourceVersion,
|
secret.Name, secret.ResourceVersion,
|
||||||
)
|
)
|
||||||
requireSuccessfulLDAPIdentityProviderConditions(t, ldapIDP, expectedMsg)
|
requireSuccessfulLDAPIdentityProviderConditions(t, ldapIDP, expectedMsg)
|
||||||
|
return ldapIDP.Name
|
||||||
},
|
},
|
||||||
requestAuthorization: func(t *testing.T, downstreamAuthorizeURL, _ string, httpClient *http.Client) {
|
requestAuthorization: func(t *testing.T, downstreamAuthorizeURL, _ string, httpClient *http.Client) {
|
||||||
requestAuthorizationUsingCLIPasswordFlow(t,
|
requestAuthorizationUsingCLIPasswordFlow(t,
|
||||||
@ -281,7 +295,13 @@ func TestSupervisorLogin(t *testing.T) {
|
|||||||
false,
|
false,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
breakRefreshSessionData: nil, // upstream refresh not yet implemented for this IDP type
|
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.Extra["username"] = "not-the-same"
|
||||||
|
},
|
||||||
// the ID token Subject should be the Host URL plus the value pulled from the requested UserSearch.Attributes.UID attribute
|
// the ID token Subject should be the Host URL plus the value pulled from the requested UserSearch.Attributes.UID attribute
|
||||||
wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta(
|
wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta(
|
||||||
"ldaps://"+env.SupervisorUpstreamLDAP.StartTLSOnlyHost+
|
"ldaps://"+env.SupervisorUpstreamLDAP.StartTLSOnlyHost+
|
||||||
@ -300,7 +320,7 @@ func TestSupervisorLogin(t *testing.T) {
|
|||||||
t.Skip("LDAP integration test requires connectivity to an LDAP server")
|
t.Skip("LDAP integration test requires connectivity to an LDAP server")
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
createIDP: func(t *testing.T) {
|
createIDP: func(t *testing.T) string {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
secret := testlib.CreateTestSecret(t, env.SupervisorNamespace, "ldap-service-account", v1.SecretTypeBasicAuth,
|
secret := testlib.CreateTestSecret(t, env.SupervisorNamespace, "ldap-service-account", v1.SecretTypeBasicAuth,
|
||||||
map[string]string{
|
map[string]string{
|
||||||
@ -338,6 +358,7 @@ func TestSupervisorLogin(t *testing.T) {
|
|||||||
secret.Name, secret.ResourceVersion,
|
secret.Name, secret.ResourceVersion,
|
||||||
)
|
)
|
||||||
requireSuccessfulLDAPIdentityProviderConditions(t, ldapIDP, expectedMsg)
|
requireSuccessfulLDAPIdentityProviderConditions(t, ldapIDP, expectedMsg)
|
||||||
|
return ldapIDP.Name
|
||||||
},
|
},
|
||||||
requestAuthorization: func(t *testing.T, downstreamAuthorizeURL, _ string, httpClient *http.Client) {
|
requestAuthorization: func(t *testing.T, downstreamAuthorizeURL, _ string, httpClient *http.Client) {
|
||||||
requestAuthorizationUsingCLIPasswordFlow(t,
|
requestAuthorizationUsingCLIPasswordFlow(t,
|
||||||
@ -348,9 +369,8 @@ func TestSupervisorLogin(t *testing.T) {
|
|||||||
true,
|
true,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
breakRefreshSessionData: nil, // upstream refresh not yet implemented for this IDP type
|
wantErrorDescription: "The resource owner or authorization server denied the request. Username/password not accepted by LDAP provider.",
|
||||||
wantErrorDescription: "The resource owner or authorization server denied the request. Username/password not accepted by LDAP provider.",
|
wantErrorType: "access_denied",
|
||||||
wantErrorType: "access_denied",
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "ldap login still works after updating bind secret",
|
name: "ldap login still works after updating bind secret",
|
||||||
@ -360,7 +380,7 @@ func TestSupervisorLogin(t *testing.T) {
|
|||||||
t.Skip("LDAP integration test requires connectivity to an LDAP server")
|
t.Skip("LDAP integration test requires connectivity to an LDAP server")
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
createIDP: func(t *testing.T) {
|
createIDP: func(t *testing.T) string {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
secret := testlib.CreateTestSecret(t, env.SupervisorNamespace, "ldap-service-account", v1.SecretTypeBasicAuth,
|
secret := testlib.CreateTestSecret(t, env.SupervisorNamespace, "ldap-service-account", v1.SecretTypeBasicAuth,
|
||||||
@ -416,6 +436,7 @@ func TestSupervisorLogin(t *testing.T) {
|
|||||||
requireEventually.NoError(err)
|
requireEventually.NoError(err)
|
||||||
requireEventuallySuccessfulLDAPIdentityProviderConditions(t, requireEventually, ldapIDP, expectedMsg)
|
requireEventuallySuccessfulLDAPIdentityProviderConditions(t, requireEventually, ldapIDP, expectedMsg)
|
||||||
}, time.Minute, 500*time.Millisecond)
|
}, time.Minute, 500*time.Millisecond)
|
||||||
|
return ldapIDP.Name
|
||||||
},
|
},
|
||||||
requestAuthorization: func(t *testing.T, downstreamAuthorizeURL, _ string, httpClient *http.Client) {
|
requestAuthorization: func(t *testing.T, downstreamAuthorizeURL, _ string, httpClient *http.Client) {
|
||||||
requestAuthorizationUsingCLIPasswordFlow(t,
|
requestAuthorizationUsingCLIPasswordFlow(t,
|
||||||
@ -426,7 +447,12 @@ func TestSupervisorLogin(t *testing.T) {
|
|||||||
false,
|
false,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
breakRefreshSessionData: nil, // upstream refresh not yet implemented for this IDP type
|
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)
|
||||||
|
customSessionData.LDAP.UserDN = "cn=not-a-user,dc=pinniped,dc=dev"
|
||||||
|
},
|
||||||
// the ID token Subject should be the Host URL plus the value pulled from the requested UserSearch.Attributes.UID attribute
|
// the ID token Subject should be the Host URL plus the value pulled from the requested UserSearch.Attributes.UID attribute
|
||||||
wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta(
|
wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta(
|
||||||
"ldaps://"+env.SupervisorUpstreamLDAP.Host+
|
"ldaps://"+env.SupervisorUpstreamLDAP.Host+
|
||||||
@ -445,7 +471,7 @@ func TestSupervisorLogin(t *testing.T) {
|
|||||||
t.Skip("LDAP integration test requires connectivity to an LDAP server")
|
t.Skip("LDAP integration test requires connectivity to an LDAP server")
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
createIDP: func(t *testing.T) {
|
createIDP: func(t *testing.T) string {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
secret := testlib.CreateTestSecret(t, env.SupervisorNamespace, "ldap-service-account", v1.SecretTypeBasicAuth,
|
secret := testlib.CreateTestSecret(t, env.SupervisorNamespace, "ldap-service-account", v1.SecretTypeBasicAuth,
|
||||||
@ -515,6 +541,7 @@ func TestSupervisorLogin(t *testing.T) {
|
|||||||
requireEventually.NoError(err)
|
requireEventually.NoError(err)
|
||||||
requireEventuallySuccessfulLDAPIdentityProviderConditions(t, requireEventually, ldapIDP, expectedMsg)
|
requireEventuallySuccessfulLDAPIdentityProviderConditions(t, requireEventually, ldapIDP, expectedMsg)
|
||||||
}, time.Minute, 500*time.Millisecond)
|
}, time.Minute, 500*time.Millisecond)
|
||||||
|
return ldapIDP.Name
|
||||||
},
|
},
|
||||||
requestAuthorization: func(t *testing.T, downstreamAuthorizeURL, _ string, httpClient *http.Client) {
|
requestAuthorization: func(t *testing.T, downstreamAuthorizeURL, _ string, httpClient *http.Client) {
|
||||||
requestAuthorizationUsingCLIPasswordFlow(t,
|
requestAuthorizationUsingCLIPasswordFlow(t,
|
||||||
@ -525,7 +552,12 @@ func TestSupervisorLogin(t *testing.T) {
|
|||||||
false,
|
false,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
breakRefreshSessionData: nil, // upstream refresh not yet implemented for this IDP type
|
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)
|
||||||
|
customSessionData.LDAP.UserDN = "cn=not-a-user,dc=pinniped,dc=dev"
|
||||||
|
},
|
||||||
// the ID token Subject should be the Host URL plus the value pulled from the requested UserSearch.Attributes.UID attribute
|
// the ID token Subject should be the Host URL plus the value pulled from the requested UserSearch.Attributes.UID attribute
|
||||||
wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta(
|
wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta(
|
||||||
"ldaps://"+env.SupervisorUpstreamLDAP.Host+
|
"ldaps://"+env.SupervisorUpstreamLDAP.Host+
|
||||||
@ -547,7 +579,7 @@ func TestSupervisorLogin(t *testing.T) {
|
|||||||
t.Skip("Active Directory hostname not specified")
|
t.Skip("Active Directory hostname not specified")
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
createIDP: func(t *testing.T) {
|
createIDP: func(t *testing.T) string {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
secret := testlib.CreateTestSecret(t, env.SupervisorNamespace, "ad-service-account", v1.SecretTypeBasicAuth,
|
secret := testlib.CreateTestSecret(t, env.SupervisorNamespace, "ad-service-account", v1.SecretTypeBasicAuth,
|
||||||
map[string]string{
|
map[string]string{
|
||||||
@ -570,6 +602,7 @@ func TestSupervisorLogin(t *testing.T) {
|
|||||||
secret.Name, secret.ResourceVersion,
|
secret.Name, secret.ResourceVersion,
|
||||||
)
|
)
|
||||||
requireSuccessfulActiveDirectoryIdentityProviderConditions(t, adIDP, expectedMsg)
|
requireSuccessfulActiveDirectoryIdentityProviderConditions(t, adIDP, expectedMsg)
|
||||||
|
return adIDP.Name
|
||||||
},
|
},
|
||||||
requestAuthorization: func(t *testing.T, downstreamAuthorizeURL, _ string, httpClient *http.Client) {
|
requestAuthorization: func(t *testing.T, downstreamAuthorizeURL, _ string, httpClient *http.Client) {
|
||||||
requestAuthorizationUsingCLIPasswordFlow(t,
|
requestAuthorizationUsingCLIPasswordFlow(t,
|
||||||
@ -580,7 +613,13 @@ func TestSupervisorLogin(t *testing.T) {
|
|||||||
false,
|
false,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
breakRefreshSessionData: nil, // upstream refresh not yet implemented for this IDP type
|
breakRefreshSessionData: func(t *testing.T, pinnipedSession *psession.PinnipedSession, _ string) {
|
||||||
|
customSessionData := pinnipedSession.Custom
|
||||||
|
require.Equal(t, psession.ProviderTypeActiveDirectory, customSessionData.ProviderType)
|
||||||
|
require.NotEmpty(t, customSessionData.ActiveDirectory.UserDN)
|
||||||
|
fositeSessionData := pinnipedSession.Fosite
|
||||||
|
fositeSessionData.Claims.Extra["username"] = "not-the-same"
|
||||||
|
},
|
||||||
// the ID token Subject should be the Host URL plus the value pulled from the requested UserSearch.Attributes.UID attribute
|
// the ID token Subject should be the Host URL plus the value pulled from the requested UserSearch.Attributes.UID attribute
|
||||||
wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta(
|
wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta(
|
||||||
"ldaps://"+env.SupervisorUpstreamActiveDirectory.Host+
|
"ldaps://"+env.SupervisorUpstreamActiveDirectory.Host+
|
||||||
@ -601,7 +640,7 @@ func TestSupervisorLogin(t *testing.T) {
|
|||||||
t.Skip("Active Directory hostname not specified")
|
t.Skip("Active Directory hostname not specified")
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
createIDP: func(t *testing.T) {
|
createIDP: func(t *testing.T) string {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
secret := testlib.CreateTestSecret(t, env.SupervisorNamespace, "ad-service-account", v1.SecretTypeBasicAuth,
|
secret := testlib.CreateTestSecret(t, env.SupervisorNamespace, "ad-service-account", v1.SecretTypeBasicAuth,
|
||||||
map[string]string{
|
map[string]string{
|
||||||
@ -638,6 +677,7 @@ func TestSupervisorLogin(t *testing.T) {
|
|||||||
secret.Name, secret.ResourceVersion,
|
secret.Name, secret.ResourceVersion,
|
||||||
)
|
)
|
||||||
requireSuccessfulActiveDirectoryIdentityProviderConditions(t, adIDP, expectedMsg)
|
requireSuccessfulActiveDirectoryIdentityProviderConditions(t, adIDP, expectedMsg)
|
||||||
|
return adIDP.Name
|
||||||
},
|
},
|
||||||
requestAuthorization: func(t *testing.T, downstreamAuthorizeURL, _ string, httpClient *http.Client) {
|
requestAuthorization: func(t *testing.T, downstreamAuthorizeURL, _ string, httpClient *http.Client) {
|
||||||
requestAuthorizationUsingCLIPasswordFlow(t,
|
requestAuthorizationUsingCLIPasswordFlow(t,
|
||||||
@ -648,7 +688,13 @@ func TestSupervisorLogin(t *testing.T) {
|
|||||||
false,
|
false,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
breakRefreshSessionData: nil, // upstream refresh not yet implemented for this IDP type
|
breakRefreshSessionData: func(t *testing.T, pinnipedSession *psession.PinnipedSession, _ string) {
|
||||||
|
customSessionData := pinnipedSession.Custom
|
||||||
|
require.Equal(t, psession.ProviderTypeActiveDirectory, customSessionData.ProviderType)
|
||||||
|
require.NotEmpty(t, customSessionData.ActiveDirectory.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
|
// the ID token Subject should be the Host URL plus the value pulled from the requested UserSearch.Attributes.UID attribute
|
||||||
wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta(
|
wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta(
|
||||||
"ldaps://"+env.SupervisorUpstreamActiveDirectory.Host+
|
"ldaps://"+env.SupervisorUpstreamActiveDirectory.Host+
|
||||||
@ -670,7 +716,7 @@ func TestSupervisorLogin(t *testing.T) {
|
|||||||
t.Skip("Active Directory hostname not specified")
|
t.Skip("Active Directory hostname not specified")
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
createIDP: func(t *testing.T) {
|
createIDP: func(t *testing.T) string {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
secret := testlib.CreateTestSecret(t, env.SupervisorNamespace, "ad-service-account", v1.SecretTypeBasicAuth,
|
secret := testlib.CreateTestSecret(t, env.SupervisorNamespace, "ad-service-account", v1.SecretTypeBasicAuth,
|
||||||
@ -711,6 +757,7 @@ func TestSupervisorLogin(t *testing.T) {
|
|||||||
requireEventually.NoError(err)
|
requireEventually.NoError(err)
|
||||||
requireEventuallySuccessfulActiveDirectoryIdentityProviderConditions(t, requireEventually, adIDP, expectedMsg)
|
requireEventuallySuccessfulActiveDirectoryIdentityProviderConditions(t, requireEventually, adIDP, expectedMsg)
|
||||||
}, time.Minute, 500*time.Millisecond)
|
}, time.Minute, 500*time.Millisecond)
|
||||||
|
return adIDP.Name
|
||||||
},
|
},
|
||||||
requestAuthorization: func(t *testing.T, downstreamAuthorizeURL, _ string, httpClient *http.Client) {
|
requestAuthorization: func(t *testing.T, downstreamAuthorizeURL, _ string, httpClient *http.Client) {
|
||||||
requestAuthorizationUsingCLIPasswordFlow(t,
|
requestAuthorizationUsingCLIPasswordFlow(t,
|
||||||
@ -721,7 +768,12 @@ func TestSupervisorLogin(t *testing.T) {
|
|||||||
false,
|
false,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
breakRefreshSessionData: nil, // upstream refresh not yet implemented for this IDP type
|
breakRefreshSessionData: func(t *testing.T, pinnipedSession *psession.PinnipedSession, _ string) {
|
||||||
|
customSessionData := pinnipedSession.Custom
|
||||||
|
require.Equal(t, psession.ProviderTypeActiveDirectory, customSessionData.ProviderType)
|
||||||
|
require.NotEmpty(t, customSessionData.ActiveDirectory.UserDN)
|
||||||
|
customSessionData.ActiveDirectory.UserDN = "cn=not-a-user,dc=pinniped,dc=dev"
|
||||||
|
},
|
||||||
// the ID token Subject should be the Host URL plus the value pulled from the requested UserSearch.Attributes.UID attribute
|
// the ID token Subject should be the Host URL plus the value pulled from the requested UserSearch.Attributes.UID attribute
|
||||||
wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta(
|
wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta(
|
||||||
"ldaps://"+env.SupervisorUpstreamActiveDirectory.Host+
|
"ldaps://"+env.SupervisorUpstreamActiveDirectory.Host+
|
||||||
@ -743,7 +795,7 @@ func TestSupervisorLogin(t *testing.T) {
|
|||||||
t.Skip("Active Directory hostname not specified")
|
t.Skip("Active Directory hostname not specified")
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
createIDP: func(t *testing.T) {
|
createIDP: func(t *testing.T) string {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
secret := testlib.CreateTestSecret(t, env.SupervisorNamespace, "ad-service-account", v1.SecretTypeBasicAuth,
|
secret := testlib.CreateTestSecret(t, env.SupervisorNamespace, "ad-service-account", v1.SecretTypeBasicAuth,
|
||||||
@ -799,6 +851,7 @@ func TestSupervisorLogin(t *testing.T) {
|
|||||||
requireEventually.NoError(err)
|
requireEventually.NoError(err)
|
||||||
requireEventuallySuccessfulActiveDirectoryIdentityProviderConditions(t, requireEventually, adIDP, expectedMsg)
|
requireEventuallySuccessfulActiveDirectoryIdentityProviderConditions(t, requireEventually, adIDP, expectedMsg)
|
||||||
}, time.Minute, 500*time.Millisecond)
|
}, time.Minute, 500*time.Millisecond)
|
||||||
|
return adIDP.Name
|
||||||
},
|
},
|
||||||
requestAuthorization: func(t *testing.T, downstreamAuthorizeURL, _ string, httpClient *http.Client) {
|
requestAuthorization: func(t *testing.T, downstreamAuthorizeURL, _ string, httpClient *http.Client) {
|
||||||
requestAuthorizationUsingCLIPasswordFlow(t,
|
requestAuthorizationUsingCLIPasswordFlow(t,
|
||||||
@ -809,7 +862,12 @@ func TestSupervisorLogin(t *testing.T) {
|
|||||||
false,
|
false,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
breakRefreshSessionData: nil, // upstream refresh not yet implemented for this IDP type
|
breakRefreshSessionData: func(t *testing.T, pinnipedSession *psession.PinnipedSession, _ string) {
|
||||||
|
customSessionData := pinnipedSession.Custom
|
||||||
|
require.Equal(t, psession.ProviderTypeActiveDirectory, customSessionData.ProviderType)
|
||||||
|
require.NotEmpty(t, customSessionData.ActiveDirectory.UserDN)
|
||||||
|
customSessionData.ActiveDirectory.UserDN = "cn=not-a-user,dc=pinniped,dc=dev"
|
||||||
|
},
|
||||||
// the ID token Subject should be the Host URL plus the value pulled from the requested UserSearch.Attributes.UID attribute
|
// the ID token Subject should be the Host URL plus the value pulled from the requested UserSearch.Attributes.UID attribute
|
||||||
wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta(
|
wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta(
|
||||||
"ldaps://"+env.SupervisorUpstreamActiveDirectory.Host+
|
"ldaps://"+env.SupervisorUpstreamActiveDirectory.Host+
|
||||||
@ -831,7 +889,7 @@ func TestSupervisorLogin(t *testing.T) {
|
|||||||
t.Skip("Active Directory hostname not specified")
|
t.Skip("Active Directory hostname not specified")
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
createIDP: func(t *testing.T) {
|
createIDP: func(t *testing.T) string {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
secret := testlib.CreateTestSecret(t, env.SupervisorNamespace, "ad-service-account", v1.SecretTypeBasicAuth,
|
secret := testlib.CreateTestSecret(t, env.SupervisorNamespace, "ad-service-account", v1.SecretTypeBasicAuth,
|
||||||
map[string]string{
|
map[string]string{
|
||||||
@ -854,6 +912,7 @@ func TestSupervisorLogin(t *testing.T) {
|
|||||||
secret.Name, secret.ResourceVersion,
|
secret.Name, secret.ResourceVersion,
|
||||||
)
|
)
|
||||||
requireSuccessfulActiveDirectoryIdentityProviderConditions(t, adIDP, expectedMsg)
|
requireSuccessfulActiveDirectoryIdentityProviderConditions(t, adIDP, expectedMsg)
|
||||||
|
return adIDP.Name
|
||||||
},
|
},
|
||||||
requestAuthorization: func(t *testing.T, downstreamAuthorizeURL, _ string, httpClient *http.Client) {
|
requestAuthorization: func(t *testing.T, downstreamAuthorizeURL, _ string, httpClient *http.Client) {
|
||||||
requestAuthorizationUsingCLIPasswordFlow(t,
|
requestAuthorizationUsingCLIPasswordFlow(t,
|
||||||
@ -864,10 +923,93 @@ func TestSupervisorLogin(t *testing.T) {
|
|||||||
true,
|
true,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
breakRefreshSessionData: nil, // upstream refresh not yet implemented for this IDP type
|
breakRefreshSessionData: nil,
|
||||||
wantErrorDescription: "The resource owner or authorization server denied the request. Username/password not accepted by LDAP provider.",
|
wantErrorDescription: "The resource owner or authorization server denied the request. Username/password not accepted by LDAP provider.",
|
||||||
wantErrorType: "access_denied",
|
wantErrorType: "access_denied",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "ldap refresh fails when username changes from email as username to dn as username",
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
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.GroupSearchBase,
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
breakRefreshSessionData: 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()
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
ldapIDP.Spec.UserSearch.Attributes.Username = "dn"
|
||||||
|
|
||||||
|
_, 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: "^" + regexp.QuoteMeta(env.SupervisorUpstreamLDAP.TestUserMailAttributeValue) + "$",
|
||||||
|
wantDownstreamIDTokenGroups: env.SupervisorUpstreamLDAP.TestUserDirectGroupsDNs,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
tt := test
|
tt := test
|
||||||
@ -1007,9 +1149,9 @@ func requireEventuallySuccessfulActiveDirectoryIdentityProviderConditions(t *tes
|
|||||||
|
|
||||||
func testSupervisorLogin(
|
func testSupervisorLogin(
|
||||||
t *testing.T,
|
t *testing.T,
|
||||||
createIDP func(t *testing.T),
|
createIDP func(t *testing.T) string,
|
||||||
requestAuthorization func(t *testing.T, downstreamAuthorizeURL, downstreamCallbackURL string, httpClient *http.Client),
|
requestAuthorization func(t *testing.T, downstreamAuthorizeURL, downstreamCallbackURL string, httpClient *http.Client),
|
||||||
breakRefreshSessionData func(t *testing.T, customSessionData *psession.CustomSessionData),
|
breakRefreshSessionData func(t *testing.T, pinnipedSession *psession.PinnipedSession, idpName string),
|
||||||
wantDownstreamIDTokenSubjectToMatch, wantDownstreamIDTokenUsernameToMatch string, wantDownstreamIDTokenGroups []string,
|
wantDownstreamIDTokenSubjectToMatch, wantDownstreamIDTokenUsernameToMatch string, wantDownstreamIDTokenGroups []string,
|
||||||
wantErrorDescription string, wantErrorType string,
|
wantErrorDescription string, wantErrorType string,
|
||||||
) {
|
) {
|
||||||
@ -1097,7 +1239,7 @@ func testSupervisorLogin(
|
|||||||
}, 30*time.Second, 200*time.Millisecond)
|
}, 30*time.Second, 200*time.Millisecond)
|
||||||
|
|
||||||
// Create upstream IDP and wait for it to become ready.
|
// Create upstream IDP and wait for it to become ready.
|
||||||
createIDP(t)
|
idpName := createIDP(t)
|
||||||
|
|
||||||
// Perform OIDC discovery for our downstream.
|
// Perform OIDC discovery for our downstream.
|
||||||
var discovery *coreosoidc.Provider
|
var discovery *coreosoidc.Provider
|
||||||
@ -1191,7 +1333,7 @@ func testSupervisorLogin(
|
|||||||
// Next mutate the part of the session that is used during upstream refresh.
|
// Next mutate the part of the session that is used during upstream refresh.
|
||||||
pinnipedSession, ok := storedRefreshSession.GetSession().(*psession.PinnipedSession)
|
pinnipedSession, ok := storedRefreshSession.GetSession().(*psession.PinnipedSession)
|
||||||
require.True(t, ok, "should have been able to cast session data to PinnipedSession")
|
require.True(t, ok, "should have been able to cast session data to PinnipedSession")
|
||||||
breakRefreshSessionData(t, pinnipedSession.Custom)
|
breakRefreshSessionData(t, pinnipedSession, idpName)
|
||||||
|
|
||||||
// Then save the mutated Secret back to Kubernetes.
|
// Then save the mutated Secret back to Kubernetes.
|
||||||
// There is no update function, so delete and create again at the same name.
|
// There is no update function, so delete and create again at the same name.
|
||||||
@ -1204,9 +1346,8 @@ func testSupervisorLogin(
|
|||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
require.Regexp(t,
|
require.Regexp(t,
|
||||||
regexp.QuoteMeta("oauth2: cannot fetch token: 401 Unauthorized\n")+
|
regexp.QuoteMeta("oauth2: cannot fetch token: 401 Unauthorized\n")+
|
||||||
regexp.QuoteMeta(`Response: {"error":"error","error_description":"Error during upstream refresh. Upstream refresh failed using provider '`)+
|
regexp.QuoteMeta(`Response: {"error":"error","error_description":"Error during upstream refresh. Upstream refresh failed`)+
|
||||||
"[^']+"+ // this would be the name of the identity provider CR
|
"[^']+",
|
||||||
regexp.QuoteMeta(fmt.Sprintf(`' of type '%s'."`, pinnipedSession.Custom.ProviderType)),
|
|
||||||
err.Error(),
|
err.Error(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user