mirror of
https://github.com/vmware-tanzu/pinniped.git
synced 2026-09-19 06:31:47 +00:00
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:
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user