Active Directory checks whether password has changed recently during
upstream refresh Signed-off-by: Margo Crawford <margaretc@vmware.com>
This commit is contained in:
parent
8db0203839
commit
da9b4620b3
@ -317,6 +317,7 @@ func (c *activeDirectoryWatcherController) validateUpstream(ctx context.Context,
|
||||
},
|
||||
Dialer: c.ldapDialer,
|
||||
UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": upstreamldap.MicrosoftUUIDFromBinary("objectGUID")},
|
||||
RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.StoredRefreshAttributes) error{"pwdLastSet": upstreamldap.PwdUnchangedSinceLogin},
|
||||
}
|
||||
|
||||
if spec.GroupSearch.Attributes.GroupName == "" {
|
||||
|
@ -221,6 +221,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
|
||||
GroupNameAttribute: testGroupNameAttrName,
|
||||
},
|
||||
UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": upstreamldap.MicrosoftUUIDFromBinary("objectGUID")},
|
||||
RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.StoredRefreshAttributes) error{"pwdLastSet": upstreamldap.PwdUnchangedSinceLogin},
|
||||
}
|
||||
|
||||
// Make a copy with targeted changes.
|
||||
@ -537,6 +538,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
|
||||
GroupNameAttribute: testGroupNameAttrName,
|
||||
},
|
||||
UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": upstreamldap.MicrosoftUUIDFromBinary("objectGUID")},
|
||||
RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.StoredRefreshAttributes) error{"pwdLastSet": upstreamldap.PwdUnchangedSinceLogin},
|
||||
},
|
||||
},
|
||||
wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{
|
||||
@ -593,6 +595,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
|
||||
GroupNameAttribute: "sAMAccountName",
|
||||
},
|
||||
UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": upstreamldap.MicrosoftUUIDFromBinary("objectGUID")},
|
||||
RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.StoredRefreshAttributes) error{"pwdLastSet": upstreamldap.PwdUnchangedSinceLogin},
|
||||
},
|
||||
},
|
||||
wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{
|
||||
@ -652,6 +655,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
|
||||
GroupNameAttribute: testGroupNameAttrName,
|
||||
},
|
||||
UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": upstreamldap.MicrosoftUUIDFromBinary("objectGUID")},
|
||||
RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.StoredRefreshAttributes) error{"pwdLastSet": upstreamldap.PwdUnchangedSinceLogin},
|
||||
},
|
||||
},
|
||||
wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{
|
||||
@ -711,6 +715,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
|
||||
GroupNameAttribute: testGroupNameAttrName,
|
||||
},
|
||||
UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": upstreamldap.MicrosoftUUIDFromBinary("objectGUID")},
|
||||
RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.StoredRefreshAttributes) error{"pwdLastSet": upstreamldap.PwdUnchangedSinceLogin},
|
||||
},
|
||||
},
|
||||
wantErr: controllerlib.ErrSyntheticRequeue.Error(),
|
||||
@ -769,6 +774,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
|
||||
GroupNameAttribute: testGroupNameAttrName,
|
||||
},
|
||||
UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": upstreamldap.MicrosoftUUIDFromBinary("objectGUID")},
|
||||
RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.StoredRefreshAttributes) error{"pwdLastSet": upstreamldap.PwdUnchangedSinceLogin},
|
||||
},
|
||||
},
|
||||
wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{
|
||||
@ -898,6 +904,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
|
||||
GroupNameAttribute: testGroupNameAttrName,
|
||||
},
|
||||
UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": upstreamldap.MicrosoftUUIDFromBinary("objectGUID")},
|
||||
RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.StoredRefreshAttributes) error{"pwdLastSet": upstreamldap.PwdUnchangedSinceLogin},
|
||||
},
|
||||
},
|
||||
wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{
|
||||
@ -1022,6 +1029,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
|
||||
GroupNameAttribute: testGroupNameAttrName,
|
||||
},
|
||||
UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": upstreamldap.MicrosoftUUIDFromBinary("objectGUID")},
|
||||
RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.StoredRefreshAttributes) error{"pwdLastSet": upstreamldap.PwdUnchangedSinceLogin},
|
||||
},
|
||||
},
|
||||
wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{
|
||||
@ -1073,6 +1081,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
|
||||
GroupNameAttribute: testGroupNameAttrName,
|
||||
},
|
||||
UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": upstreamldap.MicrosoftUUIDFromBinary("objectGUID")},
|
||||
RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.StoredRefreshAttributes) error{"pwdLastSet": upstreamldap.PwdUnchangedSinceLogin},
|
||||
},
|
||||
},
|
||||
wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{
|
||||
@ -1273,6 +1282,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
|
||||
},
|
||||
UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": upstreamldap.MicrosoftUUIDFromBinary("objectGUID")},
|
||||
GroupAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"sAMAccountName": upstreamldap.GroupSAMAccountNameWithDomainSuffix},
|
||||
RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.StoredRefreshAttributes) error{"pwdLastSet": upstreamldap.PwdUnchangedSinceLogin},
|
||||
},
|
||||
},
|
||||
wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{
|
||||
@ -1325,6 +1335,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
|
||||
GroupNameAttribute: testGroupNameAttrName,
|
||||
},
|
||||
UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": upstreamldap.MicrosoftUUIDFromBinary("objectGUID")},
|
||||
RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.StoredRefreshAttributes) error{"pwdLastSet": upstreamldap.PwdUnchangedSinceLogin},
|
||||
},
|
||||
},
|
||||
wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{
|
||||
@ -1381,6 +1392,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
|
||||
GroupNameAttribute: testGroupNameAttrName,
|
||||
},
|
||||
UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": upstreamldap.MicrosoftUUIDFromBinary("objectGUID")},
|
||||
RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.StoredRefreshAttributes) error{"pwdLastSet": upstreamldap.PwdUnchangedSinceLogin},
|
||||
},
|
||||
},
|
||||
wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{
|
||||
@ -1431,6 +1443,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
|
||||
GroupNameAttribute: testGroupNameAttrName,
|
||||
},
|
||||
UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": upstreamldap.MicrosoftUUIDFromBinary("objectGUID")},
|
||||
RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.StoredRefreshAttributes) error{"pwdLastSet": upstreamldap.PwdUnchangedSinceLogin},
|
||||
},
|
||||
},
|
||||
wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{
|
||||
@ -1627,6 +1640,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
|
||||
GroupNameAttribute: testGroupNameAttrName,
|
||||
},
|
||||
UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": upstreamldap.MicrosoftUUIDFromBinary("objectGUID")},
|
||||
RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.StoredRefreshAttributes) error{"pwdLastSet": upstreamldap.PwdUnchangedSinceLogin},
|
||||
},
|
||||
},
|
||||
wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{
|
||||
@ -1753,6 +1767,16 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
|
||||
require.Equal(t, reflect.ValueOf(v).Pointer(), reflect.ValueOf(actualGroupAttributeParsingOverrides[k]).Pointer())
|
||||
}
|
||||
|
||||
expectedRefreshAttributeChecks := copyOfExpectedValueForResultingCache.RefreshAttributeChecks
|
||||
actualRefreshAttributeChecks := actualConfig.RefreshAttributeChecks
|
||||
copyOfExpectedValueForResultingCache.RefreshAttributeChecks = map[string]func(*ldap.Entry, provider.StoredRefreshAttributes) error{}
|
||||
actualConfig.RefreshAttributeChecks = map[string]func(*ldap.Entry, provider.StoredRefreshAttributes) error{}
|
||||
require.Equal(t, len(expectedRefreshAttributeChecks), len(actualRefreshAttributeChecks))
|
||||
for k, v := range expectedRefreshAttributeChecks {
|
||||
require.NotNil(t, actualRefreshAttributeChecks[k])
|
||||
require.Equal(t, reflect.ValueOf(v).Pointer(), reflect.ValueOf(actualRefreshAttributeChecks[k]).Pointer())
|
||||
}
|
||||
|
||||
require.Equal(t, copyOfExpectedValueForResultingCache, actualConfig)
|
||||
}
|
||||
|
||||
|
@ -7,6 +7,7 @@ import (
|
||||
"context"
|
||||
"net/url"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
@ -93,7 +94,14 @@ type UpstreamLDAPIdentityProviderI interface {
|
||||
authenticators.UserAuthenticator
|
||||
|
||||
// PerformRefresh performs a refresh against the upstream LDAP identity provider
|
||||
PerformRefresh(ctx context.Context, userDN, expectedUsername, expectedSubject string) error
|
||||
PerformRefresh(ctx context.Context, storedRefreshAttributes StoredRefreshAttributes) error
|
||||
}
|
||||
|
||||
type StoredRefreshAttributes struct {
|
||||
Username string
|
||||
Subject string
|
||||
DN string
|
||||
AuthTime time.Time
|
||||
}
|
||||
|
||||
type DynamicUpstreamIDPProvider interface {
|
||||
|
@ -75,11 +75,6 @@ func NewHandler(
|
||||
|
||||
func upstreamRefresh(ctx context.Context, accessRequest fosite.AccessRequester, providerCache oidc.UpstreamIdentityProvidersLister) error {
|
||||
session := accessRequest.GetSession().(*psession.PinnipedSession)
|
||||
downstreamUsername, err := getDownstreamUsernameFromPinnipedSession(session)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
downstreamSubject := session.Fosite.Claims.Subject
|
||||
|
||||
customSessionData := session.Custom
|
||||
if customSessionData == nil {
|
||||
@ -95,9 +90,9 @@ func upstreamRefresh(ctx context.Context, accessRequest fosite.AccessRequester,
|
||||
case psession.ProviderTypeOIDC:
|
||||
return upstreamOIDCRefresh(ctx, customSessionData, providerCache)
|
||||
case psession.ProviderTypeLDAP:
|
||||
return upstreamLDAPRefresh(ctx, customSessionData, providerCache, downstreamUsername, downstreamSubject)
|
||||
return upstreamLDAPRefresh(ctx, providerCache, session)
|
||||
case psession.ProviderTypeActiveDirectory:
|
||||
return upstreamLDAPRefresh(ctx, customSessionData, providerCache, downstreamUsername, downstreamSubject)
|
||||
return upstreamLDAPRefresh(ctx, providerCache, session)
|
||||
default:
|
||||
return errorsx.WithStack(errMissingUpstreamSessionInternalError)
|
||||
}
|
||||
@ -169,7 +164,15 @@ func findOIDCProviderByNameAndValidateUID(
|
||||
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 {
|
||||
func upstreamLDAPRefresh(ctx context.Context, providerCache oidc.UpstreamIdentityProvidersLister, session *psession.PinnipedSession) error {
|
||||
username, err := getDownstreamUsernameFromPinnipedSession(session)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
subject := session.Fosite.Claims.Subject
|
||||
|
||||
s := session.Custom
|
||||
|
||||
// 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 != ""
|
||||
@ -182,8 +185,16 @@ func upstreamLDAPRefresh(ctx context.Context, s *psession.CustomSessionData, pro
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if session.IDTokenClaims().AuthTime.IsZero() {
|
||||
return errorsx.WithStack(errMissingUpstreamSessionInternalError)
|
||||
}
|
||||
// run PerformRefresh
|
||||
err = p.PerformRefresh(ctx, dn, username, subject)
|
||||
err = p.PerformRefresh(ctx, provider.StoredRefreshAttributes{
|
||||
Username: username,
|
||||
Subject: subject,
|
||||
DN: dn,
|
||||
AuthTime: session.IDTokenClaims().AuthTime,
|
||||
})
|
||||
if err != nil {
|
||||
return errorsx.WithStack(errUpstreamRefreshError.WithHint(
|
||||
"Upstream refresh failed.").WithWrap(err).WithDebugf("provider name: %q, provider type: %q", s.ProviderName, s.ProviderType))
|
||||
@ -235,16 +246,3 @@ func getDownstreamUsernameFromPinnipedSession(session *psession.PinnipedSession)
|
||||
}
|
||||
return downstreamUsername, nil
|
||||
}
|
||||
|
||||
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 := downstreamUsernameInterface.(string)
|
||||
return downstreamUsername, nil
|
||||
}
|
||||
|
@ -2181,6 +2181,45 @@ func TestRefreshGrant(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "auth time is the zero value", // time.Times can never be nil, but it is possible that it would be the zero value which would mean something's wrong
|
||||
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)
|
||||
fositeSessionClaims := session.Fosite.IDTokenClaims()
|
||||
fositeSessionClaims.AuthTime = time.Time{}
|
||||
session.Fosite.Claims = fositeSessionClaims
|
||||
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{
|
||||
@ -2201,7 +2240,7 @@ func TestRefreshGrant(t *testing.T) {
|
||||
wantErrorResponseBody: here.Doc(`
|
||||
{
|
||||
"error": "error",
|
||||
"error_description": "Error during upstream refresh. Provider 'some-ldap-idp' of type 'ldap' from upstream session data has changed its resource UID since authentication."
|
||||
"error_description": "Error during upstream refresh. Provider from upstream session data has changed its resource UID since authentication."
|
||||
}
|
||||
`),
|
||||
},
|
||||
@ -2227,7 +2266,7 @@ func TestRefreshGrant(t *testing.T) {
|
||||
wantErrorResponseBody: here.Doc(`
|
||||
{
|
||||
"error": "error",
|
||||
"error_description": "Error during upstream refresh. Provider 'some-ad-idp' of type 'activedirectory' from upstream session data has changed its resource UID since authentication."
|
||||
"error_description": "Error during upstream refresh. Provider from upstream session data has changed its resource UID since authentication."
|
||||
}
|
||||
`),
|
||||
},
|
||||
|
@ -111,16 +111,16 @@ func (u *TestUpstreamLDAPIdentityProvider) GetURL() *url.URL {
|
||||
return u.URL
|
||||
}
|
||||
|
||||
func (u *TestUpstreamLDAPIdentityProvider) PerformRefresh(ctx context.Context, userDN, expectedUsername, expectedSubject string) error {
|
||||
func (u *TestUpstreamLDAPIdentityProvider) PerformRefresh(ctx context.Context, storedRefreshAttributes provider.StoredRefreshAttributes) 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,
|
||||
DN: storedRefreshAttributes.DN,
|
||||
ExpectedUsername: storedRefreshAttributes.Username,
|
||||
ExpectedSubject: storedRefreshAttributes.Subject,
|
||||
})
|
||||
if u.PerformRefreshErr != nil {
|
||||
return u.PerformRefreshErr
|
||||
|
@ -15,6 +15,7 @@ import (
|
||||
"net/url"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@ -40,6 +41,7 @@ const (
|
||||
defaultLDAPPort = uint16(389)
|
||||
defaultLDAPSPort = uint16(636)
|
||||
sAMAccountNameAttribute = "sAMAccountName"
|
||||
pwdLastSetAttribute = "pwdLastSet"
|
||||
)
|
||||
|
||||
// Conn abstracts the upstream LDAP communication protocol (mostly for testing).
|
||||
@ -119,6 +121,9 @@ type ProviderConfig struct {
|
||||
// GroupNameMappingOverrides are the mappings between an attribute name and a way to parse it as a group
|
||||
// name when it comes out of LDAP.
|
||||
GroupAttributeParsingOverrides map[string]func(*ldap.Entry) (string, error)
|
||||
|
||||
// RefreshAttributeChecks are extra checks that attributes in a refresh response are as expected.
|
||||
RefreshAttributeChecks map[string]func(*ldap.Entry, provider.StoredRefreshAttributes) error
|
||||
}
|
||||
|
||||
// UserSearchConfig contains information about how to search for users in the upstream LDAP IDP.
|
||||
@ -170,9 +175,11 @@ func (p *Provider) GetConfig() ProviderConfig {
|
||||
return p.c
|
||||
}
|
||||
|
||||
func (p *Provider) PerformRefresh(ctx context.Context, userDN, expectedUsername, expectedSubject string) error {
|
||||
func (p *Provider) PerformRefresh(ctx context.Context, storedRefreshAttributes provider.StoredRefreshAttributes) error {
|
||||
t := trace.FromContext(ctx).Nest("slow ldap refresh attempt", trace.Field{Key: "providerName", Value: p.GetName()})
|
||||
defer t.LogIfLong(500 * time.Millisecond) // to help users debug slow LDAP searches
|
||||
userDN := storedRefreshAttributes.DN
|
||||
|
||||
searchResult, err := p.performRefresh(ctx, userDN)
|
||||
if err != nil {
|
||||
p.traceRefreshFailure(t, err)
|
||||
@ -196,9 +203,9 @@ func (p *Provider) PerformRefresh(ctx context.Context, userDN, expectedUsername,
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if newUsername != expectedUsername {
|
||||
if newUsername != storedRefreshAttributes.Username {
|
||||
return fmt.Errorf(`searching for user "%s" returned a different username than the previous value. expected: "%s", actual: "%s"`,
|
||||
userDN, expectedUsername, newUsername,
|
||||
userDN, storedRefreshAttributes.Username, newUsername,
|
||||
)
|
||||
}
|
||||
|
||||
@ -207,10 +214,15 @@ func (p *Provider) PerformRefresh(ctx context.Context, userDN, expectedUsername,
|
||||
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)
|
||||
if newSubject != storedRefreshAttributes.Subject {
|
||||
return fmt.Errorf(`searching for user "%s" produced a different subject than the previous value. expected: "%s", actual: "%s"`, userDN, storedRefreshAttributes.Subject, newSubject)
|
||||
}
|
||||
for attribute, validateFunc := range p.c.RefreshAttributeChecks {
|
||||
err = validateFunc(userEntry, storedRefreshAttributes)
|
||||
if err != nil {
|
||||
return fmt.Errorf(`validation for attribute "%s" failed during upstream refresh: %w`, attribute, err)
|
||||
}
|
||||
}
|
||||
|
||||
// we checked that the user still exists and their information is the same, so just return.
|
||||
return nil
|
||||
}
|
||||
@ -652,7 +664,7 @@ func (p *Provider) refreshUserSearchRequest(dn string) *ldap.SearchRequest {
|
||||
TimeLimit: 90,
|
||||
TypesOnly: false,
|
||||
Filter: "(objectClass=*)", // we already have the dn, so the filter doesn't matter
|
||||
Attributes: p.userSearchRequestedAttributes(),
|
||||
Attributes: p.refreshAttributes(),
|
||||
Controls: nil, // this could be used to enable paging, but we're already limiting the result max size
|
||||
}
|
||||
}
|
||||
@ -679,6 +691,14 @@ func (p *Provider) groupSearchRequestedAttributes() []string {
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Provider) refreshAttributes() []string {
|
||||
attributes := p.userSearchRequestedAttributes()
|
||||
for k := range p.c.RefreshAttributeChecks {
|
||||
attributes = append(attributes, k)
|
||||
}
|
||||
return attributes
|
||||
}
|
||||
|
||||
func (p *Provider) userSearchFilter(username string) string {
|
||||
safeUsername := p.escapeUsernameForSearchFilter(username)
|
||||
if len(p.c.UserSearch.Filter) == 0 {
|
||||
@ -836,3 +856,42 @@ func getDomainFromDistinguishedName(distinguishedName string) (string, error) {
|
||||
}
|
||||
return strings.Join(domainComponents[1:], "."), nil
|
||||
}
|
||||
|
||||
func PwdUnchangedSinceLogin(entry *ldap.Entry, attributes provider.StoredRefreshAttributes) error {
|
||||
authTime := attributes.AuthTime
|
||||
pwdLastSetWin32Format := entry.GetAttributeValues(pwdLastSetAttribute)
|
||||
if len(pwdLastSetWin32Format) != 1 {
|
||||
return fmt.Errorf("expected to find 1 value for %s attribute, but found %d", pwdLastSetAttribute, len(pwdLastSetWin32Format))
|
||||
}
|
||||
// convert to a time.Time
|
||||
pwdLastSetParsed, err := win32timestampToTime(pwdLastSetWin32Format[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// if pwdLastSet > authTime, that means that the password has been changed since the initial login.
|
||||
// return an error so the user is prompted to log in again.
|
||||
if pwdLastSetParsed.After(authTime) {
|
||||
return fmt.Errorf("password has changed since login. login time: %s, password set time: %s", authTime, pwdLastSetParsed)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func win32timestampToTime(win32timestamp string) (*time.Time, error) {
|
||||
// take a win32 timestamp (represented as the number of 100 ns intervals since
|
||||
// January 1, 1601) and make a time.Time
|
||||
|
||||
const unixTimeBaseAsWin = 116444736000000000 // The unix base time (January 1, 1970 UTC) as 100 ns since Win32 epoch (1601-01-01)
|
||||
const hundredNsToSecFactor = 10000000
|
||||
|
||||
win32Time, err := strconv.ParseUint(win32timestamp, 10, 64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("couldn't parse as timestamp")
|
||||
}
|
||||
|
||||
unixsec := int64(win32Time-unixTimeBaseAsWin) / hundredNsToSecFactor
|
||||
unixns := int64(win32Time % hundredNsToSecFactor)
|
||||
|
||||
convertedTime := time.Unix(unixsec, unixns).UTC()
|
||||
return &convertedTime, nil
|
||||
}
|
||||
|
@ -16,6 +16,8 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
provider2 "go.pinniped.dev/internal/oidc/provider"
|
||||
|
||||
"github.com/go-ldap/ldap/v3"
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/stretchr/testify/require"
|
||||
@ -1217,6 +1219,7 @@ func TestEndUserAuthentication(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestUpstreamRefresh(t *testing.T) {
|
||||
pwdLastSetAttribute := "pwdLastSet"
|
||||
expectedUserSearch := &ldap.SearchRequest{
|
||||
BaseDN: testUserSearchResultDNValue,
|
||||
Scope: ldap.ScopeBaseObject,
|
||||
@ -1225,7 +1228,7 @@ func TestUpstreamRefresh(t *testing.T) {
|
||||
TimeLimit: 90,
|
||||
TypesOnly: false,
|
||||
Filter: "(objectClass=*)",
|
||||
Attributes: []string{testUserSearchUsernameAttribute, testUserSearchUIDAttribute},
|
||||
Attributes: []string{testUserSearchUsernameAttribute, testUserSearchUIDAttribute, pwdLastSetAttribute},
|
||||
Controls: nil, // don't need paging because we set the SizeLimit so small
|
||||
}
|
||||
|
||||
@ -1242,6 +1245,10 @@ func TestUpstreamRefresh(t *testing.T) {
|
||||
Name: testUserSearchUIDAttribute,
|
||||
ByteValues: [][]byte{[]byte(testUserSearchResultUIDAttributeValue)},
|
||||
},
|
||||
{
|
||||
Name: pwdLastSetAttribute,
|
||||
Values: []string{"132801740800000000"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -1259,6 +1266,7 @@ func TestUpstreamRefresh(t *testing.T) {
|
||||
UIDAttribute: testUserSearchUIDAttribute,
|
||||
UsernameAttribute: testUserSearchUsernameAttribute,
|
||||
},
|
||||
RefreshAttributeChecks: map[string]func(*ldap.Entry, provider2.StoredRefreshAttributes) error{pwdLastSetAttribute: PwdUnchangedSinceLogin},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
@ -1512,6 +1520,36 @@ func TestUpstreamRefresh(t *testing.T) {
|
||||
},
|
||||
wantErr: "found 2 values for attribute \"some-upstream-uid-attribute\" while searching for user \"some-upstream-user-dn\", but expected 1 result",
|
||||
},
|
||||
{
|
||||
name: "search result has a recent pwdLastSet value",
|
||||
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)},
|
||||
},
|
||||
{
|
||||
Name: pwdLastSetAttribute,
|
||||
Values: []string{"132803468800000000"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}, nil).Times(1)
|
||||
conn.EXPECT().Close().Times(1)
|
||||
},
|
||||
wantErr: "validation for attribute \"pwdLastSet\" failed during upstream refresh: password has changed since login. login time: 2021-11-01 23:43:19 +0000 UTC, password set time: 2021-11-02 17:14:40 +0000 UTC",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
@ -1538,7 +1576,13 @@ func TestUpstreamRefresh(t *testing.T) {
|
||||
|
||||
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)
|
||||
authTime := time.Date(2021, time.November, 1, 23, 43, 19, 0, time.UTC)
|
||||
err := provider.PerformRefresh(context.Background(), provider2.StoredRefreshAttributes{
|
||||
Username: testUserSearchResultUsernameAttributeValue,
|
||||
Subject: subject,
|
||||
DN: testUserSearchResultDNValue,
|
||||
AuthTime: authTime,
|
||||
})
|
||||
if tt.wantErr != "" {
|
||||
require.Error(t, err)
|
||||
require.Equal(t, tt.wantErr, err.Error())
|
||||
@ -1916,3 +1960,135 @@ func TestGetDomainFromDistinguishedName(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPwdUnchangedSinceLogin(t *testing.T) {
|
||||
authTime := "2021-11-01T23:43:19.826433579Z" // this is the format that fosite automatically stores
|
||||
authTimeParsed, err := time.Parse(time.RFC3339Nano, authTime)
|
||||
require.NoError(t, err)
|
||||
pwdResetTimeAfterAuthTime := "132803468800000000" // Nov 2
|
||||
pwdResetTimeBeforeAuthTime := "132801740800000000" // Oct 31
|
||||
tests := []struct {
|
||||
name string
|
||||
authTime *time.Time
|
||||
entry *ldap.Entry
|
||||
wantResult bool
|
||||
wantErr string
|
||||
}{
|
||||
{
|
||||
name: "happy path where password has not been reset since login",
|
||||
authTime: &authTimeParsed,
|
||||
entry: &ldap.Entry{
|
||||
DN: "some-dn",
|
||||
Attributes: []*ldap.EntryAttribute{
|
||||
{
|
||||
Name: "pwdLastSet",
|
||||
Values: []string{pwdResetTimeBeforeAuthTime},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "password has been reset since login",
|
||||
authTime: &authTimeParsed,
|
||||
entry: &ldap.Entry{
|
||||
DN: "some-dn",
|
||||
Attributes: []*ldap.EntryAttribute{
|
||||
{
|
||||
Name: "pwdLastSet",
|
||||
Values: []string{pwdResetTimeAfterAuthTime},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: "password has changed since login. login time: 2021-11-01 23:43:19.826433579 +0000 UTC, password set time: 2021-11-02 17:14:40 +0000 UTC",
|
||||
},
|
||||
{
|
||||
name: "ldap timestamp is in the wrong format",
|
||||
authTime: &authTimeParsed,
|
||||
entry: &ldap.Entry{
|
||||
DN: "some-dn",
|
||||
Attributes: []*ldap.EntryAttribute{
|
||||
{
|
||||
Name: "pwdLastSet",
|
||||
Values: []string{"invalid"},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: "couldn't parse as timestamp",
|
||||
},
|
||||
{
|
||||
name: "no value for pwdLastSet attribute",
|
||||
authTime: &authTimeParsed,
|
||||
entry: &ldap.Entry{
|
||||
DN: "some-dn",
|
||||
Attributes: []*ldap.EntryAttribute{},
|
||||
},
|
||||
wantErr: "expected to find 1 value for pwdLastSet attribute, but found 0",
|
||||
},
|
||||
{
|
||||
name: "too many values for pwdLastSet attribute",
|
||||
authTime: &authTimeParsed,
|
||||
entry: &ldap.Entry{
|
||||
DN: "some-dn",
|
||||
Attributes: []*ldap.EntryAttribute{
|
||||
{
|
||||
Name: "pwdLastSet",
|
||||
Values: []string{"val1", "val2"},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: "expected to find 1 value for pwdLastSet attribute, but found 2",
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
tt := test
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := PwdUnchangedSinceLogin(tt.entry, provider2.StoredRefreshAttributes{AuthTime: *tt.authTime})
|
||||
if tt.wantErr != "" {
|
||||
require.Error(t, err)
|
||||
require.Equal(t, tt.wantErr, err.Error())
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestWin32TimestampToTime(t *testing.T) {
|
||||
happyPasswordChangeTime := time.Date(2021, time.January, 2, 0, 12, 21, 0, time.UTC).UTC()
|
||||
tests := []struct {
|
||||
name string
|
||||
timestampString string
|
||||
wantTime *time.Time
|
||||
wantErr string
|
||||
}{
|
||||
{
|
||||
name: "happy case with a valid timestamp",
|
||||
timestampString: "132540199410000000",
|
||||
wantTime: &happyPasswordChangeTime,
|
||||
},
|
||||
{
|
||||
name: "handles error with a string thats not a timestamp",
|
||||
timestampString: "not timestamp",
|
||||
wantErr: "couldn't parse as timestamp",
|
||||
},
|
||||
{
|
||||
name: "handles error with too big of a timestamp",
|
||||
timestampString: "132540199410000000000",
|
||||
wantErr: "couldn't parse as timestamp",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
tt := test
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
actualTime, err := win32timestampToTime(tt.timestampString)
|
||||
require.Equal(t, tt.wantTime, actualTime)
|
||||
if tt.wantErr != "" {
|
||||
require.Error(t, err)
|
||||
require.Equal(t, tt.wantErr, err.Error())
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -5,11 +5,16 @@ package integration
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"math/big"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
@ -19,9 +24,11 @@ import (
|
||||
"time"
|
||||
|
||||
coreosoidc "github.com/coreos/go-oidc/v3/oidc"
|
||||
"github.com/go-ldap/ldap/v3"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/oauth2"
|
||||
"golang.org/x/text/encoding/unicode"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
@ -44,18 +51,19 @@ func TestSupervisorLogin(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
maybeSkip func(t *testing.T)
|
||||
createTestUser func(t *testing.T) (string, string)
|
||||
deleteTestUser func(t *testing.T, username string)
|
||||
requestAuthorization func(t *testing.T, downstreamAuthorizeURL, downstreamCallbackURL, username, password string, httpClient *http.Client)
|
||||
createIDP func(t *testing.T) string
|
||||
requestAuthorization func(t *testing.T, downstreamAuthorizeURL, downstreamCallbackURL string, httpClient *http.Client)
|
||||
wantDownstreamIDTokenSubjectToMatch string
|
||||
wantDownstreamIDTokenUsernameToMatch string
|
||||
wantDownstreamIDTokenUsernameToMatch func(username string) string
|
||||
wantDownstreamIDTokenGroups []string
|
||||
wantErrorDescription string
|
||||
wantErrorType string
|
||||
|
||||
// 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
|
||||
// Either revoke the user's session on the upstream provider, or manipulate the user's session
|
||||
// data in such a way that it should cause the next upstream refresh attempt to fail.
|
||||
breakRefreshSessionData func(t *testing.T, sessionData *psession.PinnipedSession, idpName string)
|
||||
breakRefreshSessionData func(t *testing.T, sessionData *psession.PinnipedSession, idpName, username string)
|
||||
}{
|
||||
{
|
||||
name: "oidc with default username and groups claim settings",
|
||||
@ -76,7 +84,7 @@ func TestSupervisorLogin(t *testing.T) {
|
||||
return oidcIDP.Name
|
||||
},
|
||||
requestAuthorization: requestAuthorizationUsingBrowserAuthcodeFlow,
|
||||
breakRefreshSessionData: func(t *testing.T, pinnipedSession *psession.PinnipedSession, _ string) {
|
||||
breakRefreshSessionData: func(t *testing.T, pinnipedSession *psession.PinnipedSession, _, _ string) {
|
||||
customSessionData := pinnipedSession.Custom
|
||||
require.Equal(t, psession.ProviderTypeOIDC, customSessionData.ProviderType)
|
||||
require.NotEmpty(t, customSessionData.OIDC.UpstreamRefreshToken)
|
||||
@ -85,7 +93,7 @@ func TestSupervisorLogin(t *testing.T) {
|
||||
// the ID token Subject should include the upstream user ID after the upstream issuer name
|
||||
wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta(env.SupervisorUpstreamOIDC.Issuer+"?sub=") + ".+",
|
||||
// the ID token Username should include the upstream user ID after the upstream issuer name
|
||||
wantDownstreamIDTokenUsernameToMatch: "^" + regexp.QuoteMeta(env.SupervisorUpstreamOIDC.Issuer+"?sub=") + ".+",
|
||||
wantDownstreamIDTokenUsernameToMatch: func(_ string) string { return "^" + regexp.QuoteMeta(env.SupervisorUpstreamOIDC.Issuer+"?sub=") + ".+" },
|
||||
},
|
||||
{
|
||||
name: "oidc with custom username and groups claim settings",
|
||||
@ -113,14 +121,14 @@ func TestSupervisorLogin(t *testing.T) {
|
||||
return oidcIDP.Name
|
||||
},
|
||||
requestAuthorization: requestAuthorizationUsingBrowserAuthcodeFlow,
|
||||
breakRefreshSessionData: func(t *testing.T, pinnipedSession *psession.PinnipedSession, _ string) {
|
||||
breakRefreshSessionData: func(t *testing.T, pinnipedSession *psession.PinnipedSession, _, _ string) {
|
||||
customSessionData := pinnipedSession.Custom
|
||||
require.Equal(t, psession.ProviderTypeOIDC, customSessionData.ProviderType)
|
||||
require.NotEmpty(t, customSessionData.OIDC.UpstreamRefreshToken)
|
||||
customSessionData.OIDC.UpstreamRefreshToken = "invalid-updated-refresh-token"
|
||||
},
|
||||
wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta(env.SupervisorUpstreamOIDC.Issuer+"?sub=") + ".+",
|
||||
wantDownstreamIDTokenUsernameToMatch: "^" + regexp.QuoteMeta(env.SupervisorUpstreamOIDC.Username) + "$",
|
||||
wantDownstreamIDTokenUsernameToMatch: func(_ string) string { return "^" + regexp.QuoteMeta(env.SupervisorUpstreamOIDC.Username) + "$" },
|
||||
wantDownstreamIDTokenGroups: env.SupervisorUpstreamOIDC.ExpectedGroups,
|
||||
},
|
||||
{
|
||||
@ -144,7 +152,7 @@ func TestSupervisorLogin(t *testing.T) {
|
||||
}, 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,
|
||||
downstreamAuthorizeURL,
|
||||
env.SupervisorUpstreamOIDC.Username, // username to present to server during login
|
||||
@ -153,7 +161,7 @@ func TestSupervisorLogin(t *testing.T) {
|
||||
false,
|
||||
)
|
||||
},
|
||||
breakRefreshSessionData: func(t *testing.T, pinnipedSession *psession.PinnipedSession, _ string) {
|
||||
breakRefreshSessionData: func(t *testing.T, pinnipedSession *psession.PinnipedSession, _, _ string) {
|
||||
customSessionData := pinnipedSession.Custom
|
||||
require.Equal(t, psession.ProviderTypeOIDC, customSessionData.ProviderType)
|
||||
require.NotEmpty(t, customSessionData.OIDC.UpstreamRefreshToken)
|
||||
@ -162,7 +170,7 @@ func TestSupervisorLogin(t *testing.T) {
|
||||
// the ID token Subject should include the upstream user ID after the upstream issuer name
|
||||
wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta(env.SupervisorUpstreamOIDC.Issuer+"?sub=") + ".+",
|
||||
// the ID token Username should include the upstream user ID after the upstream issuer name
|
||||
wantDownstreamIDTokenUsernameToMatch: "^" + regexp.QuoteMeta(env.SupervisorUpstreamOIDC.Issuer+"?sub=") + ".+",
|
||||
wantDownstreamIDTokenUsernameToMatch: func(_ string) string { return "^" + regexp.QuoteMeta(env.SupervisorUpstreamOIDC.Issuer+"?sub=") + ".+" },
|
||||
},
|
||||
{
|
||||
name: "ldap with email as username and groups names as DNs and using an LDAP provider which supports TLS",
|
||||
@ -212,7 +220,7 @@ func TestSupervisorLogin(t *testing.T) {
|
||||
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,
|
||||
downstreamAuthorizeURL,
|
||||
env.SupervisorUpstreamLDAP.TestUserMailAttributeValue, // username to present to server during login
|
||||
@ -221,7 +229,7 @@ func TestSupervisorLogin(t *testing.T) {
|
||||
false,
|
||||
)
|
||||
},
|
||||
breakRefreshSessionData: func(t *testing.T, pinnipedSession *psession.PinnipedSession, _ string) {
|
||||
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)
|
||||
@ -235,8 +243,10 @@ func TestSupervisorLogin(t *testing.T) {
|
||||
"&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,
|
||||
wantDownstreamIDTokenUsernameToMatch: func(_ string) string {
|
||||
return "^" + regexp.QuoteMeta(env.SupervisorUpstreamLDAP.TestUserMailAttributeValue) + "$"
|
||||
},
|
||||
wantDownstreamIDTokenGroups: env.SupervisorUpstreamLDAP.TestUserDirectGroupsDNs,
|
||||
},
|
||||
{
|
||||
name: "ldap with CN as username and group names as CNs and using an LDAP provider which only supports StartTLS", // try another variation of configuration options
|
||||
@ -286,7 +296,7 @@ func TestSupervisorLogin(t *testing.T) {
|
||||
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,
|
||||
downstreamAuthorizeURL,
|
||||
env.SupervisorUpstreamLDAP.TestUserCN, // username to present to server during login
|
||||
@ -295,7 +305,7 @@ func TestSupervisorLogin(t *testing.T) {
|
||||
false,
|
||||
)
|
||||
},
|
||||
breakRefreshSessionData: func(t *testing.T, pinnipedSession *psession.PinnipedSession, _ string) {
|
||||
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)
|
||||
@ -309,7 +319,7 @@ func TestSupervisorLogin(t *testing.T) {
|
||||
"&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.TestUserDN) + "$",
|
||||
wantDownstreamIDTokenUsernameToMatch: func(_ string) string { return "^" + regexp.QuoteMeta(env.SupervisorUpstreamLDAP.TestUserDN) + "$" },
|
||||
wantDownstreamIDTokenGroups: env.SupervisorUpstreamLDAP.TestUserDirectGroupsCNs,
|
||||
},
|
||||
{
|
||||
@ -360,7 +370,7 @@ func TestSupervisorLogin(t *testing.T) {
|
||||
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,
|
||||
downstreamAuthorizeURL,
|
||||
env.SupervisorUpstreamLDAP.TestUserMailAttributeValue, // username to present to server during login
|
||||
@ -438,7 +448,7 @@ func TestSupervisorLogin(t *testing.T) {
|
||||
}, 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,
|
||||
downstreamAuthorizeURL,
|
||||
env.SupervisorUpstreamLDAP.TestUserMailAttributeValue, // username to present to server during login
|
||||
@ -447,7 +457,7 @@ func TestSupervisorLogin(t *testing.T) {
|
||||
false,
|
||||
)
|
||||
},
|
||||
breakRefreshSessionData: func(t *testing.T, pinnipedSession *psession.PinnipedSession, _ string) {
|
||||
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)
|
||||
@ -460,8 +470,10 @@ func TestSupervisorLogin(t *testing.T) {
|
||||
"&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,
|
||||
wantDownstreamIDTokenUsernameToMatch: func(_ string) string {
|
||||
return "^" + regexp.QuoteMeta(env.SupervisorUpstreamLDAP.TestUserMailAttributeValue) + "$"
|
||||
},
|
||||
wantDownstreamIDTokenGroups: env.SupervisorUpstreamLDAP.TestUserDirectGroupsDNs,
|
||||
},
|
||||
{
|
||||
name: "ldap login still works after deleting and recreating the bind secret",
|
||||
@ -543,7 +555,7 @@ func TestSupervisorLogin(t *testing.T) {
|
||||
}, 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,
|
||||
downstreamAuthorizeURL,
|
||||
env.SupervisorUpstreamLDAP.TestUserMailAttributeValue, // username to present to server during login
|
||||
@ -552,7 +564,7 @@ func TestSupervisorLogin(t *testing.T) {
|
||||
false,
|
||||
)
|
||||
},
|
||||
breakRefreshSessionData: func(t *testing.T, pinnipedSession *psession.PinnipedSession, _ string) {
|
||||
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)
|
||||
@ -565,8 +577,10 @@ func TestSupervisorLogin(t *testing.T) {
|
||||
"&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,
|
||||
wantDownstreamIDTokenUsernameToMatch: func(_ string) string {
|
||||
return "^" + regexp.QuoteMeta(env.SupervisorUpstreamLDAP.TestUserMailAttributeValue) + "$"
|
||||
},
|
||||
wantDownstreamIDTokenGroups: env.SupervisorUpstreamLDAP.TestUserDirectGroupsDNs,
|
||||
},
|
||||
{
|
||||
name: "activedirectory with all default options",
|
||||
@ -604,7 +618,7 @@ func TestSupervisorLogin(t *testing.T) {
|
||||
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,
|
||||
downstreamAuthorizeURL,
|
||||
env.SupervisorUpstreamActiveDirectory.TestUserPrincipalNameValue, // username to present to server during login
|
||||
@ -613,7 +627,7 @@ func TestSupervisorLogin(t *testing.T) {
|
||||
false,
|
||||
)
|
||||
},
|
||||
breakRefreshSessionData: func(t *testing.T, pinnipedSession *psession.PinnipedSession, _ string) {
|
||||
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)
|
||||
@ -627,8 +641,10 @@ func TestSupervisorLogin(t *testing.T) {
|
||||
"&sub="+env.SupervisorUpstreamActiveDirectory.TestUserUniqueIDAttributeValue,
|
||||
) + "$",
|
||||
// the ID token Username should have been pulled from the requested UserSearch.Attributes.Username attribute
|
||||
wantDownstreamIDTokenUsernameToMatch: "^" + regexp.QuoteMeta(env.SupervisorUpstreamActiveDirectory.TestUserPrincipalNameValue) + "$",
|
||||
wantDownstreamIDTokenGroups: env.SupervisorUpstreamActiveDirectory.TestUserIndirectGroupsSAMAccountPlusDomainNames,
|
||||
wantDownstreamIDTokenUsernameToMatch: func(_ string) string {
|
||||
return "^" + regexp.QuoteMeta(env.SupervisorUpstreamActiveDirectory.TestUserPrincipalNameValue) + "$"
|
||||
},
|
||||
wantDownstreamIDTokenGroups: env.SupervisorUpstreamActiveDirectory.TestUserIndirectGroupsSAMAccountPlusDomainNames,
|
||||
}, {
|
||||
name: "activedirectory with custom options",
|
||||
maybeSkip: func(t *testing.T) {
|
||||
@ -679,7 +695,7 @@ func TestSupervisorLogin(t *testing.T) {
|
||||
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,
|
||||
downstreamAuthorizeURL,
|
||||
env.SupervisorUpstreamActiveDirectory.TestUserMailAttributeValue, // username to present to server during login
|
||||
@ -688,7 +704,7 @@ func TestSupervisorLogin(t *testing.T) {
|
||||
false,
|
||||
)
|
||||
},
|
||||
breakRefreshSessionData: func(t *testing.T, pinnipedSession *psession.PinnipedSession, _ string) {
|
||||
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)
|
||||
@ -702,8 +718,10 @@ func TestSupervisorLogin(t *testing.T) {
|
||||
"&sub="+env.SupervisorUpstreamActiveDirectory.TestUserUniqueIDAttributeValue,
|
||||
) + "$",
|
||||
// the ID token Username should have been pulled from the requested UserSearch.Attributes.Username attribute
|
||||
wantDownstreamIDTokenUsernameToMatch: "^" + regexp.QuoteMeta(env.SupervisorUpstreamActiveDirectory.TestUserMailAttributeValue) + "$",
|
||||
wantDownstreamIDTokenGroups: env.SupervisorUpstreamActiveDirectory.TestUserDirectGroupsDNs,
|
||||
wantDownstreamIDTokenUsernameToMatch: func(_ string) string {
|
||||
return "^" + regexp.QuoteMeta(env.SupervisorUpstreamActiveDirectory.TestUserMailAttributeValue) + "$"
|
||||
},
|
||||
wantDownstreamIDTokenGroups: env.SupervisorUpstreamActiveDirectory.TestUserDirectGroupsDNs,
|
||||
},
|
||||
{
|
||||
name: "active directory login still works after updating bind secret",
|
||||
@ -759,7 +777,7 @@ func TestSupervisorLogin(t *testing.T) {
|
||||
}, 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,
|
||||
downstreamAuthorizeURL,
|
||||
env.SupervisorUpstreamActiveDirectory.TestUserPrincipalNameValue, // username to present to server during login
|
||||
@ -768,7 +786,7 @@ func TestSupervisorLogin(t *testing.T) {
|
||||
false,
|
||||
)
|
||||
},
|
||||
breakRefreshSessionData: func(t *testing.T, pinnipedSession *psession.PinnipedSession, _ string) {
|
||||
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)
|
||||
@ -781,8 +799,10 @@ func TestSupervisorLogin(t *testing.T) {
|
||||
"&sub="+env.SupervisorUpstreamActiveDirectory.TestUserUniqueIDAttributeValue,
|
||||
) + "$",
|
||||
// the ID token Username should have been pulled from the requested UserSearch.Attributes.Username attribute
|
||||
wantDownstreamIDTokenUsernameToMatch: "^" + regexp.QuoteMeta(env.SupervisorUpstreamActiveDirectory.TestUserPrincipalNameValue) + "$",
|
||||
wantDownstreamIDTokenGroups: env.SupervisorUpstreamActiveDirectory.TestUserIndirectGroupsSAMAccountPlusDomainNames,
|
||||
wantDownstreamIDTokenUsernameToMatch: func(_ string) string {
|
||||
return "^" + regexp.QuoteMeta(env.SupervisorUpstreamActiveDirectory.TestUserPrincipalNameValue) + "$"
|
||||
},
|
||||
wantDownstreamIDTokenGroups: env.SupervisorUpstreamActiveDirectory.TestUserIndirectGroupsSAMAccountPlusDomainNames,
|
||||
},
|
||||
{
|
||||
name: "active directory login still works after deleting and recreating bind secret",
|
||||
@ -853,7 +873,7 @@ func TestSupervisorLogin(t *testing.T) {
|
||||
}, 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,
|
||||
downstreamAuthorizeURL,
|
||||
env.SupervisorUpstreamActiveDirectory.TestUserPrincipalNameValue, // username to present to server during login
|
||||
@ -862,7 +882,7 @@ func TestSupervisorLogin(t *testing.T) {
|
||||
false,
|
||||
)
|
||||
},
|
||||
breakRefreshSessionData: func(t *testing.T, pinnipedSession *psession.PinnipedSession, _ string) {
|
||||
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)
|
||||
@ -875,8 +895,72 @@ func TestSupervisorLogin(t *testing.T) {
|
||||
"&sub="+env.SupervisorUpstreamActiveDirectory.TestUserUniqueIDAttributeValue,
|
||||
) + "$",
|
||||
// the ID token Username should have been pulled from the requested UserSearch.Attributes.Username attribute
|
||||
wantDownstreamIDTokenUsernameToMatch: "^" + regexp.QuoteMeta(env.SupervisorUpstreamActiveDirectory.TestUserPrincipalNameValue) + "$",
|
||||
wantDownstreamIDTokenGroups: env.SupervisorUpstreamActiveDirectory.TestUserIndirectGroupsSAMAccountPlusDomainNames,
|
||||
wantDownstreamIDTokenUsernameToMatch: func(_ string) string {
|
||||
return "^" + regexp.QuoteMeta(env.SupervisorUpstreamActiveDirectory.TestUserPrincipalNameValue) + "$"
|
||||
},
|
||||
wantDownstreamIDTokenGroups: env.SupervisorUpstreamActiveDirectory.TestUserIndirectGroupsSAMAccountPlusDomainNames,
|
||||
},
|
||||
{
|
||||
name: "active directory login fails after the user password is changed",
|
||||
maybeSkip: func(t *testing.T) {
|
||||
t.Helper()
|
||||
if len(env.ToolsNamespace) == 0 && !env.HasCapability(testlib.CanReachInternetLDAPPorts) {
|
||||
t.Skip("LDAP integration test requires connectivity to an LDAP server")
|
||||
}
|
||||
if env.SupervisorUpstreamActiveDirectory.Host == "" {
|
||||
t.Skip("Active Directory hostname not specified")
|
||||
}
|
||||
},
|
||||
createIDP: func(t *testing.T) string {
|
||||
t.Helper()
|
||||
secret := testlib.CreateTestSecret(t, env.SupervisorNamespace, "ad-service-account", v1.SecretTypeBasicAuth,
|
||||
map[string]string{
|
||||
v1.BasicAuthUsernameKey: env.SupervisorUpstreamActiveDirectory.BindUsername,
|
||||
v1.BasicAuthPasswordKey: env.SupervisorUpstreamActiveDirectory.BindPassword,
|
||||
},
|
||||
)
|
||||
adIDP := testlib.CreateTestActiveDirectoryIdentityProvider(t, idpv1alpha1.ActiveDirectoryIdentityProviderSpec{
|
||||
Host: env.SupervisorUpstreamActiveDirectory.Host,
|
||||
TLS: &idpv1alpha1.TLSSpec{
|
||||
CertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte(env.SupervisorUpstreamActiveDirectory.CABundle)),
|
||||
},
|
||||
Bind: idpv1alpha1.ActiveDirectoryIdentityProviderBind{
|
||||
SecretName: secret.Name,
|
||||
},
|
||||
}, idpv1alpha1.ActiveDirectoryPhaseReady)
|
||||
expectedMsg := fmt.Sprintf(
|
||||
`successfully able to connect to "%s" and bind as user "%s" [validated with Secret "%s" at version "%s"]`,
|
||||
env.SupervisorUpstreamActiveDirectory.Host, env.SupervisorUpstreamActiveDirectory.BindUsername,
|
||||
secret.Name, secret.ResourceVersion,
|
||||
)
|
||||
requireSuccessfulActiveDirectoryIdentityProviderConditions(t, adIDP, expectedMsg)
|
||||
return adIDP.Name
|
||||
},
|
||||
createTestUser: func(t *testing.T) (string, string) {
|
||||
return createFreshADTestUser(t, env)
|
||||
},
|
||||
deleteTestUser: func(t *testing.T, username string) {
|
||||
deleteTestADUser(t, env, username)
|
||||
},
|
||||
requestAuthorization: func(t *testing.T, downstreamAuthorizeURL, _, testUserName, testUserPassword string, httpClient *http.Client) {
|
||||
requestAuthorizationUsingCLIPasswordFlow(t,
|
||||
downstreamAuthorizeURL,
|
||||
testUserName, // username to present to server during login
|
||||
testUserPassword, // password to present to server during login
|
||||
httpClient,
|
||||
false,
|
||||
)
|
||||
},
|
||||
breakRefreshSessionData: func(t *testing.T, sessionData *psession.PinnipedSession, _, username string) {
|
||||
changeADTestUserPassword(t, env, username) // this will fail for now
|
||||
},
|
||||
// we can't know the subject ahead of time because we created a new user and don't know their uid,
|
||||
// so skip wantDownstreamIDTokenSubjectToMatch
|
||||
// the ID token Username should have been pulled from the requested UserSearch.Attributes.Username attribute
|
||||
wantDownstreamIDTokenUsernameToMatch: func(username string) string {
|
||||
return "^" + regexp.QuoteMeta(username+"@"+env.SupervisorUpstreamActiveDirectory.Domain) + "$"
|
||||
},
|
||||
wantDownstreamIDTokenGroups: []string{}, // none for now.
|
||||
},
|
||||
{
|
||||
name: "logging in to activedirectory with a deactivated user fails",
|
||||
@ -914,7 +998,7 @@ func TestSupervisorLogin(t *testing.T) {
|
||||
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,
|
||||
downstreamAuthorizeURL,
|
||||
env.SupervisorUpstreamActiveDirectory.TestDeactivatedUserSAMAccountNameValue, // username to present to server during login
|
||||
@ -975,7 +1059,7 @@ func TestSupervisorLogin(t *testing.T) {
|
||||
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,
|
||||
downstreamAuthorizeURL,
|
||||
env.SupervisorUpstreamLDAP.TestUserMailAttributeValue, // username to present to server during login
|
||||
@ -984,7 +1068,7 @@ func TestSupervisorLogin(t *testing.T) {
|
||||
false,
|
||||
)
|
||||
},
|
||||
breakRefreshSessionData: func(t *testing.T, pinnipedSession *psession.PinnipedSession, idpName string) {
|
||||
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)
|
||||
@ -1007,8 +1091,10 @@ func TestSupervisorLogin(t *testing.T) {
|
||||
"&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,
|
||||
wantDownstreamIDTokenUsernameToMatch: func(username string) string {
|
||||
return "^" + regexp.QuoteMeta(env.SupervisorUpstreamLDAP.TestUserMailAttributeValue) + "$"
|
||||
},
|
||||
wantDownstreamIDTokenGroups: env.SupervisorUpstreamLDAP.TestUserDirectGroupsDNs,
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
@ -1020,6 +1106,8 @@ func TestSupervisorLogin(t *testing.T) {
|
||||
tt.createIDP,
|
||||
tt.requestAuthorization,
|
||||
tt.breakRefreshSessionData,
|
||||
tt.createTestUser,
|
||||
tt.deleteTestUser,
|
||||
tt.wantDownstreamIDTokenSubjectToMatch,
|
||||
tt.wantDownstreamIDTokenUsernameToMatch,
|
||||
tt.wantDownstreamIDTokenGroups,
|
||||
@ -1150,10 +1238,15 @@ func requireEventuallySuccessfulActiveDirectoryIdentityProviderConditions(t *tes
|
||||
func testSupervisorLogin(
|
||||
t *testing.T,
|
||||
createIDP func(t *testing.T) string,
|
||||
requestAuthorization func(t *testing.T, downstreamAuthorizeURL, downstreamCallbackURL string, httpClient *http.Client),
|
||||
breakRefreshSessionData func(t *testing.T, pinnipedSession *psession.PinnipedSession, idpName string),
|
||||
wantDownstreamIDTokenSubjectToMatch, wantDownstreamIDTokenUsernameToMatch string, wantDownstreamIDTokenGroups []string,
|
||||
wantErrorDescription string, wantErrorType string,
|
||||
requestAuthorization func(t *testing.T, downstreamAuthorizeURL, downstreamCallbackURL, username, password string, httpClient *http.Client),
|
||||
breakRefreshSessionData func(t *testing.T, pinnipedSession *psession.PinnipedSession, idpName, username string),
|
||||
createTestUser func(t *testing.T) (string, string),
|
||||
deleteTestUser func(t *testing.T, username string),
|
||||
wantDownstreamIDTokenSubjectToMatch string,
|
||||
wantDownstreamIDTokenUsernameToMatch func(username string) string,
|
||||
wantDownstreamIDTokenGroups []string,
|
||||
wantErrorDescription string,
|
||||
wantErrorType string,
|
||||
) {
|
||||
env := testlib.IntegrationEnv(t)
|
||||
|
||||
@ -1241,6 +1334,12 @@ func testSupervisorLogin(
|
||||
// Create upstream IDP and wait for it to become ready.
|
||||
idpName := createIDP(t)
|
||||
|
||||
username, password := "", ""
|
||||
if createTestUser != nil {
|
||||
username, password = createTestUser(t)
|
||||
defer deleteTestUser(t, username)
|
||||
}
|
||||
|
||||
// Perform OIDC discovery for our downstream.
|
||||
var discovery *coreosoidc.Provider
|
||||
testlib.RequireEventually(t, func(requireEventually *require.Assertions) {
|
||||
@ -1276,7 +1375,7 @@ func testSupervisorLogin(
|
||||
)
|
||||
|
||||
// Perform parameterized auth code acquisition.
|
||||
requestAuthorization(t, downstreamAuthorizeURL, localCallbackServer.URL, httpClient)
|
||||
requestAuthorization(t, downstreamAuthorizeURL, localCallbackServer.URL, username, password, httpClient)
|
||||
|
||||
// Expect that our callback handler was invoked.
|
||||
callback := localCallbackServer.waitForCallback(10 * time.Second)
|
||||
@ -1294,7 +1393,7 @@ func testSupervisorLogin(
|
||||
expectedIDTokenClaims := []string{"iss", "exp", "sub", "aud", "auth_time", "iat", "jti", "nonce", "rat", "username", "groups"}
|
||||
verifyTokenResponse(t,
|
||||
tokenResponse, discovery, downstreamOAuth2Config, nonceParam,
|
||||
expectedIDTokenClaims, wantDownstreamIDTokenSubjectToMatch, wantDownstreamIDTokenUsernameToMatch, wantDownstreamIDTokenGroups)
|
||||
expectedIDTokenClaims, wantDownstreamIDTokenSubjectToMatch, wantDownstreamIDTokenUsernameToMatch(username), wantDownstreamIDTokenGroups)
|
||||
|
||||
// token exchange on the original token
|
||||
doTokenExchange(t, &downstreamOAuth2Config, tokenResponse, httpClient, discovery)
|
||||
@ -1308,7 +1407,7 @@ func testSupervisorLogin(
|
||||
expectRefreshedIDTokenClaims := []string{"iss", "exp", "sub", "aud", "auth_time", "iat", "jti", "rat", "username", "groups", "at_hash"}
|
||||
verifyTokenResponse(t,
|
||||
refreshedTokenResponse, discovery, downstreamOAuth2Config, "",
|
||||
expectRefreshedIDTokenClaims, wantDownstreamIDTokenSubjectToMatch, wantDownstreamIDTokenUsernameToMatch, wantDownstreamIDTokenGroups)
|
||||
expectRefreshedIDTokenClaims, wantDownstreamIDTokenSubjectToMatch, wantDownstreamIDTokenUsernameToMatch(username), wantDownstreamIDTokenGroups)
|
||||
|
||||
require.NotEqual(t, tokenResponse.AccessToken, refreshedTokenResponse.AccessToken)
|
||||
require.NotEqual(t, tokenResponse.RefreshToken, refreshedTokenResponse.RefreshToken)
|
||||
@ -1333,7 +1432,7 @@ func testSupervisorLogin(
|
||||
// Next mutate the part of the session that is used during upstream refresh.
|
||||
pinnipedSession, ok := storedRefreshSession.GetSession().(*psession.PinnipedSession)
|
||||
require.True(t, ok, "should have been able to cast session data to PinnipedSession")
|
||||
breakRefreshSessionData(t, pinnipedSession, idpName)
|
||||
breakRefreshSessionData(t, pinnipedSession, idpName, username)
|
||||
|
||||
// Then save the mutated Secret back to Kubernetes.
|
||||
// There is no update function, so delete and create again at the same name.
|
||||
@ -1423,7 +1522,7 @@ func verifyTokenResponse(
|
||||
require.NotEmpty(t, tokenResponse.RefreshToken)
|
||||
}
|
||||
|
||||
func requestAuthorizationUsingBrowserAuthcodeFlow(t *testing.T, downstreamAuthorizeURL, downstreamCallbackURL string, httpClient *http.Client) {
|
||||
func requestAuthorizationUsingBrowserAuthcodeFlow(t *testing.T, downstreamAuthorizeURL, downstreamCallbackURL, _, _ string, httpClient *http.Client) {
|
||||
t.Helper()
|
||||
env := testlib.IntegrationEnv(t)
|
||||
|
||||
@ -1595,3 +1694,113 @@ func expectSecurityHeaders(t *testing.T, response *http.Response, expectFositeTo
|
||||
assert.Equal(t, "no-cache", h.Get("Pragma"))
|
||||
assert.Equal(t, "0", h.Get("Expires"))
|
||||
}
|
||||
|
||||
// create a fresh test user in AD to use for this test.
|
||||
func createFreshADTestUser(t *testing.T, env *testlib.TestEnv) (string, string) {
|
||||
t.Helper()
|
||||
// dial tls
|
||||
conn := dialTLS(t, env)
|
||||
// bind
|
||||
err := conn.Bind(env.SupervisorUpstreamActiveDirectory.BindUsername, env.SupervisorUpstreamActiveDirectory.BindPassword)
|
||||
require.NoError(t, err)
|
||||
|
||||
testUserName := "user-" + createRandomHexString(t, 7) // sAMAccountNames are limited to 20 characters, so this is as long as we can make it.
|
||||
// create
|
||||
userDN := fmt.Sprintf("CN=%s,OU=test-users,%s", testUserName, env.SupervisorUpstreamActiveDirectory.UserSearchBase)
|
||||
a := ldap.NewAddRequest(userDN, []ldap.Control{})
|
||||
a.Attribute("objectClass", []string{"top", "person", "organizationalPerson", "user"})
|
||||
a.Attribute("userPrincipalName", []string{fmt.Sprintf("%s@%s", testUserName, env.SupervisorUpstreamActiveDirectory.Domain)})
|
||||
a.Attribute("sAMAccountName", []string{testUserName})
|
||||
|
||||
err = conn.Add(a)
|
||||
require.NoError(t, err)
|
||||
|
||||
// modify password and enable account
|
||||
testUserPassword := createRandomASCIIString(t, 20)
|
||||
enc := unicode.UTF16(unicode.LittleEndian, unicode.IgnoreBOM).NewEncoder()
|
||||
encodedTestUserPassword, err := enc.String("\"" + testUserPassword + "\"")
|
||||
require.NoError(t, err)
|
||||
|
||||
m := ldap.NewModifyRequest(userDN, []ldap.Control{})
|
||||
m.Replace("unicodePwd", []string{encodedTestUserPassword})
|
||||
m.Replace("userAccountControl", []string{"512"})
|
||||
err = conn.Modify(m)
|
||||
require.NoError(t, err)
|
||||
return testUserName, testUserPassword
|
||||
}
|
||||
|
||||
// change the user's password to a new one.
|
||||
func changeADTestUserPassword(t *testing.T, env *testlib.TestEnv, testUserName string) {
|
||||
conn := dialTLS(t, env)
|
||||
// bind
|
||||
err := conn.Bind(env.SupervisorUpstreamActiveDirectory.BindUsername, env.SupervisorUpstreamActiveDirectory.BindPassword)
|
||||
require.NoError(t, err)
|
||||
|
||||
newTestUserPassword := createRandomASCIIString(t, 20)
|
||||
enc := unicode.UTF16(unicode.LittleEndian, unicode.IgnoreBOM).NewEncoder()
|
||||
encodedTestUserPassword, err := enc.String("\"" + newTestUserPassword + "\"")
|
||||
require.NoError(t, err)
|
||||
|
||||
userDN := fmt.Sprintf("CN=%s,OU=test-users,%s", testUserName, env.SupervisorUpstreamActiveDirectory.UserSearchBase)
|
||||
m := ldap.NewModifyRequest(userDN, []ldap.Control{})
|
||||
m.Replace("unicodePwd", []string{encodedTestUserPassword})
|
||||
err = conn.Modify(m)
|
||||
require.NoError(t, err)
|
||||
// don't bother to return the new password... we won't be using it, just checking that it's changed.
|
||||
}
|
||||
|
||||
// delete the test user created for this test.
|
||||
func deleteTestADUser(t *testing.T, env *testlib.TestEnv, testUserName string) {
|
||||
t.Helper()
|
||||
|
||||
conn := dialTLS(t, env)
|
||||
// bind
|
||||
err := conn.Bind(env.SupervisorUpstreamActiveDirectory.BindUsername, env.SupervisorUpstreamActiveDirectory.BindPassword)
|
||||
require.NoError(t, err)
|
||||
|
||||
userDN := fmt.Sprintf("CN=%s,OU=test-users,%s", testUserName, env.SupervisorUpstreamActiveDirectory.UserSearchBase)
|
||||
d := ldap.NewDelRequest(userDN, []ldap.Control{})
|
||||
err = conn.Del(d)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func dialTLS(t *testing.T, env *testlib.TestEnv) *ldap.Conn {
|
||||
t.Helper()
|
||||
// dial tls
|
||||
rootCAs := x509.NewCertPool()
|
||||
success := rootCAs.AppendCertsFromPEM([]byte(env.SupervisorUpstreamActiveDirectory.CABundle))
|
||||
require.True(t, success)
|
||||
tlsConfig := &tls.Config{MinVersion: tls.VersionTLS12, RootCAs: rootCAs}
|
||||
dialer := &tls.Dialer{NetDialer: &net.Dialer{Timeout: time.Minute}, Config: tlsConfig}
|
||||
c, err := dialer.DialContext(context.Background(), "tcp", env.SupervisorUpstreamActiveDirectory.Host)
|
||||
require.NoError(t, err)
|
||||
conn := ldap.NewConn(c, true)
|
||||
conn.Start()
|
||||
return conn
|
||||
}
|
||||
|
||||
func createRandomHexString(t *testing.T, length int) string {
|
||||
t.Helper()
|
||||
bytes := make([]byte, length)
|
||||
_, err := rand.Read(bytes)
|
||||
require.NoError(t, err)
|
||||
randomString := hex.EncodeToString(bytes)
|
||||
return randomString
|
||||
}
|
||||
|
||||
func createRandomASCIIString(t *testing.T, length int) string {
|
||||
result := ""
|
||||
for {
|
||||
if len(result) >= length {
|
||||
return result
|
||||
}
|
||||
num, err := rand.Int(rand.Reader, big.NewInt(int64(127)))
|
||||
require.NoError(t, err)
|
||||
n := num.Int64()
|
||||
// Make sure that the number/byte/letter is inside
|
||||
// the range of printable ASCII characters (excluding space and DEL)
|
||||
if n > 32 && n < 127 {
|
||||
result += string(rune(n))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -84,6 +84,7 @@ type TestOIDCUpstream struct {
|
||||
|
||||
type TestLDAPUpstream struct {
|
||||
Host string `json:"host"`
|
||||
Domain string `json:"domain"`
|
||||
StartTLSOnlyHost string `json:"startTLSOnlyHost"`
|
||||
CABundle string `json:"caBundle"`
|
||||
BindUsername string `json:"bindUsername"`
|
||||
@ -279,6 +280,7 @@ func loadEnvVars(t *testing.T, result *TestEnv) {
|
||||
|
||||
result.SupervisorUpstreamActiveDirectory = TestLDAPUpstream{
|
||||
Host: wantEnv("PINNIPED_TEST_AD_HOST", ""),
|
||||
Domain: wantEnv("PINNIPED_TEST_AD_DOMAIN", ""),
|
||||
CABundle: base64Decoded(t, os.Getenv("PINNIPED_TEST_AD_LDAPS_CA_BUNDLE")),
|
||||
BindUsername: wantEnv("PINNIPED_TEST_AD_BIND_ACCOUNT_USERNAME", ""),
|
||||
BindPassword: wantEnv("PINNIPED_TEST_AD_BIND_ACCOUNT_PASSWORD", ""),
|
||||
|
Loading…
Reference in New Issue
Block a user