Add authentication dry run validation to LDAPIdentityProvider

Also force the LDAP server pod to restart whenever the LDIF file
changes, so whenever you redeploy the tools deployment with a new test
user password the server will be updated.
This commit is contained in:
Ryan Richard
2021-04-16 14:04:05 -07:00
parent 83085aa3d6
commit e9d5743845
21 changed files with 802 additions and 175 deletions
@@ -33,12 +33,13 @@ const (
testLDAPConnectionTimeout = 90 * time.Second
// Constants related to conditions.
typeBindSecretValid = "BindSecretValid"
typeTLSConfigurationValid = "TLSConfigurationValid"
typeLDAPConnectionValid = "LDAPConnectionValid"
reasonLDAPConnectionError = "LDAPConnectionError"
noTLSConfigurationMessage = "no TLS configuration provided"
loadedTLSConfigurationMessage = "loaded TLS configuration"
typeBindSecretValid = "BindSecretValid"
typeTLSConfigurationValid = "TLSConfigurationValid"
typeLDAPConnectionValid = "LDAPConnectionValid"
reasonLDAPConnectionError = "LDAPConnectionError"
reasonAuthenticationDryRunError = "AuthenticationDryRunError"
noTLSConfigurationMessage = "no TLS configuration provided"
loadedTLSConfigurationMessage = "loaded TLS configuration"
)
var (
@@ -184,7 +185,21 @@ func (c *ldapWatcherController) validateFinishedConfig(ctx context.Context, upst
testConnectionTimeout, cancelFunc := context.WithTimeout(ctx, testLDAPConnectionTimeout)
defer cancelFunc()
err := ldapProvider.TestConnection(testConnectionTimeout)
if len(upstream.Spec.DryRunAuthenticationUsername) > 0 {
return c.dryRunAuthentication(testConnectionTimeout, upstream, ldapProvider, currentSecretVersion)
}
return c.testConnection(testConnectionTimeout, upstream, config, ldapProvider, currentSecretVersion)
}
func (c *ldapWatcherController) testConnection(
ctx context.Context,
upstream *v1alpha1.LDAPIdentityProvider,
config *upstreamldap.ProviderConfig,
ldapProvider *upstreamldap.Provider,
currentSecretVersion string,
) *v1alpha1.Condition {
err := ldapProvider.TestConnection(ctx)
if err != nil {
return &v1alpha1.Condition{
Type: typeLDAPConnectionValid,
@@ -204,6 +219,47 @@ func (c *ldapWatcherController) validateFinishedConfig(ctx context.Context, upst
}
}
func (c *ldapWatcherController) dryRunAuthentication(
ctx context.Context,
upstream *v1alpha1.LDAPIdentityProvider,
ldapProvider *upstreamldap.Provider,
currentSecretVersion string,
) *v1alpha1.Condition {
authResponse, authenticated, err := ldapProvider.DryRunAuthenticateUser(ctx, upstream.Spec.DryRunAuthenticationUsername)
if err != nil {
return &v1alpha1.Condition{
Type: typeLDAPConnectionValid,
Status: v1alpha1.ConditionFalse,
Reason: reasonAuthenticationDryRunError,
Message: fmt.Sprintf(`failed authentication dry run for end user "%s": %s`,
upstream.Spec.DryRunAuthenticationUsername, err.Error()),
}
}
if !authenticated {
// Since we aren't doing a real auth with a password that could be wrong, the only reason we should get
// an unauthenticated response without an error is when the username was wrong.
return &v1alpha1.Condition{
Type: typeLDAPConnectionValid,
Status: v1alpha1.ConditionFalse,
Reason: reasonAuthenticationDryRunError,
Message: fmt.Sprintf(`failed authentication dry run for end user "%s": user not found`,
upstream.Spec.DryRunAuthenticationUsername),
}
}
return &v1alpha1.Condition{
Type: typeLDAPConnectionValid,
Status: v1alpha1.ConditionTrue,
Reason: reasonSuccess,
Message: fmt.Sprintf(
`successful authentication dry run for end user "%s": selected username "%s" and UID "%s" [validated with Secret "%s" at version "%s"]`,
upstream.Spec.DryRunAuthenticationUsername,
authResponse.User.GetName(), authResponse.User.GetUID(),
upstream.Spec.Bind.SecretName, currentSecretVersion),
}
}
func hasPreviousSuccessfulConditionForCurrentSpecGenerationAndSecretVersion(upstream *v1alpha1.LDAPIdentityProvider, currentSecretVersion string) bool {
currentGeneration := upstream.Generation
for _, c := range upstream.Status.Conditions {
@@ -12,6 +12,8 @@ import (
"testing"
"time"
"github.com/go-ldap/ldap/v3"
"github.com/golang/mock/gomock"
"github.com/stretchr/testify/require"
corev1 "k8s.io/api/core/v1"
@@ -665,7 +667,125 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) {
},
}},
},
{
name: "when DryRunAuthenticationUsername is specified and a successful dry run authentication is performed",
inputUpstreams: []runtime.Object{editedValidUpstream(func(upstream *v1alpha1.LDAPIdentityProvider) {
upstream.Spec.DryRunAuthenticationUsername = "endUserUsername"
})},
inputSecrets: []runtime.Object{validBindUserSecret("4242")},
setupMocks: func(conn *mockldapconn.MockConn) {
// Should perform a full auth dry run.
conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1)
conn.EXPECT().Search(gomock.Any()).Return(&ldap.SearchResult{
Entries: []*ldap.Entry{
{
DN: "testFoundUserDN",
Attributes: []*ldap.EntryAttribute{
ldap.NewEntryAttribute(testUsernameAttrName, []string{"testDownstreamUsername"}),
ldap.NewEntryAttribute(testUIDAttrName, []string{"testDownstreamUID"}),
},
},
},
}, nil).Times(1)
conn.EXPECT().Close().Times(1)
},
wantResultingCache: []*upstreamldap.ProviderConfig{providerConfigForValidUpstream},
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(
`successful authentication dry run for end user "%s": selected username "%s" and UID "%s" [validated with Secret "%s" at version "%s"]`,
"endUserUsername", "testDownstreamUsername", "testDownstreamUID", testSecretName, "4242"),
ObservedGeneration: 1234,
},
tlsConfigurationValidLoadedTrueCondition(1234),
},
},
}},
},
{
name: "when DryRunAuthenticationUsername is specified and the dry run authentication returns an error",
inputUpstreams: []runtime.Object{editedValidUpstream(func(upstream *v1alpha1.LDAPIdentityProvider) {
upstream.Spec.DryRunAuthenticationUsername = "endUserUsername"
})},
inputSecrets: []runtime.Object{validBindUserSecret("4242")},
setupMocks: func(conn *mockldapconn.MockConn) {
// Failure during a full auth dry run.
conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1)
conn.EXPECT().Search(gomock.Any()).Return(nil, errors.New("some dry run error")).Times(1)
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{
bindSecretValidTrueCondition(1234),
{
Type: "LDAPConnectionValid",
Status: "False",
LastTransitionTime: now,
Reason: "AuthenticationDryRunError",
Message: fmt.Sprintf(
`failed authentication dry run for end user "%s": error searching for user "%s": some dry run error`,
"endUserUsername", "endUserUsername"),
ObservedGeneration: 1234,
},
tlsConfigurationValidLoadedTrueCondition(1234),
},
},
}},
},
{
name: "when DryRunAuthenticationUsername is specified and the dry run authentication returns unauthenticated without an error",
inputUpstreams: []runtime.Object{editedValidUpstream(func(upstream *v1alpha1.LDAPIdentityProvider) {
upstream.Spec.DryRunAuthenticationUsername = "endUserUsername"
})},
inputSecrets: []runtime.Object{validBindUserSecret("4242")},
setupMocks: func(conn *mockldapconn.MockConn) {
// Failure during full auth dry run which will cause it to return unauthenticated instead of error.
conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1)
conn.EXPECT().Search(gomock.Any()).Return(&ldap.SearchResult{
// No search results means the user did not enter a valid username, which is unauthenticated instead of error.
Entries: []*ldap.Entry{},
}, nil).Times(1)
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{
bindSecretValidTrueCondition(1234),
{
Type: "LDAPConnectionValid",
Status: "False",
LastTransitionTime: now,
Reason: "AuthenticationDryRunError",
Message: fmt.Sprintf(
`failed authentication dry run for end user "%s": user not found`,
"endUserUsername"),
ObservedGeneration: 1234,
},
tlsConfigurationValidLoadedTrueCondition(1234),
},
},
}},
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {