Test the LDAP config by connecting to the server in the controller
This commit is contained in:
parent
da7216c1ef
commit
b9ce84fd68
@ -8,6 +8,7 @@ import (
|
|||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
corev1 "k8s.io/api/core/v1"
|
corev1 "k8s.io/api/core/v1"
|
||||||
"k8s.io/apimachinery/pkg/api/equality"
|
"k8s.io/apimachinery/pkg/api/equality"
|
||||||
@ -31,7 +32,9 @@ const (
|
|||||||
|
|
||||||
// Constants related to conditions.
|
// Constants related to conditions.
|
||||||
typeBindSecretValid = "BindSecretValid"
|
typeBindSecretValid = "BindSecretValid"
|
||||||
tlsConfigurationValid = "TLSConfigurationValid"
|
typeTLSConfigurationValid = "TLSConfigurationValid"
|
||||||
|
typeLDAPConnectionValid = "LDAPConnectionValid"
|
||||||
|
reasonLDAPConnectionError = "LDAPConnectionError"
|
||||||
noTLSConfigurationMessage = "no TLS configuration provided"
|
noTLSConfigurationMessage = "no TLS configuration provided"
|
||||||
loadedTLSConfigurationMessage = "loaded TLS configuration"
|
loadedTLSConfigurationMessage = "loaded TLS configuration"
|
||||||
)
|
)
|
||||||
@ -118,18 +121,26 @@ func (c *ldapWatcherController) validateUpstream(ctx context.Context, upstream *
|
|||||||
},
|
},
|
||||||
Dialer: c.ldapDialer,
|
Dialer: c.ldapDialer,
|
||||||
}
|
}
|
||||||
conditions := []*v1alpha1.Condition{
|
|
||||||
c.validateSecret(upstream, config),
|
conditions := []*v1alpha1.Condition{}
|
||||||
c.validateTLSConfig(upstream, config),
|
secretValidCondition := c.validateSecret(upstream, config)
|
||||||
|
tlsValidCondition := c.validateTLSConfig(upstream, config)
|
||||||
|
conditions = append(conditions, secretValidCondition, tlsValidCondition)
|
||||||
|
|
||||||
|
// No point in trying to connect to the server if the config was already determined to be invalid.
|
||||||
|
if secretValidCondition.Status == v1alpha1.ConditionTrue && tlsValidCondition.Status == v1alpha1.ConditionTrue {
|
||||||
|
conditions = append(conditions, c.validateFinishedConfig(ctx, config))
|
||||||
}
|
}
|
||||||
|
|
||||||
hadErrorCondition := c.updateStatus(ctx, upstream, conditions)
|
hadErrorCondition := c.updateStatus(ctx, upstream, conditions)
|
||||||
if hadErrorCondition {
|
if hadErrorCondition {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return upstreamldap.New(*config)
|
return upstreamldap.New(*config)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ldapWatcherController) validateTLSConfig(upstream *v1alpha1.LDAPIdentityProvider, result *upstreamldap.ProviderConfig) *v1alpha1.Condition {
|
func (c *ldapWatcherController) validateTLSConfig(upstream *v1alpha1.LDAPIdentityProvider, config *upstreamldap.ProviderConfig) *v1alpha1.Condition {
|
||||||
tlsSpec := upstream.Spec.TLS
|
tlsSpec := upstream.Spec.TLS
|
||||||
if tlsSpec == nil {
|
if tlsSpec == nil {
|
||||||
return c.validTLSCondition(noTLSConfigurationMessage)
|
return c.validTLSCondition(noTLSConfigurationMessage)
|
||||||
@ -149,13 +160,37 @@ func (c *ldapWatcherController) validateTLSConfig(upstream *v1alpha1.LDAPIdentit
|
|||||||
return c.invalidTLSCondition(fmt.Sprintf("certificateAuthorityData is invalid: %s", errNoCertificates))
|
return c.invalidTLSCondition(fmt.Sprintf("certificateAuthorityData is invalid: %s", errNoCertificates))
|
||||||
}
|
}
|
||||||
|
|
||||||
result.CABundle = bundle
|
config.CABundle = bundle
|
||||||
return c.validTLSCondition(loadedTLSConfigurationMessage)
|
return c.validTLSCondition(loadedTLSConfigurationMessage)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *ldapWatcherController) validateFinishedConfig(ctx context.Context, config *upstreamldap.ProviderConfig) *v1alpha1.Condition {
|
||||||
|
ldapProvider := upstreamldap.New(*config)
|
||||||
|
|
||||||
|
testConnectionTimeout, cancelFunc := context.WithTimeout(ctx, 60*time.Second)
|
||||||
|
defer cancelFunc()
|
||||||
|
|
||||||
|
err := ldapProvider.TestConnection(testConnectionTimeout)
|
||||||
|
if err != nil {
|
||||||
|
return &v1alpha1.Condition{
|
||||||
|
Type: typeLDAPConnectionValid,
|
||||||
|
Status: v1alpha1.ConditionFalse,
|
||||||
|
Reason: reasonLDAPConnectionError,
|
||||||
|
Message: fmt.Sprintf(`could not successfully connect to "%s" and bind as user "%s: %s`, config.Host, config.BindUsername, err.Error()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &v1alpha1.Condition{
|
||||||
|
Type: typeLDAPConnectionValid,
|
||||||
|
Status: v1alpha1.ConditionTrue,
|
||||||
|
Reason: reasonSuccess,
|
||||||
|
Message: fmt.Sprintf(`successfully able to connect to "%s" and bind as user "%s"`, config.Host, config.BindUsername),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (c *ldapWatcherController) validTLSCondition(message string) *v1alpha1.Condition {
|
func (c *ldapWatcherController) validTLSCondition(message string) *v1alpha1.Condition {
|
||||||
return &v1alpha1.Condition{
|
return &v1alpha1.Condition{
|
||||||
Type: tlsConfigurationValid,
|
Type: typeTLSConfigurationValid,
|
||||||
Status: v1alpha1.ConditionTrue,
|
Status: v1alpha1.ConditionTrue,
|
||||||
Reason: reasonSuccess,
|
Reason: reasonSuccess,
|
||||||
Message: message,
|
Message: message,
|
||||||
@ -164,14 +199,14 @@ func (c *ldapWatcherController) validTLSCondition(message string) *v1alpha1.Cond
|
|||||||
|
|
||||||
func (c *ldapWatcherController) invalidTLSCondition(message string) *v1alpha1.Condition {
|
func (c *ldapWatcherController) invalidTLSCondition(message string) *v1alpha1.Condition {
|
||||||
return &v1alpha1.Condition{
|
return &v1alpha1.Condition{
|
||||||
Type: tlsConfigurationValid,
|
Type: typeTLSConfigurationValid,
|
||||||
Status: v1alpha1.ConditionFalse,
|
Status: v1alpha1.ConditionFalse,
|
||||||
Reason: reasonInvalidTLSConfig,
|
Reason: reasonInvalidTLSConfig,
|
||||||
Message: message,
|
Message: message,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ldapWatcherController) validateSecret(upstream *v1alpha1.LDAPIdentityProvider, result *upstreamldap.ProviderConfig) *v1alpha1.Condition {
|
func (c *ldapWatcherController) validateSecret(upstream *v1alpha1.LDAPIdentityProvider, config *upstreamldap.ProviderConfig) *v1alpha1.Condition {
|
||||||
secretName := upstream.Spec.Bind.SecretName
|
secretName := upstream.Spec.Bind.SecretName
|
||||||
|
|
||||||
secret, err := c.secretInformer.Lister().Secrets(upstream.Namespace).Get(secretName)
|
secret, err := c.secretInformer.Lister().Secrets(upstream.Namespace).Get(secretName)
|
||||||
@ -193,9 +228,9 @@ func (c *ldapWatcherController) validateSecret(upstream *v1alpha1.LDAPIdentityPr
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
result.BindUsername = string(secret.Data[corev1.BasicAuthUsernameKey])
|
config.BindUsername = string(secret.Data[corev1.BasicAuthUsernameKey])
|
||||||
result.BindPassword = string(secret.Data[corev1.BasicAuthPasswordKey])
|
config.BindPassword = string(secret.Data[corev1.BasicAuthPasswordKey])
|
||||||
if len(result.BindUsername) == 0 || len(result.BindPassword) == 0 {
|
if len(config.BindUsername) == 0 || len(config.BindPassword) == 0 {
|
||||||
return &v1alpha1.Condition{
|
return &v1alpha1.Condition{
|
||||||
Type: typeBindSecretValid,
|
Type: typeBindSecretValid,
|
||||||
Status: v1alpha1.ConditionFalse,
|
Status: v1alpha1.ConditionFalse,
|
||||||
|
@ -6,11 +6,13 @@ package upstreamwatcher
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"sort"
|
"sort"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"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"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
@ -23,6 +25,7 @@ import (
|
|||||||
pinnipedinformers "go.pinniped.dev/generated/latest/client/supervisor/informers/externalversions"
|
pinnipedinformers "go.pinniped.dev/generated/latest/client/supervisor/informers/externalversions"
|
||||||
"go.pinniped.dev/internal/certauthority"
|
"go.pinniped.dev/internal/certauthority"
|
||||||
"go.pinniped.dev/internal/controllerlib"
|
"go.pinniped.dev/internal/controllerlib"
|
||||||
|
"go.pinniped.dev/internal/mocks/mockldapconn"
|
||||||
"go.pinniped.dev/internal/oidc/provider"
|
"go.pinniped.dev/internal/oidc/provider"
|
||||||
"go.pinniped.dev/internal/testutil"
|
"go.pinniped.dev/internal/testutil"
|
||||||
"go.pinniped.dev/internal/upstreamldap"
|
"go.pinniped.dev/internal/upstreamldap"
|
||||||
@ -165,13 +168,6 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) {
|
|||||||
testCABundle := testCA.Bundle()
|
testCABundle := testCA.Bundle()
|
||||||
testCABundleBase64Encoded := base64.StdEncoding.EncodeToString(testCABundle)
|
testCABundleBase64Encoded := base64.StdEncoding.EncodeToString(testCABundle)
|
||||||
|
|
||||||
successfulDialer := &comparableDialer{
|
|
||||||
f: func(ctx context.Context, hostAndPort string) (upstreamldap.Conn, error) {
|
|
||||||
// TODO return a fake implementation of upstreamldap.Conn, or return an error for testing errors
|
|
||||||
return nil, nil
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
validUpstream := &v1alpha1.LDAPIdentityProvider{
|
validUpstream := &v1alpha1.LDAPIdentityProvider{
|
||||||
ObjectMeta: metav1.ObjectMeta{Name: testName, Namespace: testNamespace, Generation: 1234},
|
ObjectMeta: metav1.ObjectMeta{Name: testName, Namespace: testNamespace, Generation: 1234},
|
||||||
Spec: v1alpha1.LDAPIdentityProviderSpec{
|
Spec: v1alpha1.LDAPIdentityProviderSpec{
|
||||||
@ -206,30 +202,35 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) {
|
|||||||
UsernameAttribute: testUsernameAttrName,
|
UsernameAttribute: testUsernameAttrName,
|
||||||
UIDAttribute: testUIDAttrName,
|
UIDAttribute: testUIDAttrName,
|
||||||
},
|
},
|
||||||
Dialer: successfulDialer, // the dialer passed to the controller's constructor should have been passed through
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
inputUpstreams []runtime.Object
|
inputUpstreams []runtime.Object
|
||||||
inputSecrets []runtime.Object
|
inputSecrets []runtime.Object
|
||||||
ldapDialer upstreamldap.LDAPDialer
|
setupMocks func(conn *mockldapconn.MockConn)
|
||||||
|
dialError error
|
||||||
wantErr string
|
wantErr string
|
||||||
wantResultingCache []*upstreamldap.ProviderConfig
|
wantResultingCache []*upstreamldap.ProviderConfig
|
||||||
wantResultingUpstreams []v1alpha1.LDAPIdentityProvider
|
wantResultingUpstreams []v1alpha1.LDAPIdentityProvider
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "no LDAPIdentityProvider upstreams clears the cache",
|
name: "no LDAPIdentityProvider upstreams clears the cache",
|
||||||
|
wantResultingCache: []*upstreamldap.ProviderConfig{},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "one valid upstream updates the cache to include only that upstream",
|
name: "one valid upstream updates the cache to include only that upstream",
|
||||||
ldapDialer: successfulDialer,
|
|
||||||
inputUpstreams: []runtime.Object{validUpstream},
|
inputUpstreams: []runtime.Object{validUpstream},
|
||||||
inputSecrets: []runtime.Object{&corev1.Secret{
|
inputSecrets: []runtime.Object{&corev1.Secret{
|
||||||
ObjectMeta: metav1.ObjectMeta{Name: testSecretName, Namespace: testNamespace},
|
ObjectMeta: metav1.ObjectMeta{Name: testSecretName, Namespace: testNamespace},
|
||||||
Type: corev1.SecretTypeBasicAuth,
|
Type: corev1.SecretTypeBasicAuth,
|
||||||
Data: testValidSecretData,
|
Data: testValidSecretData,
|
||||||
}},
|
}},
|
||||||
|
setupMocks: func(conn *mockldapconn.MockConn) {
|
||||||
|
// Should perform a test dial and bind.
|
||||||
|
conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1)
|
||||||
|
conn.EXPECT().Close().Times(1)
|
||||||
|
},
|
||||||
wantResultingCache: []*upstreamldap.ProviderConfig{providerConfigForValidUpstream},
|
wantResultingCache: []*upstreamldap.ProviderConfig{providerConfigForValidUpstream},
|
||||||
wantResultingUpstreams: []v1alpha1.LDAPIdentityProvider{{
|
wantResultingUpstreams: []v1alpha1.LDAPIdentityProvider{{
|
||||||
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234},
|
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234},
|
||||||
@ -244,6 +245,14 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) {
|
|||||||
Message: "loaded bind secret",
|
Message: "loaded bind secret",
|
||||||
ObservedGeneration: 1234,
|
ObservedGeneration: 1234,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Type: "LDAPConnectionValid",
|
||||||
|
Status: "True",
|
||||||
|
LastTransitionTime: now,
|
||||||
|
Reason: "Success",
|
||||||
|
Message: fmt.Sprintf(`successfully able to connect to "%s" and bind as user "%s"`, testHost, testBindUsername),
|
||||||
|
ObservedGeneration: 1234,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
Type: "TLSConfigurationValid",
|
Type: "TLSConfigurationValid",
|
||||||
Status: "True",
|
Status: "True",
|
||||||
@ -258,7 +267,6 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "missing secret",
|
name: "missing secret",
|
||||||
ldapDialer: successfulDialer,
|
|
||||||
inputUpstreams: []runtime.Object{validUpstream},
|
inputUpstreams: []runtime.Object{validUpstream},
|
||||||
inputSecrets: []runtime.Object{},
|
inputSecrets: []runtime.Object{},
|
||||||
wantErr: controllerlib.ErrSyntheticRequeue.Error(),
|
wantErr: controllerlib.ErrSyntheticRequeue.Error(),
|
||||||
@ -290,7 +298,6 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "secret has wrong type",
|
name: "secret has wrong type",
|
||||||
ldapDialer: successfulDialer,
|
|
||||||
inputUpstreams: []runtime.Object{validUpstream},
|
inputUpstreams: []runtime.Object{validUpstream},
|
||||||
inputSecrets: []runtime.Object{&corev1.Secret{
|
inputSecrets: []runtime.Object{&corev1.Secret{
|
||||||
ObjectMeta: metav1.ObjectMeta{Name: testSecretName, Namespace: testNamespace},
|
ObjectMeta: metav1.ObjectMeta{Name: testSecretName, Namespace: testNamespace},
|
||||||
@ -326,7 +333,6 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "secret is missing key",
|
name: "secret is missing key",
|
||||||
ldapDialer: successfulDialer,
|
|
||||||
inputUpstreams: []runtime.Object{validUpstream},
|
inputUpstreams: []runtime.Object{validUpstream},
|
||||||
inputSecrets: []runtime.Object{&corev1.Secret{
|
inputSecrets: []runtime.Object{&corev1.Secret{
|
||||||
ObjectMeta: metav1.ObjectMeta{Name: testSecretName, Namespace: testNamespace},
|
ObjectMeta: metav1.ObjectMeta{Name: testSecretName, Namespace: testNamespace},
|
||||||
@ -360,8 +366,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) {
|
|||||||
}},
|
}},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "CertificateAuthorityData is not base64 encoded",
|
name: "CertificateAuthorityData is not base64 encoded",
|
||||||
ldapDialer: successfulDialer,
|
|
||||||
inputUpstreams: []runtime.Object{modifiedCopyOfValidUpstream(func(upstream *v1alpha1.LDAPIdentityProvider) {
|
inputUpstreams: []runtime.Object{modifiedCopyOfValidUpstream(func(upstream *v1alpha1.LDAPIdentityProvider) {
|
||||||
upstream.Spec.TLS.CertificateAuthorityData = "this-is-not-base64-encoded"
|
upstream.Spec.TLS.CertificateAuthorityData = "this-is-not-base64-encoded"
|
||||||
})},
|
})},
|
||||||
@ -398,8 +403,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) {
|
|||||||
}},
|
}},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "CertificateAuthorityData is not valid pem data",
|
name: "CertificateAuthorityData is not valid pem data",
|
||||||
ldapDialer: successfulDialer,
|
|
||||||
inputUpstreams: []runtime.Object{modifiedCopyOfValidUpstream(func(upstream *v1alpha1.LDAPIdentityProvider) {
|
inputUpstreams: []runtime.Object{modifiedCopyOfValidUpstream(func(upstream *v1alpha1.LDAPIdentityProvider) {
|
||||||
upstream.Spec.TLS.CertificateAuthorityData = base64.StdEncoding.EncodeToString([]byte("this is not pem data"))
|
upstream.Spec.TLS.CertificateAuthorityData = base64.StdEncoding.EncodeToString([]byte("this is not pem data"))
|
||||||
})},
|
})},
|
||||||
@ -436,8 +440,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) {
|
|||||||
}},
|
}},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "nil TLS configuration",
|
name: "nil TLS configuration is valid",
|
||||||
ldapDialer: successfulDialer,
|
|
||||||
inputUpstreams: []runtime.Object{modifiedCopyOfValidUpstream(func(upstream *v1alpha1.LDAPIdentityProvider) {
|
inputUpstreams: []runtime.Object{modifiedCopyOfValidUpstream(func(upstream *v1alpha1.LDAPIdentityProvider) {
|
||||||
upstream.Spec.TLS = nil
|
upstream.Spec.TLS = nil
|
||||||
})},
|
})},
|
||||||
@ -446,6 +449,11 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) {
|
|||||||
Type: corev1.SecretTypeBasicAuth,
|
Type: corev1.SecretTypeBasicAuth,
|
||||||
Data: testValidSecretData,
|
Data: testValidSecretData,
|
||||||
}},
|
}},
|
||||||
|
setupMocks: func(conn *mockldapconn.MockConn) {
|
||||||
|
// Should perform a test dial and bind.
|
||||||
|
conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1)
|
||||||
|
conn.EXPECT().Close().Times(1)
|
||||||
|
},
|
||||||
wantResultingCache: []*upstreamldap.ProviderConfig{
|
wantResultingCache: []*upstreamldap.ProviderConfig{
|
||||||
{
|
{
|
||||||
Name: testName,
|
Name: testName,
|
||||||
@ -459,7 +467,6 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) {
|
|||||||
UsernameAttribute: testUsernameAttrName,
|
UsernameAttribute: testUsernameAttrName,
|
||||||
UIDAttribute: testUIDAttrName,
|
UIDAttribute: testUIDAttrName,
|
||||||
},
|
},
|
||||||
Dialer: successfulDialer, // the dialer passed to the controller's constructor should have been passed through
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
wantResultingUpstreams: []v1alpha1.LDAPIdentityProvider{{
|
wantResultingUpstreams: []v1alpha1.LDAPIdentityProvider{{
|
||||||
@ -475,6 +482,14 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) {
|
|||||||
Message: "loaded bind secret",
|
Message: "loaded bind secret",
|
||||||
ObservedGeneration: 1234,
|
ObservedGeneration: 1234,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Type: "LDAPConnectionValid",
|
||||||
|
Status: "True",
|
||||||
|
LastTransitionTime: now,
|
||||||
|
Reason: "Success",
|
||||||
|
Message: fmt.Sprintf(`successfully able to connect to "%s" and bind as user "%s"`, testHost, testBindUsername),
|
||||||
|
ObservedGeneration: 1234,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
Type: "TLSConfigurationValid",
|
Type: "TLSConfigurationValid",
|
||||||
Status: "True",
|
Status: "True",
|
||||||
@ -488,8 +503,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) {
|
|||||||
}},
|
}},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "non-nil TLS configuration with empty CertificateAuthorityData",
|
name: "non-nil TLS configuration with empty CertificateAuthorityData is valid",
|
||||||
ldapDialer: successfulDialer,
|
|
||||||
inputUpstreams: []runtime.Object{modifiedCopyOfValidUpstream(func(upstream *v1alpha1.LDAPIdentityProvider) {
|
inputUpstreams: []runtime.Object{modifiedCopyOfValidUpstream(func(upstream *v1alpha1.LDAPIdentityProvider) {
|
||||||
upstream.Spec.TLS.CertificateAuthorityData = ""
|
upstream.Spec.TLS.CertificateAuthorityData = ""
|
||||||
})},
|
})},
|
||||||
@ -498,6 +512,11 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) {
|
|||||||
Type: corev1.SecretTypeBasicAuth,
|
Type: corev1.SecretTypeBasicAuth,
|
||||||
Data: testValidSecretData,
|
Data: testValidSecretData,
|
||||||
}},
|
}},
|
||||||
|
setupMocks: func(conn *mockldapconn.MockConn) {
|
||||||
|
// Should perform a test dial and bind.
|
||||||
|
conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1)
|
||||||
|
conn.EXPECT().Close().Times(1)
|
||||||
|
},
|
||||||
wantResultingCache: []*upstreamldap.ProviderConfig{
|
wantResultingCache: []*upstreamldap.ProviderConfig{
|
||||||
{
|
{
|
||||||
Name: testName,
|
Name: testName,
|
||||||
@ -511,7 +530,6 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) {
|
|||||||
UsernameAttribute: testUsernameAttrName,
|
UsernameAttribute: testUsernameAttrName,
|
||||||
UIDAttribute: testUIDAttrName,
|
UIDAttribute: testUIDAttrName,
|
||||||
},
|
},
|
||||||
Dialer: successfulDialer, // the dialer passed to the controller's constructor should have been passed through
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
wantResultingUpstreams: []v1alpha1.LDAPIdentityProvider{{
|
wantResultingUpstreams: []v1alpha1.LDAPIdentityProvider{{
|
||||||
@ -527,6 +545,14 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) {
|
|||||||
Message: "loaded bind secret",
|
Message: "loaded bind secret",
|
||||||
ObservedGeneration: 1234,
|
ObservedGeneration: 1234,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Type: "LDAPConnectionValid",
|
||||||
|
Status: "True",
|
||||||
|
LastTransitionTime: now,
|
||||||
|
Reason: "Success",
|
||||||
|
Message: fmt.Sprintf(`successfully able to connect to "%s" and bind as user "%s"`, testHost, testBindUsername),
|
||||||
|
ObservedGeneration: 1234,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
Type: "TLSConfigurationValid",
|
Type: "TLSConfigurationValid",
|
||||||
Status: "True",
|
Status: "True",
|
||||||
@ -540,8 +566,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) {
|
|||||||
}},
|
}},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
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",
|
||||||
ldapDialer: successfulDialer,
|
|
||||||
inputUpstreams: []runtime.Object{validUpstream, modifiedCopyOfValidUpstream(func(upstream *v1alpha1.LDAPIdentityProvider) {
|
inputUpstreams: []runtime.Object{validUpstream, modifiedCopyOfValidUpstream(func(upstream *v1alpha1.LDAPIdentityProvider) {
|
||||||
upstream.Name = "other-upstream"
|
upstream.Name = "other-upstream"
|
||||||
upstream.Generation = 42
|
upstream.Generation = 42
|
||||||
@ -552,6 +577,11 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) {
|
|||||||
Type: corev1.SecretTypeBasicAuth,
|
Type: corev1.SecretTypeBasicAuth,
|
||||||
Data: testValidSecretData,
|
Data: testValidSecretData,
|
||||||
}},
|
}},
|
||||||
|
setupMocks: func(conn *mockldapconn.MockConn) {
|
||||||
|
// Should perform a test dial and bind for the one valid upstream configuration.
|
||||||
|
conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1)
|
||||||
|
conn.EXPECT().Close().Times(1)
|
||||||
|
},
|
||||||
wantErr: controllerlib.ErrSyntheticRequeue.Error(),
|
wantErr: controllerlib.ErrSyntheticRequeue.Error(),
|
||||||
wantResultingCache: []*upstreamldap.ProviderConfig{providerConfigForValidUpstream},
|
wantResultingCache: []*upstreamldap.ProviderConfig{providerConfigForValidUpstream},
|
||||||
wantResultingUpstreams: []v1alpha1.LDAPIdentityProvider{
|
wantResultingUpstreams: []v1alpha1.LDAPIdentityProvider{
|
||||||
@ -592,6 +622,14 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) {
|
|||||||
Message: "loaded bind secret",
|
Message: "loaded bind secret",
|
||||||
ObservedGeneration: 1234,
|
ObservedGeneration: 1234,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Type: "LDAPConnectionValid",
|
||||||
|
Status: "True",
|
||||||
|
LastTransitionTime: now,
|
||||||
|
Reason: "Success",
|
||||||
|
Message: fmt.Sprintf(`successfully able to connect to "%s" and bind as user "%s"`, testHost, testBindUsername),
|
||||||
|
ObservedGeneration: 1234,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
Type: "TLSConfigurationValid",
|
Type: "TLSConfigurationValid",
|
||||||
Status: "True",
|
Status: "True",
|
||||||
@ -605,11 +643,62 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "when testing the connection to the LDAP server fails then the upstream is not added to the cache",
|
||||||
|
inputUpstreams: []runtime.Object{validUpstream},
|
||||||
|
inputSecrets: []runtime.Object{&corev1.Secret{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Name: testSecretName, Namespace: testNamespace},
|
||||||
|
Type: corev1.SecretTypeBasicAuth,
|
||||||
|
Data: testValidSecretData,
|
||||||
|
}},
|
||||||
|
setupMocks: func(conn *mockldapconn.MockConn) {
|
||||||
|
// Should perform a test dial and bind.
|
||||||
|
conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1).Return(errors.New("some bind error"))
|
||||||
|
conn.EXPECT().Close().Times(1)
|
||||||
|
},
|
||||||
|
wantErr: controllerlib.ErrSyntheticRequeue.Error(),
|
||||||
|
wantResultingCache: []*upstreamldap.ProviderConfig{},
|
||||||
|
wantResultingUpstreams: []v1alpha1.LDAPIdentityProvider{{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234},
|
||||||
|
Status: v1alpha1.LDAPIdentityProviderStatus{
|
||||||
|
Phase: "Error",
|
||||||
|
Conditions: []v1alpha1.Condition{
|
||||||
|
{
|
||||||
|
Type: "BindSecretValid",
|
||||||
|
Status: "True",
|
||||||
|
LastTransitionTime: now,
|
||||||
|
Reason: "Success",
|
||||||
|
Message: "loaded bind secret",
|
||||||
|
ObservedGeneration: 1234,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: "LDAPConnectionValid",
|
||||||
|
Status: "False",
|
||||||
|
LastTransitionTime: now,
|
||||||
|
Reason: "LDAPConnectionError",
|
||||||
|
Message: fmt.Sprintf(
|
||||||
|
`could not successfully connect to "%s" and bind as user "%s: error binding as "%s": some bind error`,
|
||||||
|
testHost, testBindUsername, testBindUsername),
|
||||||
|
ObservedGeneration: 1234,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: "TLSConfigurationValid",
|
||||||
|
Status: "True",
|
||||||
|
LastTransitionTime: now,
|
||||||
|
Reason: "Success",
|
||||||
|
Message: "loaded TLS configuration",
|
||||||
|
ObservedGeneration: 1234,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
tt := tt
|
tt := tt
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
fakePinnipedClient := pinnipedfake.NewSimpleClientset(tt.inputUpstreams...)
|
fakePinnipedClient := pinnipedfake.NewSimpleClientset(tt.inputUpstreams...)
|
||||||
pinnipedInformers := pinnipedinformers.NewSharedInformerFactory(fakePinnipedClient, 0)
|
pinnipedInformers := pinnipedinformers.NewSharedInformerFactory(fakePinnipedClient, 0)
|
||||||
fakeKubeClient := fake.NewSimpleClientset(tt.inputSecrets...)
|
fakeKubeClient := fake.NewSimpleClientset(tt.inputSecrets...)
|
||||||
@ -619,9 +708,24 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) {
|
|||||||
upstreamldap.New(upstreamldap.ProviderConfig{Name: "initial-entry"}),
|
upstreamldap.New(upstreamldap.ProviderConfig{Name: "initial-entry"}),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
ctrl := gomock.NewController(t)
|
||||||
|
t.Cleanup(ctrl.Finish)
|
||||||
|
|
||||||
|
conn := mockldapconn.NewMockConn(ctrl)
|
||||||
|
if tt.setupMocks != nil {
|
||||||
|
tt.setupMocks(conn)
|
||||||
|
}
|
||||||
|
|
||||||
|
dialer := &comparableDialer{f: upstreamldap.LDAPDialerFunc(func(ctx context.Context, _ string) (upstreamldap.Conn, error) {
|
||||||
|
if tt.dialError != nil {
|
||||||
|
return nil, tt.dialError
|
||||||
|
}
|
||||||
|
return conn, nil
|
||||||
|
})}
|
||||||
|
|
||||||
controller := NewLDAPUpstreamWatcherController(
|
controller := NewLDAPUpstreamWatcherController(
|
||||||
cache,
|
cache,
|
||||||
successfulDialer,
|
dialer,
|
||||||
fakePinnipedClient,
|
fakePinnipedClient,
|
||||||
pinnipedInformers.IDP().V1alpha1().LDAPIdentityProviders(),
|
pinnipedInformers.IDP().V1alpha1().LDAPIdentityProviders(),
|
||||||
kubeInformers.Core().V1().Secrets(),
|
kubeInformers.Core().V1().Secrets(),
|
||||||
@ -647,7 +751,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)
|
||||||
require.Equal(t, *tt.wantResultingCache[i], actualIDP.GetConfig())
|
copyOfExpectedValue := *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
|
||||||
|
// passed through to the provider.
|
||||||
|
copyOfExpectedValue.Dialer = dialer
|
||||||
|
require.Equal(t, copyOfExpectedValue, actualIDP.GetConfig())
|
||||||
}
|
}
|
||||||
|
|
||||||
actualUpstreams, err := fakePinnipedClient.IDPV1alpha1().LDAPIdentityProviders(testNamespace).List(ctx, metav1.ListOptions{})
|
actualUpstreams, err := fakePinnipedClient.IDPV1alpha1().LDAPIdentityProviders(testNamespace).List(ctx, metav1.ListOptions{})
|
||||||
@ -660,13 +768,6 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) {
|
|||||||
// Require each separately to get a nice diff when the test fails.
|
// Require each separately to get a nice diff when the test fails.
|
||||||
require.Equal(t, tt.wantResultingUpstreams[i], normalizedActualUpstreams[i])
|
require.Equal(t, tt.wantResultingUpstreams[i], normalizedActualUpstreams[i])
|
||||||
}
|
}
|
||||||
|
|
||||||
// Running the sync() a second time should be idempotent, and should return the same error.
|
|
||||||
if err := controllerlib.TestSync(t, controller, syncCtx); tt.wantErr != "" {
|
|
||||||
require.EqualError(t, err, tt.wantErr)
|
|
||||||
} else {
|
|
||||||
require.NoError(t, err)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -182,10 +182,24 @@ func (p *Provider) GetURL() string {
|
|||||||
|
|
||||||
// TestConnection provides a method for testing the connection and bind settings. It performs a dial and bind
|
// TestConnection provides a method for testing the connection and bind settings. It performs a dial and bind
|
||||||
// and returns any errors that we encountered.
|
// and returns any errors that we encountered.
|
||||||
func (p *Provider) TestConnection(ctx context.Context) (*authenticator.Response, error) {
|
func (p *Provider) TestConnection(ctx context.Context) error {
|
||||||
_, _ = p.dial(ctx)
|
err := p.validateConfig()
|
||||||
// TODO implement me
|
if err != nil {
|
||||||
return nil, nil
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
conn, err := p.dial(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf(`error dialing host "%s": %w`, p.c.Host, err)
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
err = conn.Bind(p.c.BindUsername, p.c.BindPassword)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf(`error binding as "%s": %w`, p.c.BindUsername, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestAuthenticateUser provides a method for testing all of the Provider settings in a kind of dry run of
|
// TestAuthenticateUser provides a method for testing all of the Provider settings in a kind of dry run of
|
||||||
@ -199,9 +213,9 @@ func (p *Provider) TestAuthenticateUser(ctx context.Context, testUsername string
|
|||||||
|
|
||||||
// Authenticate a user and return their mapped username, groups, and UID. Implements authenticators.UserAuthenticator.
|
// Authenticate a user and return their mapped username, groups, and UID. Implements authenticators.UserAuthenticator.
|
||||||
func (p *Provider) AuthenticateUser(ctx context.Context, username, password string) (*authenticator.Response, bool, error) {
|
func (p *Provider) AuthenticateUser(ctx context.Context, username, password string) (*authenticator.Response, bool, error) {
|
||||||
if p.c.UserSearch.UsernameAttribute == distinguishedNameAttributeName && len(p.c.UserSearch.Filter) == 0 {
|
err := p.validateConfig()
|
||||||
// LDAP search filters do not allow searching by DN.
|
if err != nil {
|
||||||
return nil, false, fmt.Errorf(`must specify UserSearch Filter when UserSearch UsernameAttribute is "dn"`)
|
return nil, false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(username) == 0 {
|
if len(username) == 0 {
|
||||||
@ -239,6 +253,14 @@ func (p *Provider) AuthenticateUser(ctx context.Context, username, password stri
|
|||||||
return response, true, nil
|
return response, true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *Provider) validateConfig() error {
|
||||||
|
if p.c.UserSearch.UsernameAttribute == distinguishedNameAttributeName && len(p.c.UserSearch.Filter) == 0 {
|
||||||
|
// LDAP search filters do not allow searching by DN, so we would have no reasonable default for Filter.
|
||||||
|
return fmt.Errorf(`must specify UserSearch Filter when UserSearch UsernameAttribute is "dn"`)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (p *Provider) searchAndBindUser(conn Conn, username string, password string) (string, string, error) {
|
func (p *Provider) searchAndBindUser(conn Conn, username string, password string) (string, string, error) {
|
||||||
searchResult, err := conn.Search(p.userSearchRequest(username))
|
searchResult, err := conn.Search(p.userSearchRequest(username))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -608,6 +608,101 @@ func TestAuthenticateUser(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestTestConnection(t *testing.T) {
|
||||||
|
providerConfig := func(editFunc func(p *ProviderConfig)) *ProviderConfig {
|
||||||
|
config := &ProviderConfig{
|
||||||
|
Name: "some-provider-name",
|
||||||
|
Host: testHost,
|
||||||
|
CABundle: nil, // this field is only used by the production dialer, which is replaced by a mock for this test
|
||||||
|
BindUsername: testBindUsername,
|
||||||
|
BindPassword: testBindPassword,
|
||||||
|
UserSearch: UserSearchConfig{}, // not used by TestConnection
|
||||||
|
}
|
||||||
|
if editFunc != nil {
|
||||||
|
editFunc(config)
|
||||||
|
}
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
providerConfig *ProviderConfig
|
||||||
|
setupMocks func(conn *mockldapconn.MockConn)
|
||||||
|
dialError error
|
||||||
|
wantError string
|
||||||
|
wantToSkipDial bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "happy path",
|
||||||
|
providerConfig: providerConfig(nil),
|
||||||
|
setupMocks: func(conn *mockldapconn.MockConn) {
|
||||||
|
conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1)
|
||||||
|
conn.EXPECT().Close().Times(1)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "when dial fails",
|
||||||
|
providerConfig: providerConfig(nil),
|
||||||
|
dialError: errors.New("some dial error"),
|
||||||
|
wantError: fmt.Sprintf(`error dialing host "%s": some dial error`, testHost),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "when binding as the bind user returns an error",
|
||||||
|
providerConfig: providerConfig(nil),
|
||||||
|
setupMocks: func(conn *mockldapconn.MockConn) {
|
||||||
|
conn.EXPECT().Bind(testBindUsername, testBindPassword).Return(errors.New("some bind error")).Times(1)
|
||||||
|
conn.EXPECT().Close().Times(1)
|
||||||
|
},
|
||||||
|
wantError: fmt.Sprintf(`error binding as "%s": some bind error`, testBindUsername),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "when the config is invalid",
|
||||||
|
providerConfig: providerConfig(func(p *ProviderConfig) {
|
||||||
|
// This particular combination of options is not allowed.
|
||||||
|
p.UserSearch.UsernameAttribute = "dn"
|
||||||
|
p.UserSearch.Filter = ""
|
||||||
|
}),
|
||||||
|
wantToSkipDial: true,
|
||||||
|
wantError: `must specify UserSearch Filter when UserSearch UsernameAttribute is "dn"`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
tt := test
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
ctrl := gomock.NewController(t)
|
||||||
|
t.Cleanup(ctrl.Finish)
|
||||||
|
|
||||||
|
conn := mockldapconn.NewMockConn(ctrl)
|
||||||
|
if tt.setupMocks != nil {
|
||||||
|
tt.setupMocks(conn)
|
||||||
|
}
|
||||||
|
|
||||||
|
dialWasAttempted := false
|
||||||
|
tt.providerConfig.Dialer = LDAPDialerFunc(func(ctx context.Context, hostAndPort string) (Conn, error) {
|
||||||
|
dialWasAttempted = true
|
||||||
|
require.Equal(t, tt.providerConfig.Host, hostAndPort)
|
||||||
|
if tt.dialError != nil {
|
||||||
|
return nil, tt.dialError
|
||||||
|
}
|
||||||
|
return conn, nil
|
||||||
|
})
|
||||||
|
|
||||||
|
provider := New(*tt.providerConfig)
|
||||||
|
err := provider.TestConnection(context.Background())
|
||||||
|
|
||||||
|
require.Equal(t, !tt.wantToSkipDial, dialWasAttempted)
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case tt.wantError != "":
|
||||||
|
require.EqualError(t, err, tt.wantError)
|
||||||
|
default:
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestGetConfig(t *testing.T) {
|
func TestGetConfig(t *testing.T) {
|
||||||
c := ProviderConfig{
|
c := ProviderConfig{
|
||||||
Name: "original-provider-name",
|
Name: "original-provider-name",
|
||||||
|
Loading…
Reference in New Issue
Block a user