LDAP upstream watcher controller tries using both TLS and StartTLS

- Automatically try to fall back to using StartTLS when using TLS
  doesn't work. Only complain when both don't work.
- Remember (in-memory) which one worked and keeping using that one
  in the future (unless the pod restarts).
This commit is contained in:
Ryan Richard 2021-05-20 12:46:33 -07:00
parent 025b37f839
commit 7e76b66639
4 changed files with 263 additions and 79 deletions

View File

@ -58,13 +58,18 @@ type ldapWatcherController struct {
} }
// An in-memory cache with an entry for each LDAPIdentityProvider, to keep track of which ResourceVersion // An in-memory cache with an entry for each LDAPIdentityProvider, to keep track of which ResourceVersion
// of the bind Secret was used during the most recent successful validation. // of the bind Secret and which TLS/StartTLS setting was used during the most recent successful validation.
type secretVersionCache struct { type secretVersionCache struct {
ResourceVersionsByName map[string]string ValidatedSettingsByName map[string]validatedSettings
}
type validatedSettings struct {
BindSecretResourceVersion string
LDAPConnectionProtocol upstreamldap.LDAPConnectionProtocol
} }
func newSecretVersionCache() *secretVersionCache { func newSecretVersionCache() *secretVersionCache {
return &secretVersionCache{ResourceVersionsByName: map[string]string{}} return &secretVersionCache{ValidatedSettingsByName: map[string]validatedSettings{}}
} }
// New instantiates a new controllerlib.Controller which will populate the provided UpstreamLDAPIdentityProviderICache. // New instantiates a new controllerlib.Controller which will populate the provided UpstreamLDAPIdentityProviderICache.
@ -154,7 +159,6 @@ func (c *ldapWatcherController) validateUpstream(ctx context.Context, upstream *
config := &upstreamldap.ProviderConfig{ config := &upstreamldap.ProviderConfig{
Name: upstream.Name, Name: upstream.Name,
Host: spec.Host, Host: spec.Host,
ConnectionProtocol: upstreamldap.TLS,
UserSearch: upstreamldap.UserSearchConfig{ UserSearch: upstreamldap.UserSearchConfig{
Base: spec.UserSearch.Base, Base: spec.UserSearch.Base,
Filter: spec.UserSearch.Filter, Filter: spec.UserSearch.Filter,
@ -229,22 +233,23 @@ func (c *ldapWatcherController) validateTLSConfig(upstream *v1alpha1.LDAPIdentit
} }
func (c *ldapWatcherController) validateFinishedConfig(ctx context.Context, upstream *v1alpha1.LDAPIdentityProvider, config *upstreamldap.ProviderConfig, currentSecretVersion string) *v1alpha1.Condition { func (c *ldapWatcherController) validateFinishedConfig(ctx context.Context, upstream *v1alpha1.LDAPIdentityProvider, config *upstreamldap.ProviderConfig, currentSecretVersion string) *v1alpha1.Condition {
ldapProvider := upstreamldap.New(*config) if c.hasPreviousSuccessfulConditionForCurrentSpecGenerationAndSecretVersion(upstream, currentSecretVersion, config) {
if c.hasPreviousSuccessfulConditionForCurrentSpecGenerationAndSecretVersion(upstream, currentSecretVersion) {
return nil return nil
} }
testConnectionTimeout, cancelFunc := context.WithTimeout(ctx, testLDAPConnectionTimeout) testConnectionTimeout, cancelFunc := context.WithTimeout(ctx, testLDAPConnectionTimeout)
defer cancelFunc() defer cancelFunc()
condition := c.testConnection(testConnectionTimeout, upstream, config, ldapProvider, currentSecretVersion) condition := c.testConnection(testConnectionTimeout, upstream, config, currentSecretVersion)
if condition.Status == v1alpha1.ConditionTrue { if condition.Status == v1alpha1.ConditionTrue {
// Remember (in-memory for this pod) that the controller has successfully validated the LDAP provider // Remember (in-memory for this pod) that the controller has successfully validated the LDAP provider
// using this version of the Secret. This is for performance reasons, to avoid attempting to connect to // using this version of the Secret. This is for performance reasons, to avoid attempting to connect to
// the LDAP server more than is needed. If the pod restarts, it will attempt this validation again. // the LDAP server more than is needed. If the pod restarts, it will attempt this validation again.
c.validatedSecretVersionsCache.ResourceVersionsByName[upstream.GetName()] = currentSecretVersion c.validatedSecretVersionsCache.ValidatedSettingsByName[upstream.GetName()] = validatedSettings{
BindSecretResourceVersion: currentSecretVersion,
LDAPConnectionProtocol: config.ConnectionProtocol,
}
} }
return condition return condition
@ -254,10 +259,28 @@ func (c *ldapWatcherController) testConnection(
ctx context.Context, ctx context.Context,
upstream *v1alpha1.LDAPIdentityProvider, upstream *v1alpha1.LDAPIdentityProvider,
config *upstreamldap.ProviderConfig, config *upstreamldap.ProviderConfig,
ldapProvider *upstreamldap.Provider,
currentSecretVersion string, currentSecretVersion string,
) *v1alpha1.Condition { ) *v1alpha1.Condition {
err := ldapProvider.TestConnection(ctx) // First try using TLS.
config.ConnectionProtocol = upstreamldap.TLS
tlsLDAPProvider := upstreamldap.New(*config)
err := tlsLDAPProvider.TestConnection(ctx)
if err != nil {
// If there was any error, try again with StartTLS instead.
config.ConnectionProtocol = upstreamldap.StartTLS
startTLSLDAPProvider := upstreamldap.New(*config)
startTLSErr := startTLSLDAPProvider.TestConnection(ctx)
if startTLSErr == nil {
// Successfully able to fall back to using StartTLS, so clear the original
// error and consider the connection test to be successful.
err = nil
} else {
// Falling back to StartTLS also failed, so put TLS back into the config
// and consider the connection test to be failed.
config.ConnectionProtocol = upstreamldap.TLS
}
}
if err != nil { if err != nil {
return &v1alpha1.Condition{ return &v1alpha1.Condition{
Type: typeLDAPConnectionValid, Type: typeLDAPConnectionValid,
@ -277,14 +300,16 @@ func (c *ldapWatcherController) testConnection(
} }
} }
func (c *ldapWatcherController) hasPreviousSuccessfulConditionForCurrentSpecGenerationAndSecretVersion(upstream *v1alpha1.LDAPIdentityProvider, currentSecretVersion string) bool { func (c *ldapWatcherController) hasPreviousSuccessfulConditionForCurrentSpecGenerationAndSecretVersion(upstream *v1alpha1.LDAPIdentityProvider, currentSecretVersion string, config *upstreamldap.ProviderConfig) bool {
currentGeneration := upstream.Generation currentGeneration := upstream.Generation
for _, cond := range upstream.Status.Conditions { for _, cond := range upstream.Status.Conditions {
if cond.Type == typeLDAPConnectionValid && cond.Status == v1alpha1.ConditionTrue && cond.ObservedGeneration == currentGeneration { if cond.Type == typeLDAPConnectionValid && cond.Status == v1alpha1.ConditionTrue && cond.ObservedGeneration == currentGeneration {
// Found a previously successful condition for the current spec generation. // Found a previously successful condition for the current spec generation.
// Now figure out which version of the bind Secret was used during that previous validation, if any. // Now figure out which version of the bind Secret was used during that previous validation, if any.
validatedSecretVersion := c.validatedSecretVersionsCache.ResourceVersionsByName[upstream.GetName()] validatedSecretVersion := c.validatedSecretVersionsCache.ValidatedSettingsByName[upstream.GetName()]
if validatedSecretVersion == currentSecretVersion { if validatedSecretVersion.BindSecretResourceVersion == currentSecretVersion {
// Reload the TLS vs StartTLS setting that was previously validated.
config.ConnectionProtocol = validatedSecretVersion.LDAPConnectionProtocol
return true return true
} }
} }

View File

@ -12,6 +12,7 @@ import (
"testing" "testing"
"time" "time"
"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"
corev1 "k8s.io/api/core/v1" corev1 "k8s.io/api/core/v1"
@ -196,7 +197,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) {
return deepCopy return deepCopy
} }
providerConfigForValidUpstream := &upstreamldap.ProviderConfig{ providerConfigForValidUpstreamWithTLS := &upstreamldap.ProviderConfig{
Name: testName, Name: testName,
Host: testHost, Host: testHost,
ConnectionProtocol: upstreamldap.TLS, ConnectionProtocol: upstreamldap.TLS,
@ -216,6 +217,11 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) {
}, },
} }
// Make a copy with targeted changes.
copyOfProviderConfigForValidUpstreamWithTLS := *providerConfigForValidUpstreamWithTLS
providerConfigForValidUpstreamWithStartTLS := &copyOfProviderConfigForValidUpstreamWithTLS
providerConfigForValidUpstreamWithStartTLS.ConnectionProtocol = upstreamldap.StartTLS
bindSecretValidTrueCondition := func(gen int64) v1alpha1.Condition { bindSecretValidTrueCondition := func(gen int64) v1alpha1.Condition {
return v1alpha1.Condition{ return v1alpha1.Condition{
Type: "BindSecretValid", Type: "BindSecretValid",
@ -266,15 +272,15 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
initialValidatedSecretVersions map[string]string initialValidatedSettings map[string]validatedSettings
inputUpstreams []runtime.Object inputUpstreams []runtime.Object
inputSecrets []runtime.Object inputSecrets []runtime.Object
setupMocks func(conn *mockldapconn.MockConn) setupMocks func(conn *mockldapconn.MockConn)
dialError error dialErrors map[string]error
wantErr string wantErr string
wantResultingCache []*upstreamldap.ProviderConfig wantResultingCache []*upstreamldap.ProviderConfig
wantResultingUpstreams []v1alpha1.LDAPIdentityProvider wantResultingUpstreams []v1alpha1.LDAPIdentityProvider
wantValidatedSecretVersions map[string]string wantValidatedSettings map[string]validatedSettings
}{ }{
{ {
name: "no LDAPIdentityProvider upstreams clears the cache", name: "no LDAPIdentityProvider upstreams clears the cache",
@ -289,7 +295,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) {
conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1)
conn.EXPECT().Close().Times(1) conn.EXPECT().Close().Times(1)
}, },
wantResultingCache: []*upstreamldap.ProviderConfig{providerConfigForValidUpstream}, wantResultingCache: []*upstreamldap.ProviderConfig{providerConfigForValidUpstreamWithTLS},
wantResultingUpstreams: []v1alpha1.LDAPIdentityProvider{{ wantResultingUpstreams: []v1alpha1.LDAPIdentityProvider{{
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234},
Status: v1alpha1.LDAPIdentityProviderStatus{ Status: v1alpha1.LDAPIdentityProviderStatus{
@ -297,7 +303,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) {
Conditions: allConditionsTrue(1234, "4242"), Conditions: allConditionsTrue(1234, "4242"),
}, },
}}, }},
wantValidatedSecretVersions: map[string]string{testName: "4242"}, wantValidatedSettings: map[string]validatedSettings{testName: {BindSecretResourceVersion: "4242", LDAPConnectionProtocol: upstreamldap.TLS}},
}, },
{ {
name: "missing secret", name: "missing secret",
@ -480,7 +486,121 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) {
}, },
}, },
}}, }},
wantValidatedSecretVersions: map[string]string{testName: "4242"}, wantValidatedSettings: map[string]validatedSettings{testName: {BindSecretResourceVersion: "4242", LDAPConnectionProtocol: upstreamldap.TLS}},
},
{
name: "when TLS connection fails it tries to use StartTLS instead: without a specified port it automatically switches ports",
inputUpstreams: []runtime.Object{editedValidUpstream(func(upstream *v1alpha1.LDAPIdentityProvider) {
upstream.Spec.Host = "ldap.example.com" // when the port is not specified, automatically switch ports for StartTLS
})},
inputSecrets: []runtime.Object{validBindUserSecret("4242")},
setupMocks: func(conn *mockldapconn.MockConn) {
// Should perform a test dial and bind.
conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1)
conn.EXPECT().Close().Times(1)
},
dialErrors: map[string]error{
"ldap.example.com:" + ldap.DefaultLdapsPort: fmt.Errorf("some ldaps dial error"),
"ldap.example.com:" + ldap.DefaultLdapPort: nil, // no error on the regular ldap:// port
},
wantResultingCache: []*upstreamldap.ProviderConfig{
{
Name: testName,
Host: "ldap.example.com",
ConnectionProtocol: upstreamldap.StartTLS, // successfully fell back to using StartTLS
CABundle: testCABundle,
BindUsername: testBindUsername,
BindPassword: testBindPassword,
UserSearch: upstreamldap.UserSearchConfig{
Base: testUserSearchBase,
Filter: testUserSearchFilter,
UsernameAttribute: testUsernameAttrName,
UIDAttribute: testUIDAttrName,
},
GroupSearch: upstreamldap.GroupSearchConfig{
Base: testGroupSearchBase,
Filter: testGroupSearchFilter,
GroupNameAttribute: testGroupNameAttrName,
},
},
},
wantResultingUpstreams: []v1alpha1.LDAPIdentityProvider{{
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234},
Status: v1alpha1.LDAPIdentityProviderStatus{
Phase: "Ready",
Conditions: []v1alpha1.Condition{
bindSecretValidTrueCondition(1234),
{
Type: "LDAPConnectionValid",
Status: "True",
LastTransitionTime: now,
Reason: "Success",
Message: fmt.Sprintf(
`successfully able to connect to "%s" and bind as user "%s" [validated with Secret "%s" at version "%s"]`,
"ldap.example.com", testBindUsername, testSecretName, "4242"),
ObservedGeneration: 1234,
},
tlsConfigurationValidLoadedTrueCondition(1234),
},
},
}},
wantValidatedSettings: map[string]validatedSettings{testName: {BindSecretResourceVersion: "4242", LDAPConnectionProtocol: upstreamldap.StartTLS}},
},
{
name: "when TLS connection fails it tries to use StartTLS instead: with a specified port it does not automatically switch ports",
inputUpstreams: []runtime.Object{editedValidUpstream(func(upstream *v1alpha1.LDAPIdentityProvider) {
upstream.Spec.Host = "ldap.example.com:5678" // when the port is specified, do not automatically switch ports for StartTLS
})},
inputSecrets: []runtime.Object{validBindUserSecret("4242")},
setupMocks: func(conn *mockldapconn.MockConn) {
// Both dials fail, so there should be no bind.
},
dialErrors: map[string]error{
"ldap.example.com:5678": fmt.Errorf("some dial error"), // both TLS and StartTLS should try the same port and both fail
},
wantResultingCache: []*upstreamldap.ProviderConfig{
// even though the connection test failed, still loads into the cache because it is treated like a warning
{
Name: testName,
Host: "ldap.example.com:5678",
ConnectionProtocol: upstreamldap.TLS, // need to pick TLS or StartTLS to load into the cache when both fail, so choose TLS
CABundle: testCABundle,
BindUsername: testBindUsername,
BindPassword: testBindPassword,
UserSearch: upstreamldap.UserSearchConfig{
Base: testUserSearchBase,
Filter: testUserSearchFilter,
UsernameAttribute: testUsernameAttrName,
UIDAttribute: testUIDAttrName,
},
GroupSearch: upstreamldap.GroupSearchConfig{
Base: testGroupSearchBase,
Filter: testGroupSearchFilter,
GroupNameAttribute: testGroupNameAttrName,
},
},
},
wantErr: controllerlib.ErrSyntheticRequeue.Error(),
wantResultingUpstreams: []v1alpha1.LDAPIdentityProvider{{
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234},
Status: v1alpha1.LDAPIdentityProviderStatus{
Phase: "Error",
Conditions: []v1alpha1.Condition{
bindSecretValidTrueCondition(1234),
{
Type: "LDAPConnectionValid",
Status: "False",
LastTransitionTime: now,
Reason: "LDAPConnectionError",
Message: fmt.Sprintf(
`could not successfully connect to "%s" and bind as user "%s": error dialing host "%s": some dial error`,
"ldap.example.com:5678", testBindUsername, "ldap.example.com:5678"),
ObservedGeneration: 1234,
},
tlsConfigurationValidLoadedTrueCondition(1234),
},
},
}},
}, },
{ {
name: "non-nil TLS configuration with empty CertificateAuthorityData is valid", name: "non-nil TLS configuration with empty CertificateAuthorityData is valid",
@ -521,7 +641,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) {
Conditions: allConditionsTrue(1234, "4242"), Conditions: allConditionsTrue(1234, "4242"),
}, },
}}, }},
wantValidatedSecretVersions: map[string]string{testName: "4242"}, wantValidatedSettings: map[string]validatedSettings{testName: {BindSecretResourceVersion: "4242", LDAPConnectionProtocol: upstreamldap.TLS}},
}, },
{ {
name: "one valid upstream and one invalid upstream updates the cache to include only the valid upstream", name: "one valid upstream and one invalid upstream updates the cache to include only the valid upstream",
@ -537,7 +657,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) {
conn.EXPECT().Close().Times(1) conn.EXPECT().Close().Times(1)
}, },
wantErr: controllerlib.ErrSyntheticRequeue.Error(), wantErr: controllerlib.ErrSyntheticRequeue.Error(),
wantResultingCache: []*upstreamldap.ProviderConfig{providerConfigForValidUpstream}, wantResultingCache: []*upstreamldap.ProviderConfig{providerConfigForValidUpstreamWithTLS},
wantResultingUpstreams: []v1alpha1.LDAPIdentityProvider{ wantResultingUpstreams: []v1alpha1.LDAPIdentityProvider{
{ {
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: "other-upstream", Generation: 42}, ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: "other-upstream", Generation: 42},
@ -564,7 +684,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) {
}, },
}, },
}, },
wantValidatedSecretVersions: map[string]string{testName: "4242"}, wantValidatedSettings: map[string]validatedSettings{testName: {BindSecretResourceVersion: "4242", LDAPConnectionProtocol: upstreamldap.TLS}},
}, },
{ {
name: "when testing the connection to the LDAP server fails then the upstream is still added to the cache anyway (treated like a warning)", name: "when testing the connection to the LDAP server fails then the upstream is still added to the cache anyway (treated like a warning)",
@ -572,11 +692,12 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) {
inputSecrets: []runtime.Object{validBindUserSecret("")}, inputSecrets: []runtime.Object{validBindUserSecret("")},
setupMocks: func(conn *mockldapconn.MockConn) { setupMocks: func(conn *mockldapconn.MockConn) {
// Should perform a test dial and bind. // Should perform a test dial and bind.
conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1).Return(errors.New("some bind error")) // Expect two calls to each of these: once for trying TLS and once for trying StartTLS.
conn.EXPECT().Close().Times(1) conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(2).Return(errors.New("some bind error"))
conn.EXPECT().Close().Times(2)
}, },
wantErr: controllerlib.ErrSyntheticRequeue.Error(), wantErr: controllerlib.ErrSyntheticRequeue.Error(),
wantResultingCache: []*upstreamldap.ProviderConfig{providerConfigForValidUpstream}, wantResultingCache: []*upstreamldap.ProviderConfig{providerConfigForValidUpstreamWithTLS},
wantResultingUpstreams: []v1alpha1.LDAPIdentityProvider{{ wantResultingUpstreams: []v1alpha1.LDAPIdentityProvider{{
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234},
Status: v1alpha1.LDAPIdentityProviderStatus{ Status: v1alpha1.LDAPIdentityProviderStatus{
@ -599,7 +720,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) {
}}, }},
}, },
{ {
name: "when the LDAP server connection was already validated for the current resource generation and secret version, then do not validate it again", name: "when the LDAP server connection was already validated using TLS for the current resource generation and secret version, then do not validate it again and keep using TLS",
inputUpstreams: []runtime.Object{editedValidUpstream(func(upstream *v1alpha1.LDAPIdentityProvider) { inputUpstreams: []runtime.Object{editedValidUpstream(func(upstream *v1alpha1.LDAPIdentityProvider) {
upstream.Generation = 1234 upstream.Generation = 1234
upstream.Status.Conditions = []v1alpha1.Condition{ upstream.Status.Conditions = []v1alpha1.Condition{
@ -607,11 +728,11 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) {
} }
})}, })},
inputSecrets: []runtime.Object{validBindUserSecret("4242")}, inputSecrets: []runtime.Object{validBindUserSecret("4242")},
initialValidatedSecretVersions: map[string]string{testName: "4242"}, initialValidatedSettings: map[string]validatedSettings{testName: {BindSecretResourceVersion: "4242", LDAPConnectionProtocol: upstreamldap.TLS}},
setupMocks: func(conn *mockldapconn.MockConn) { setupMocks: func(conn *mockldapconn.MockConn) {
// Should not perform a test dial and bind. No mocking here means the test will fail if Bind() or Close() are called. // Should not perform a test dial and bind. No mocking here means the test will fail if Bind() or Close() are called.
}, },
wantResultingCache: []*upstreamldap.ProviderConfig{providerConfigForValidUpstream}, wantResultingCache: []*upstreamldap.ProviderConfig{providerConfigForValidUpstreamWithTLS},
wantResultingUpstreams: []v1alpha1.LDAPIdentityProvider{{ wantResultingUpstreams: []v1alpha1.LDAPIdentityProvider{{
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234},
Status: v1alpha1.LDAPIdentityProviderStatus{ Status: v1alpha1.LDAPIdentityProviderStatus{
@ -619,7 +740,30 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) {
Conditions: allConditionsTrue(1234, "4242"), Conditions: allConditionsTrue(1234, "4242"),
}, },
}}, }},
wantValidatedSecretVersions: map[string]string{testName: "4242"}, wantValidatedSettings: map[string]validatedSettings{testName: {BindSecretResourceVersion: "4242", LDAPConnectionProtocol: upstreamldap.TLS}},
},
{
name: "when the LDAP server connection was already validated using StartTLS for the current resource generation and secret version, then do not validate it again and keep using StartTLS",
inputUpstreams: []runtime.Object{editedValidUpstream(func(upstream *v1alpha1.LDAPIdentityProvider) {
upstream.Generation = 1234
upstream.Status.Conditions = []v1alpha1.Condition{
ldapConnectionValidTrueCondition(1234, "4242"),
}
})},
inputSecrets: []runtime.Object{validBindUserSecret("4242")},
initialValidatedSettings: map[string]validatedSettings{testName: {BindSecretResourceVersion: "4242", LDAPConnectionProtocol: upstreamldap.StartTLS}},
setupMocks: func(conn *mockldapconn.MockConn) {
// Should not perform a test dial and bind. No mocking here means the test will fail if Bind() or Close() are called.
},
wantResultingCache: []*upstreamldap.ProviderConfig{providerConfigForValidUpstreamWithStartTLS},
wantResultingUpstreams: []v1alpha1.LDAPIdentityProvider{{
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234},
Status: v1alpha1.LDAPIdentityProviderStatus{
Phase: "Ready",
Conditions: allConditionsTrue(1234, "4242"),
},
}},
wantValidatedSettings: map[string]validatedSettings{testName: {BindSecretResourceVersion: "4242", LDAPConnectionProtocol: upstreamldap.StartTLS}},
}, },
{ {
name: "when the LDAP server connection was validated for an older resource generation, then try to validate it again", name: "when the LDAP server connection was validated for an older resource generation, then try to validate it again",
@ -630,13 +774,13 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) {
} }
})}, })},
inputSecrets: []runtime.Object{validBindUserSecret("4242")}, inputSecrets: []runtime.Object{validBindUserSecret("4242")},
initialValidatedSecretVersions: map[string]string{testName: "4242"}, initialValidatedSettings: map[string]validatedSettings{testName: {BindSecretResourceVersion: "4242", LDAPConnectionProtocol: upstreamldap.TLS}},
setupMocks: func(conn *mockldapconn.MockConn) { setupMocks: func(conn *mockldapconn.MockConn) {
// Should perform a test dial and bind. // Should perform a test dial and bind.
conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1)
conn.EXPECT().Close().Times(1) conn.EXPECT().Close().Times(1)
}, },
wantResultingCache: []*upstreamldap.ProviderConfig{providerConfigForValidUpstream}, wantResultingCache: []*upstreamldap.ProviderConfig{providerConfigForValidUpstreamWithTLS},
wantResultingUpstreams: []v1alpha1.LDAPIdentityProvider{{ wantResultingUpstreams: []v1alpha1.LDAPIdentityProvider{{
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234},
Status: v1alpha1.LDAPIdentityProviderStatus{ Status: v1alpha1.LDAPIdentityProviderStatus{
@ -644,7 +788,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) {
Conditions: allConditionsTrue(1234, "4242"), Conditions: allConditionsTrue(1234, "4242"),
}, },
}}, }},
wantValidatedSecretVersions: map[string]string{testName: "4242"}, wantValidatedSettings: map[string]validatedSettings{testName: {BindSecretResourceVersion: "4242", LDAPConnectionProtocol: upstreamldap.TLS}},
}, },
{ {
name: "when the LDAP server connection validation previously failed for this resource generation, then try to validate it again", name: "when the LDAP server connection validation previously failed for this resource generation, then try to validate it again",
@ -662,13 +806,13 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) {
} }
})}, })},
inputSecrets: []runtime.Object{validBindUserSecret("4242")}, inputSecrets: []runtime.Object{validBindUserSecret("4242")},
initialValidatedSecretVersions: map[string]string{testName: "1"}, initialValidatedSettings: map[string]validatedSettings{testName: {BindSecretResourceVersion: "1", LDAPConnectionProtocol: upstreamldap.TLS}},
setupMocks: func(conn *mockldapconn.MockConn) { setupMocks: func(conn *mockldapconn.MockConn) {
// Should perform a test dial and bind. // Should perform a test dial and bind.
conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1)
conn.EXPECT().Close().Times(1) conn.EXPECT().Close().Times(1)
}, },
wantResultingCache: []*upstreamldap.ProviderConfig{providerConfigForValidUpstream}, wantResultingCache: []*upstreamldap.ProviderConfig{providerConfigForValidUpstreamWithTLS},
wantResultingUpstreams: []v1alpha1.LDAPIdentityProvider{{ wantResultingUpstreams: []v1alpha1.LDAPIdentityProvider{{
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234},
Status: v1alpha1.LDAPIdentityProviderStatus{ Status: v1alpha1.LDAPIdentityProviderStatus{
@ -676,7 +820,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) {
Conditions: allConditionsTrue(1234, "4242"), Conditions: allConditionsTrue(1234, "4242"),
}, },
}}, }},
wantValidatedSecretVersions: map[string]string{testName: "4242"}, wantValidatedSettings: map[string]validatedSettings{testName: {BindSecretResourceVersion: "4242", LDAPConnectionProtocol: upstreamldap.TLS}},
}, },
{ {
name: "when the LDAP server connection was already validated for this resource generation but the bind secret has changed, then try to validate it again", name: "when the LDAP server connection was already validated for this resource generation but the bind secret has changed, then try to validate it again",
@ -687,13 +831,13 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) {
} }
})}, })},
inputSecrets: []runtime.Object{validBindUserSecret("4242")}, // newer secret version! inputSecrets: []runtime.Object{validBindUserSecret("4242")}, // newer secret version!
initialValidatedSecretVersions: map[string]string{testName: "4241"}, // old version was validated initialValidatedSettings: map[string]validatedSettings{testName: {BindSecretResourceVersion: "4241", LDAPConnectionProtocol: upstreamldap.TLS}}, // old version was validated
setupMocks: func(conn *mockldapconn.MockConn) { setupMocks: func(conn *mockldapconn.MockConn) {
// Should perform a test dial and bind. // Should perform a test dial and bind.
conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1)
conn.EXPECT().Close().Times(1) conn.EXPECT().Close().Times(1)
}, },
wantResultingCache: []*upstreamldap.ProviderConfig{providerConfigForValidUpstream}, wantResultingCache: []*upstreamldap.ProviderConfig{providerConfigForValidUpstreamWithTLS},
wantResultingUpstreams: []v1alpha1.LDAPIdentityProvider{{ wantResultingUpstreams: []v1alpha1.LDAPIdentityProvider{{
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234},
Status: v1alpha1.LDAPIdentityProviderStatus{ Status: v1alpha1.LDAPIdentityProviderStatus{
@ -701,7 +845,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) {
Conditions: allConditionsTrue(1234, "4242"), Conditions: allConditionsTrue(1234, "4242"),
}, },
}}, }},
wantValidatedSecretVersions: map[string]string{testName: "4242"}, wantValidatedSettings: map[string]validatedSettings{testName: {BindSecretResourceVersion: "4242", LDAPConnectionProtocol: upstreamldap.TLS}},
}, },
} }
@ -727,16 +871,19 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) {
tt.setupMocks(conn) tt.setupMocks(conn)
} }
dialer := &comparableDialer{upstreamldap.LDAPDialerFunc(func(ctx context.Context, _ string) (upstreamldap.Conn, error) { dialer := &comparableDialer{upstreamldap.LDAPDialerFunc(func(ctx context.Context, hostAndPort string) (upstreamldap.Conn, error) {
if tt.dialError != nil { if tt.dialErrors != nil {
return nil, tt.dialError dialErr := tt.dialErrors[hostAndPort]
if dialErr != nil {
return nil, dialErr
}
} }
return conn, nil return conn, nil
})} })}
validatedSecretVersionCache := newSecretVersionCache() validatedSecretVersionCache := newSecretVersionCache()
if tt.initialValidatedSecretVersions != nil { if tt.initialValidatedSettings != nil {
validatedSecretVersionCache.ResourceVersionsByName = tt.initialValidatedSecretVersions validatedSecretVersionCache.ValidatedSettingsByName = tt.initialValidatedSettings
} }
controller := newInternal( controller := newInternal(
@ -768,11 +915,11 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) {
require.Equal(t, len(tt.wantResultingCache), len(actualIDPList)) require.Equal(t, len(tt.wantResultingCache), len(actualIDPList))
for i := range actualIDPList { for i := range actualIDPList {
actualIDP := actualIDPList[i].(*upstreamldap.Provider) actualIDP := actualIDPList[i].(*upstreamldap.Provider)
copyOfExpectedValue := *tt.wantResultingCache[i] // copy before edit to avoid race because these tests are run in parallel copyOfExpectedValueForResultingCache := *tt.wantResultingCache[i] // copy before edit to avoid race because these tests are run in parallel
// The dialer that was passed in to the controller's constructor should always have been // The dialer that was passed in to the controller's constructor should always have been
// passed through to the provider. // passed through to the provider.
copyOfExpectedValue.Dialer = dialer copyOfExpectedValueForResultingCache.Dialer = dialer
require.Equal(t, copyOfExpectedValue, actualIDP.GetConfig()) require.Equal(t, copyOfExpectedValueForResultingCache, actualIDP.GetConfig())
} }
actualUpstreams, err := fakePinnipedClient.IDPV1alpha1().LDAPIdentityProviders(testNamespace).List(ctx, metav1.ListOptions{}) actualUpstreams, err := fakePinnipedClient.IDPV1alpha1().LDAPIdentityProviders(testNamespace).List(ctx, metav1.ListOptions{})
@ -787,10 +934,10 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) {
} }
// Check that the controller remembered which version of the secret it most recently validated successfully with. // Check that the controller remembered which version of the secret it most recently validated successfully with.
if tt.wantValidatedSecretVersions == nil { if tt.wantValidatedSettings == nil {
tt.wantValidatedSecretVersions = map[string]string{} tt.wantValidatedSettings = map[string]validatedSettings{}
} }
require.Equal(t, tt.wantValidatedSecretVersions, validatedSecretVersionCache.ResourceVersionsByName) require.Equal(t, tt.wantValidatedSettings, validatedSecretVersionCache.ValidatedSettingsByName)
}) })
} }
} }

View File

@ -157,16 +157,26 @@ func (p *Provider) dial(ctx context.Context) (Conn, error) {
return nil, ldap.NewError(ldap.ErrorNetwork, err) return nil, ldap.NewError(ldap.ErrorNetwork, err)
} }
// Choose how and where to dial based on TLS vs. StartTLS config option.
var dialFunc LDAPDialerFunc
var hostAndPort string
switch { switch {
case p.c.Dialer != nil:
return p.c.Dialer.Dial(ctx, tlsHostAndPort)
case p.c.ConnectionProtocol == TLS: case p.c.ConnectionProtocol == TLS:
return p.dialTLS(ctx, tlsHostAndPort) dialFunc = p.dialTLS
hostAndPort = tlsHostAndPort
case p.c.ConnectionProtocol == StartTLS: case p.c.ConnectionProtocol == StartTLS:
return p.dialStartTLS(ctx, startTLSHostAndPort) dialFunc = p.dialStartTLS
hostAndPort = startTLSHostAndPort
default: default:
return nil, ldap.NewError(ldap.ErrorNetwork, fmt.Errorf("did not specify valid ConnectionProtocol")) return nil, ldap.NewError(ldap.ErrorNetwork, fmt.Errorf("did not specify valid ConnectionProtocol"))
} }
// Override the real dialer for testing purposes sometimes.
if p.c.Dialer != nil {
dialFunc = p.c.Dialer.Dial
}
return dialFunc(ctx, hostAndPort)
} }
// dialTLS is a default implementation of the Dialer, used when Dialer is nil and ConnectionProtocol is TLS. // dialTLS is a default implementation of the Dialer, used when Dialer is nil and ConnectionProtocol is TLS.

View File

@ -58,6 +58,7 @@ func TestEndUserAuthentication(t *testing.T) {
Name: "some-provider-name", Name: "some-provider-name",
Host: testHost, Host: testHost,
CABundle: nil, // this field is only used by the production dialer, which is replaced by a mock for this test CABundle: nil, // this field is only used by the production dialer, which is replaced by a mock for this test
ConnectionProtocol: TLS,
BindUsername: testBindUsername, BindUsername: testBindUsername,
BindPassword: testBindPassword, BindPassword: testBindPassword,
UserSearch: UserSearchConfig{ UserSearch: UserSearchConfig{
@ -992,6 +993,7 @@ func TestTestConnection(t *testing.T) {
Name: "some-provider-name", Name: "some-provider-name",
Host: testHost, Host: testHost,
CABundle: nil, // this field is only used by the production dialer, which is replaced by a mock for this test CABundle: nil, // this field is only used by the production dialer, which is replaced by a mock for this test
ConnectionProtocol: TLS,
BindUsername: testBindUsername, BindUsername: testBindUsername,
BindPassword: testBindPassword, BindPassword: testBindPassword,
UserSearch: UserSearchConfig{}, // not used by TestConnection UserSearch: UserSearchConfig{}, // not used by TestConnection