Add integration tests to ensure that LDAP/AD conditions with status Unknown if they cannot be validated

Co-authored-by: Ryan Richard <richardry@vmware.com>
This commit is contained in:
Joshua Casey
2024-08-06 16:28:13 -05:00
committed by Ryan Richard
parent 6b49cd7d28
commit f918edd846
5 changed files with 463 additions and 22 deletions

View File

@@ -49,10 +49,10 @@ func TestLDAPSearch_Parallel(t *testing.T) {
ldapsLocalhostPort := localhostPorts[1]
unusedLocalhostPort := localhostPorts[2]
// Expose the the test LDAP server's TLS port on the localhost.
// Expose the test LDAP server's TLS port on the localhost.
startKubectlPortForward(ctx, t, ldapsLocalhostPort, "ldaps", "ldap", env.ToolsNamespace)
// Expose the the test LDAP server's StartTLS port on the localhost.
// Expose the test LDAP server's StartTLS port on the localhost.
startKubectlPortForward(ctx, t, ldapLocalhostPort, "ldap", "ldap", env.ToolsNamespace)
providerConfig := func(editFunc func(p *upstreamldap.ProviderConfig)) *upstreamldap.ProviderConfig {
@@ -853,11 +853,11 @@ func startKubectlPortForward(ctx context.Context, t *testing.T, hostPort, remote
func findRecentlyUnusedLocalhostPorts(t *testing.T, howManyPorts int) []string {
t.Helper()
listeners := []net.Listener{}
for range howManyPorts {
unusedPortGrabbingListener, err := net.Listen("tcp", "127.0.0.1:0")
listeners := make([]net.Listener, howManyPorts)
for i := range howManyPorts {
var err error
listeners[i], err = net.Listen("tcp", "127.0.0.1:0")
require.NoError(t, err)
listeners = append(listeners, unusedPortGrabbingListener)
}
ports := make([]string, len(listeners))

View File

@@ -0,0 +1,193 @@
// Copyright 2024 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package integration
import (
"context"
"encoding/base64"
"fmt"
"testing"
"time"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
idpv1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/idp/v1alpha1"
"go.pinniped.dev/test/testlib"
)
func TestActiveDirectoryIDPPhaseAndConditions_Parallel(t *testing.T) {
env := testlib.IntegrationEnv(t)
testlib.SkipTestWhenActiveDirectoryIsUnavailable(t, env)
supervisorNamespace := testlib.IntegrationEnv(t).SupervisorNamespace
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute)
t.Cleanup(cancel)
adIDPClient := testlib.NewSupervisorClientset(t).IDPV1alpha1().ActiveDirectoryIdentityProviders(supervisorNamespace)
bindSecret := testlib.CreateTestSecret(
t,
env.SupervisorNamespace,
"ad-bind-secret",
corev1.SecretTypeBasicAuth,
map[string]string{
corev1.BasicAuthUsernameKey: env.SupervisorUpstreamActiveDirectory.BindUsername,
corev1.BasicAuthPasswordKey: env.SupervisorUpstreamActiveDirectory.BindPassword,
},
)
happySpec := idpv1alpha1.ActiveDirectoryIdentityProviderSpec{
Host: env.SupervisorUpstreamActiveDirectory.Host,
Bind: idpv1alpha1.ActiveDirectoryIdentityProviderBind{
SecretName: bindSecret.Name,
},
TLS: &idpv1alpha1.TLSSpec{
CertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte(env.SupervisorUpstreamActiveDirectory.CABundle)),
},
UserSearch: idpv1alpha1.ActiveDirectoryIdentityProviderUserSearch{
Base: env.SupervisorUpstreamActiveDirectory.UserSearchBase,
Filter: "",
Attributes: idpv1alpha1.ActiveDirectoryIdentityProviderUserSearchAttributes{
Username: env.SupervisorUpstreamActiveDirectory.TestUserMailAttributeName,
UID: env.SupervisorUpstreamActiveDirectory.TestUserUniqueIDAttributeName,
},
},
GroupSearch: idpv1alpha1.ActiveDirectoryIdentityProviderGroupSearch{
Base: env.SupervisorUpstreamActiveDirectory.GroupSearchBase,
Filter: "", // use the default value of "member={}"
Attributes: idpv1alpha1.ActiveDirectoryIdentityProviderGroupSearchAttributes{
GroupName: "", // use the default value of "dn"
},
},
}
tests := []struct {
name string
adSpec idpv1alpha1.ActiveDirectoryIdentityProviderSpec
wantPhase idpv1alpha1.ActiveDirectoryIdentityProviderPhase
wantConditions []*metav1.Condition
}{
{
name: "Happy Path",
adSpec: happySpec,
wantPhase: idpv1alpha1.ActiveDirectoryPhaseReady,
wantConditions: []*metav1.Condition{
{
Type: "BindSecretValid",
Status: "True",
Reason: "Success",
Message: "loaded bind secret",
},
{
Type: "LDAPConnectionValid",
Status: "True",
Reason: "Success",
Message: fmt.Sprintf(
`successfully able to connect to %q and bind as user %q [validated with Secret %q at version %q]`,
env.SupervisorUpstreamActiveDirectory.Host,
env.SupervisorUpstreamActiveDirectory.BindUsername,
bindSecret.Name,
bindSecret.ResourceVersion),
},
{
Type: "SearchBaseFound",
Status: "True",
Reason: "UsingConfigurationFromSpec",
Message: "Using search base from ActiveDirectoryIdentityProvider config.",
},
{
Type: "TLSConfigurationValid",
Status: "True",
Reason: "Success",
Message: "spec.tls is valid: using configured CA bundle",
},
},
},
{
name: "CA bundle is invalid yields conditions TLSConfigurationValid with status 'False' and LDAPConnectionValid with status 'Unknown'",
adSpec: func() idpv1alpha1.ActiveDirectoryIdentityProviderSpec {
temp := happySpec.DeepCopy()
temp.TLS.CertificateAuthorityData = "this-is-not-base64-encoded"
return *temp
}(),
wantPhase: idpv1alpha1.ActiveDirectoryPhaseError,
wantConditions: []*metav1.Condition{
{
Type: "BindSecretValid",
Status: "True",
Reason: "Success",
Message: "loaded bind secret",
},
{
Type: "LDAPConnectionValid",
Status: "Unknown",
Reason: "UnableToValidate",
Message: "unable to validate; see other conditions for details",
},
{
Type: "SearchBaseFound",
Status: "Unknown",
Reason: "UnableToValidate",
Message: "unable to validate; see other conditions for details",
},
{
Type: "TLSConfigurationValid",
Status: "False",
Reason: "InvalidTLSConfig",
Message: "spec.tls.certificateAuthorityData is invalid: illegal base64 data at input byte 4",
},
},
},
{
name: "Bind secret not found yields conditions BindSecretValid with status 'False' and LDAPConnectionValid with status 'Unknown'",
adSpec: func() idpv1alpha1.ActiveDirectoryIdentityProviderSpec {
temp := happySpec.DeepCopy()
temp.Bind.SecretName = "this-secret-does-not-exist"
return *temp
}(),
wantPhase: idpv1alpha1.ActiveDirectoryPhaseError,
wantConditions: []*metav1.Condition{
{
Type: "BindSecretValid",
Status: "False",
Reason: "SecretNotFound",
Message: `secret "this-secret-does-not-exist" not found`,
},
{
Type: "LDAPConnectionValid",
Status: "Unknown",
Reason: "UnableToValidate",
Message: "unable to validate; see other conditions for details",
},
{
Type: "SearchBaseFound",
Status: "Unknown",
Reason: "UnableToValidate",
Message: "unable to validate; see other conditions for details",
},
{
Type: "TLSConfigurationValid",
Status: "True",
Reason: "Success",
Message: "spec.tls is valid: using configured CA bundle",
},
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
t.Parallel()
idp := testlib.CreateTestActiveDirectoryIdentityProvider(t, test.adSpec, test.wantPhase)
testlib.WaitForActiveDirectoryIdentityProviderStatusConditions(
ctx,
t,
adIDPClient,
idp.Name,
test.wantConditions,
)
})
}
}

View File

@@ -24,7 +24,7 @@ import (
"go.pinniped.dev/test/testlib"
)
const generateNamePrefix = "integration-test-github-idp-"
const generateGitHubNamePrefix = "integration-test-github-idp-"
func TestGitHubIDPStaticValidationOnCreate_Parallel(t *testing.T) {
adminClient := testlib.NewKubernetesClientset(t)
@@ -37,7 +37,7 @@ func TestGitHubIDPStaticValidationOnCreate_Parallel(t *testing.T) {
ns, err := namespaceClient.Create(ctx, &corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
GenerateName: generateNamePrefix,
GenerateName: generateGitHubNamePrefix,
},
}, metav1.CreateOptions{})
require.NoError(t, err)
@@ -240,7 +240,7 @@ func TestGitHubIDPStaticValidationOnCreate_Parallel(t *testing.T) {
input := &idpv1alpha1.GitHubIdentityProvider{
ObjectMeta: metav1.ObjectMeta{
GenerateName: generateNamePrefix,
GenerateName: generateGitHubNamePrefix,
},
Spec: tt.inputSpec,
}
@@ -268,7 +268,7 @@ func TestGitHubIDPSetsDefaultsWithKubectl_Parallel(t *testing.T) {
ns, err := namespaceClient.Create(ctx, &corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
GenerateName: generateNamePrefix,
GenerateName: generateGitHubNamePrefix,
},
}, metav1.CreateOptions{})
require.NoError(t, err)
@@ -278,7 +278,7 @@ func TestGitHubIDPSetsDefaultsWithKubectl_Parallel(t *testing.T) {
})
t.Logf("Created namespace %s", ns.Name)
idpName := generateNamePrefix + testlib.RandHex(t, 16)
idpName := generateGitHubNamePrefix + testlib.RandHex(t, 16)
githubIDPYaml := []byte(here.Doc(fmt.Sprintf(`
---
@@ -336,8 +336,8 @@ func TestGitHubIDPPhaseAndConditions_Parallel(t *testing.T) {
secretsClient := kubernetesClient.CoreV1().Secrets(supervisorNamespace)
gitHubIDPClient := testlib.NewSupervisorClientset(t).IDPV1alpha1().GitHubIdentityProviders(supervisorNamespace)
happySecretName := generateNamePrefix + testlib.RandHex(t, 16)
invalidSecretName := generateNamePrefix + testlib.RandHex(t, 16)
happySecretName := generateGitHubNamePrefix + testlib.RandHex(t, 16)
invalidSecretName := generateGitHubNamePrefix + testlib.RandHex(t, 16)
tests := []struct {
name string
@@ -490,7 +490,7 @@ func TestGitHubIDPPhaseAndConditions_Parallel(t *testing.T) {
var secretName string
for _, secret := range tt.secrets {
secret.GenerateName = generateNamePrefix
secret.GenerateName = generateGitHubNamePrefix
created, err := secretsClient.Create(ctx, secret, metav1.CreateOptions{})
require.NoError(t, err)
@@ -505,7 +505,7 @@ func TestGitHubIDPPhaseAndConditions_Parallel(t *testing.T) {
for _, idp := range tt.idps {
idp.Name = ""
idp.GenerateName = generateNamePrefix
idp.GenerateName = generateGitHubNamePrefix
idp.Spec.Client.SecretName = secretName
created, err := gitHubIDPClient.Create(ctx, idp, metav1.CreateOptions{})
@@ -532,7 +532,7 @@ func TestGitHubIDPInWrongNamespace_Parallel(t *testing.T) {
namespaceClient := kubernetesClient.CoreV1().Namespaces()
otherNamespace, err := namespaceClient.Create(ctx, &corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
GenerateName: generateNamePrefix,
GenerateName: generateGitHubNamePrefix,
},
}, metav1.CreateOptions{})
require.NoError(t, err)
@@ -545,7 +545,7 @@ func TestGitHubIDPInWrongNamespace_Parallel(t *testing.T) {
idp := &idpv1alpha1.GitHubIdentityProvider{
ObjectMeta: metav1.ObjectMeta{
GenerateName: generateNamePrefix,
GenerateName: generateGitHubNamePrefix,
Namespace: otherNamespace.Name,
},
Spec: idpv1alpha1.GitHubIdentityProviderSpec{
@@ -592,7 +592,7 @@ func TestGitHubIDPSecretInOtherNamespace_Parallel(t *testing.T) {
namespaceClient := kubernetesClient.CoreV1().Namespaces()
otherNamespace, err := namespaceClient.Create(ctx, &corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
GenerateName: generateNamePrefix,
GenerateName: generateGitHubNamePrefix,
},
}, metav1.CreateOptions{})
require.NoError(t, err)
@@ -605,7 +605,7 @@ func TestGitHubIDPSecretInOtherNamespace_Parallel(t *testing.T) {
secret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
GenerateName: generateNamePrefix,
GenerateName: generateGitHubNamePrefix,
Namespace: otherNamespace.Name,
},
Type: "secrets.pinniped.dev/github-client",
@@ -621,7 +621,7 @@ func TestGitHubIDPSecretInOtherNamespace_Parallel(t *testing.T) {
idp := &idpv1alpha1.GitHubIdentityProvider{
ObjectMeta: metav1.ObjectMeta{
GenerateName: generateNamePrefix,
GenerateName: generateGitHubNamePrefix,
Namespace: supervisorNamespace,
},
Spec: idpv1alpha1.GitHubIdentityProviderSpec{
@@ -701,7 +701,7 @@ func TestGitHubIDPTooManyOrganizationsStaticValidationOnCreate_Parallel(t *testi
ns, err := namespaceClient.Create(ctx, &corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
GenerateName: generateNamePrefix,
GenerateName: generateGitHubNamePrefix,
},
}, metav1.CreateOptions{})
require.NoError(t, err)
@@ -714,7 +714,7 @@ func TestGitHubIDPTooManyOrganizationsStaticValidationOnCreate_Parallel(t *testi
input := &idpv1alpha1.GitHubIdentityProvider{
ObjectMeta: metav1.ObjectMeta{
GenerateName: generateNamePrefix,
GenerateName: generateGitHubNamePrefix,
},
Spec: idpv1alpha1.GitHubIdentityProviderSpec{
AllowAuthentication: idpv1alpha1.GitHubAllowAuthenticationSpec{

View File

@@ -0,0 +1,174 @@
// Copyright 2024 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package integration
import (
"context"
"encoding/base64"
"fmt"
"testing"
"time"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
idpv1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/idp/v1alpha1"
"go.pinniped.dev/test/testlib"
)
func TestLDAPIDPPhaseAndConditions_Parallel(t *testing.T) {
env := testlib.IntegrationEnv(t)
supervisorNamespace := testlib.IntegrationEnv(t).SupervisorNamespace
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute)
t.Cleanup(cancel)
ldapIDPClient := testlib.NewSupervisorClientset(t).IDPV1alpha1().LDAPIdentityProviders(supervisorNamespace)
bindSecret := testlib.CreateTestSecret(
t,
env.SupervisorNamespace,
"ldap-bind-secret",
corev1.SecretTypeBasicAuth,
map[string]string{
corev1.BasicAuthUsernameKey: env.SupervisorUpstreamLDAP.BindUsername,
corev1.BasicAuthPasswordKey: env.SupervisorUpstreamLDAP.BindPassword,
},
)
happySpec := idpv1alpha1.LDAPIdentityProviderSpec{
Host: env.SupervisorUpstreamLDAP.Host,
Bind: idpv1alpha1.LDAPIdentityProviderBind{
SecretName: bindSecret.Name,
},
TLS: &idpv1alpha1.TLSSpec{
CertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte(env.SupervisorUpstreamLDAP.CABundle)),
},
UserSearch: idpv1alpha1.LDAPIdentityProviderUserSearch{
Base: env.SupervisorUpstreamLDAP.UserSearchBase,
Filter: "",
Attributes: idpv1alpha1.LDAPIdentityProviderUserSearchAttributes{
Username: env.SupervisorUpstreamLDAP.TestUserMailAttributeName,
UID: env.SupervisorUpstreamLDAP.TestUserUniqueIDAttributeName,
},
},
GroupSearch: idpv1alpha1.LDAPIdentityProviderGroupSearch{
Base: env.SupervisorUpstreamLDAP.GroupSearchBase,
Filter: "", // use the default value of "member={}"
Attributes: idpv1alpha1.LDAPIdentityProviderGroupSearchAttributes{
GroupName: "", // use the default value of "dn"
},
},
}
tests := []struct {
name string
ldapSpec idpv1alpha1.LDAPIdentityProviderSpec
wantPhase idpv1alpha1.LDAPIdentityProviderPhase
wantConditions []*metav1.Condition
}{
{
name: "Happy Path",
ldapSpec: happySpec,
wantPhase: idpv1alpha1.LDAPPhaseReady,
wantConditions: []*metav1.Condition{
{
Type: "BindSecretValid",
Status: "True",
Reason: "Success",
Message: "loaded bind secret",
},
{
Type: "LDAPConnectionValid",
Status: "True",
Reason: "Success",
Message: fmt.Sprintf(
`successfully able to connect to %q and bind as user %q [validated with Secret %q at version %q]`,
env.SupervisorUpstreamLDAP.Host,
env.SupervisorUpstreamLDAP.BindUsername,
bindSecret.Name,
bindSecret.ResourceVersion),
},
{
Type: "TLSConfigurationValid",
Status: "True",
Reason: "Success",
Message: "spec.tls is valid: using configured CA bundle",
},
},
},
{
name: "CA bundle is invalid yields conditions TLSConfigurationValid with status 'False' and LDAPConnectionValid/SearchBaseFound with status 'Unknown'",
ldapSpec: func() idpv1alpha1.LDAPIdentityProviderSpec {
temp := happySpec.DeepCopy()
temp.TLS.CertificateAuthorityData = "this-is-not-base64-encoded"
return *temp
}(),
wantPhase: idpv1alpha1.LDAPPhaseError,
wantConditions: []*metav1.Condition{
{
Type: "BindSecretValid",
Status: "True",
Reason: "Success",
Message: "loaded bind secret",
},
{
Type: "LDAPConnectionValid",
Status: "Unknown",
Reason: "UnableToValidate",
Message: "unable to validate; see other conditions for details",
},
{
Type: "TLSConfigurationValid",
Status: "False",
Reason: "InvalidTLSConfig",
Message: "spec.tls.certificateAuthorityData is invalid: illegal base64 data at input byte 4",
},
},
},
{
name: "Bind secret not found yields conditions BindSecretValid with status 'False' and LDAPConnectionValid/SearchBaseFound with status 'Unknown'",
ldapSpec: func() idpv1alpha1.LDAPIdentityProviderSpec {
temp := happySpec.DeepCopy()
temp.Bind.SecretName = "this-secret-does-not-exist"
return *temp
}(),
wantPhase: idpv1alpha1.LDAPPhaseError,
wantConditions: []*metav1.Condition{
{
Type: "BindSecretValid",
Status: "False",
Reason: "SecretNotFound",
Message: `secret "this-secret-does-not-exist" not found`,
},
{
Type: "LDAPConnectionValid",
Status: "Unknown",
Reason: "UnableToValidate",
Message: "unable to validate; see other conditions for details",
},
{
Type: "TLSConfigurationValid",
Status: "True",
Reason: "Success",
Message: "spec.tls is valid: using configured CA bundle",
},
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
t.Parallel()
idp := testlib.CreateTestLDAPIdentityProvider(t, test.ldapSpec, test.wantPhase)
testlib.WaitForLDAPIdentityProviderStatusConditions(
ctx,
t,
ldapIDPClient,
idp.Name,
test.wantConditions,
)
})
}
}

View File

@@ -1000,6 +1000,80 @@ func WaitForGitHubIdentityProviderStatusConditions(
}, 60*time.Second, 1*time.Second, "wanted conditions for GitHubIdentityProvider %q", gitHubIDPName)
}
func WaitForLDAPIdentityProviderStatusConditions(
ctx context.Context,
t *testing.T,
client alpha1.LDAPIdentityProviderInterface,
ldapIDPName string,
expectConditions []*metav1.Condition,
) {
t.Helper()
RequireEventuallyf(t, func(requireEventually *require.Assertions) {
idp, err := client.Get(ctx, ldapIDPName, metav1.GetOptions{})
requireEventually.NoError(err)
actualConditions := make([]*metav1.Condition, len(idp.Status.Conditions))
for i, c := range idp.Status.Conditions {
actualConditions[i] = c.DeepCopy()
}
requireEventually.Lenf(actualConditions, len(expectConditions),
"wanted status conditions: %#v", expectConditions)
for i, wantCond := range expectConditions {
actualCond := actualConditions[i]
// This is a cheat to avoid needing to make equality assertions on these fields.
requireEventually.NotZero(actualCond.LastTransitionTime)
wantCond.LastTransitionTime = actualCond.LastTransitionTime
requireEventually.NotZero(actualCond.ObservedGeneration)
wantCond.ObservedGeneration = actualCond.ObservedGeneration
requireEventually.Equalf(wantCond, actualCond,
"wanted status conditions: %#v\nactual status conditions were: %#v\nnot equal at index %d",
expectConditions, &actualConditions, i)
}
}, 60*time.Second, 1*time.Second, "wanted conditions for LDAPIdentityProvider %q", ldapIDPName)
}
func WaitForActiveDirectoryIdentityProviderStatusConditions(
ctx context.Context,
t *testing.T,
client alpha1.ActiveDirectoryIdentityProviderInterface,
activeDirectoryIDPName string,
expectConditions []*metav1.Condition,
) {
t.Helper()
RequireEventuallyf(t, func(requireEventually *require.Assertions) {
idp, err := client.Get(ctx, activeDirectoryIDPName, metav1.GetOptions{})
requireEventually.NoError(err)
actualConditions := make([]*metav1.Condition, len(idp.Status.Conditions))
for i, c := range idp.Status.Conditions {
actualConditions[i] = c.DeepCopy()
}
requireEventually.Lenf(actualConditions, len(expectConditions),
"wanted status conditions: %#v", expectConditions)
for i, wantCond := range expectConditions {
actualCond := actualConditions[i]
// This is a cheat to avoid needing to make equality assertions on these fields.
requireEventually.NotZero(actualCond.LastTransitionTime)
wantCond.LastTransitionTime = actualCond.LastTransitionTime
requireEventually.NotZero(actualCond.ObservedGeneration)
wantCond.ObservedGeneration = actualCond.ObservedGeneration
requireEventually.Equalf(wantCond, actualCond,
"wanted status conditions: %#v\nactual status conditions were: %#v\nnot equal at index %d",
expectConditions, &actualConditions, i)
}
}, 60*time.Second, 1*time.Second, "wanted conditions for ActiveDirectoryIdentityProvider %q", activeDirectoryIDPName)
}
func TestObjectMeta(t *testing.T, baseName string) metav1.ObjectMeta {
return metav1.ObjectMeta{
GenerateName: fmt.Sprintf("test-%s-", baseName),