From cb4b63f8b356442d255897b98d906ae50df2894d Mon Sep 17 00:00:00 2001 From: Ashish Amarnath Date: Wed, 10 Jul 2024 00:15:16 -0700 Subject: [PATCH] integration tests for concierge authenticators Signed-off-by: Ashish Amarnath --- .../webhookcachefiller/webhookcachefiller.go | 26 +++- .../webhookcachefiller_test.go | 1 + .../controllermanager/prepare_controllers.go | 1 + test/integration/concierge_client_test.go | 123 ++++++++++++++---- 4 files changed, 122 insertions(+), 29 deletions(-) diff --git a/internal/controller/authenticator/webhookcachefiller/webhookcachefiller.go b/internal/controller/authenticator/webhookcachefiller/webhookcachefiller.go index ef13bef0d..f3615eae3 100644 --- a/internal/controller/authenticator/webhookcachefiller/webhookcachefiller.go +++ b/internal/controller/authenticator/webhookcachefiller/webhookcachefiller.go @@ -14,6 +14,7 @@ import ( "time" k8sauthv1beta1 "k8s.io/api/authentication/v1beta1" + corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/equality" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -66,6 +67,7 @@ type cachedWebhookAuthenticator struct { // New instantiates a new controllerlib.Controller which will populate the provided authncache.Cache. func New( + namespace string, cache *authncache.Cache, client conciergeclientset.Interface, webhooks authinformers.WebhookAuthenticatorInformer, @@ -78,6 +80,7 @@ func New( controllerlib.Config{ Name: controllerName, Syncer: &webhookCacheFillerController{ + namespace: namespace, cache: cache, client: client, webhooks: webhooks, @@ -92,11 +95,28 @@ func New( pinnipedcontroller.MatchAnythingFilter(nil), // nil parent func is fine because each event is distinct controllerlib.InformerOption{}, ), + controllerlib.WithInformer( + secretInformer, + pinnipedcontroller.MatchAnySecretOfTypesFilter( + []corev1.SecretType{ + corev1.SecretTypeOpaque, + corev1.SecretTypeTLS, + }, + pinnipedcontroller.SingletonQueue(), + ), // nil parent func is fine because each event is distinct + controllerlib.InformerOption{}, + ), + controllerlib.WithInformer( + configMapInformer, + pinnipedcontroller.MatchAnythingFilter(pinnipedcontroller.SingletonQueue()), + controllerlib.InformerOption{}, + ), ) } type webhookCacheFillerController struct { cache *authncache.Cache + namespace string webhooks authinformers.WebhookAuthenticatorInformer secretInformer corev1informers.SecretInformer configMapInformer corev1informers.ConfigMapInformer @@ -144,7 +164,7 @@ func (c *webhookCacheFillerController) Sync(ctx controllerlib.Context) error { conditions := make([]*metav1.Condition, 0) var errs []error - certPool, pemBytes, conditions, tlsBundleOk := c.validateTLSBundle(obj.Spec.TLS, obj.Namespace, conditions) + certPool, pemBytes, conditions, tlsBundleOk := c.validateTLSBundle(obj.Spec.TLS, conditions) endpointHostPort, conditions, endpointOk := c.validateEndpoint(obj.Spec.Endpoint, conditions) okSoFar := tlsBundleOk && endpointOk @@ -320,11 +340,11 @@ func (c *webhookCacheFillerController) validateConnection(certPool *x509.CertPoo return conditions, nil } -func (c *webhookCacheFillerController) validateTLSBundle(tlsSpec *authenticationv1alpha1.TLSSpec, namespace string, conditions []*metav1.Condition) (*x509.CertPool, []byte, []*metav1.Condition, bool) { +func (c *webhookCacheFillerController) validateTLSBundle(tlsSpec *authenticationv1alpha1.TLSSpec, conditions []*metav1.Condition) (*x509.CertPool, []byte, []*metav1.Condition, bool) { condition, pemBytes, rootCAs, _ := tlsconfigutil.ValidateTLSConfig( tlsconfigutil.TlsSpecForConcierge(tlsSpec), "spec.tls", - namespace, + c.namespace, c.secretInformer, c.configMapInformer) diff --git a/internal/controller/authenticator/webhookcachefiller/webhookcachefiller_test.go b/internal/controller/authenticator/webhookcachefiller/webhookcachefiller_test.go index bc6157adb..49ab602e3 100644 --- a/internal/controller/authenticator/webhookcachefiller/webhookcachefiller_test.go +++ b/internal/controller/authenticator/webhookcachefiller/webhookcachefiller_test.go @@ -1501,6 +1501,7 @@ func TestController(t *testing.T) { } controller := New( + "concierge", // namespace for controller cache, pinnipedAPIClient, informers.Authentication().V1alpha1().WebhookAuthenticators(), diff --git a/internal/controllermanager/prepare_controllers.go b/internal/controllermanager/prepare_controllers.go index 7bb059edc..7dc14c629 100644 --- a/internal/controllermanager/prepare_controllers.go +++ b/internal/controllermanager/prepare_controllers.go @@ -235,6 +235,7 @@ func PrepareControllers(c *Config) (controllerinit.RunnerBuilder, error) { //nol // authenticators up to date. WithController( webhookcachefiller.New( + c.ServerInstallationInfo.Namespace, c.AuthenticatorCache, client.PinnipedConcierge, informers.pinniped.Authentication().V1alpha1().WebhookAuthenticators(), diff --git a/test/integration/concierge_client_test.go b/test/integration/concierge_client_test.go index 7ea58ee97..2d6eb428a 100644 --- a/test/integration/concierge_client_test.go +++ b/test/integration/concierge_client_test.go @@ -5,11 +5,13 @@ package integration import ( "context" + "encoding/base64" "strings" "testing" "time" "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" authenticationv1alpha1 "go.pinniped.dev/generated/latest/apis/concierge/authentication/v1alpha1" "go.pinniped.dev/internal/here" @@ -59,34 +61,103 @@ func TestClient(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), time.Minute) defer cancel() - webhook := testlib.CreateTestWebhookAuthenticator(ctx, t, &testlib.IntegrationEnv(t).TestWebhook, authenticationv1alpha1.WebhookAuthenticatorPhaseReady) - - // Use an invalid certificate/key to validate that the ServerVersion API fails like we assume. - invalidClient := testlib.NewClientsetWithCertAndKey(t, testCert, testKey) - _, err := invalidClient.Discovery().ServerVersion() - require.EqualError(t, err, "the server has asked for the client to provide credentials") - - // Using the CA bundle and host from the current (admin) kubeconfig, do the token exchange. - clientConfig := testlib.NewClientConfig(t) - client, err := conciergeclient.New( - conciergeclient.WithCABundle(string(clientConfig.CAData)), - conciergeclient.WithEndpoint(clientConfig.Host), - conciergeclient.WithAuthenticator("webhook", webhook.Name), - conciergeclient.WithAPIGroupSuffix(env.APIGroupSuffix), - ) + defaultWebhook := &testlib.IntegrationEnv(t).TestWebhook + TLSCABundle, err := base64.StdEncoding.DecodeString(env.TestWebhook.TLS.CertificateAuthorityData) require.NoError(t, err) - testlib.RequireEventually(t, func(requireEventually *require.Assertions) { - resp, err := client.ExchangeToken(ctx, env.TestUser.Token) - requireEventually.NoError(err) - requireEventually.NotNil(resp.Status.ExpirationTimestamp) - requireEventually.InDelta(5*time.Minute, time.Until(resp.Status.ExpirationTimestamp.Time), float64(time.Minute)) + tests := []struct { + name string + edit func(t *testing.T, spec *authenticationv1alpha1.WebhookAuthenticatorSpec) + }{ + { + name: "default webhook authenticator", + edit: nil, + }, + { + name: "webhook authenticator with secret of type TLS to source ca bundle", + edit: func(t *testing.T, spec *authenticationv1alpha1.WebhookAuthenticatorSpec) { + caSecret := testlib.CreateTestSecret(t, env.ConciergeNamespace, "ca-cert", corev1.SecretTypeTLS, + map[string]string{ + "ca.crt": string(TLSCABundle), + "tls.crt": "", + "tls.key": "", + }) + spec.TLS.CertificateAuthorityData = "" + spec.TLS.CertificateAuthorityDataSource = &authenticationv1alpha1.CABundleSource{ + Kind: "Secret", + Name: caSecret.Name, + Key: "ca.crt", + } + }, + }, + { + name: "webhook authenticator with secret of type opaque to source ca bundle", + edit: func(t *testing.T, spec *authenticationv1alpha1.WebhookAuthenticatorSpec) { + caSecret := testlib.CreateTestSecret(t, env.ConciergeNamespace, "ca-cert", corev1.SecretTypeOpaque, + map[string]string{ + "ca.crt": string(TLSCABundle), + }) + spec.TLS.CertificateAuthorityData = "" + spec.TLS.CertificateAuthorityDataSource = &authenticationv1alpha1.CABundleSource{ + Kind: "Secret", + Name: caSecret.Name, + Key: "ca.crt", + } + }, + }, + { + name: "webhook authenticator with configmap to source ca bundle", + edit: func(t *testing.T, spec *authenticationv1alpha1.WebhookAuthenticatorSpec) { + caConfigmap := testlib.CreateTestConfigMap(t, env.ConciergeNamespace, "ca-cert", + map[string]string{ + "ca.crt": string(TLSCABundle), + }) + spec.TLS.CertificateAuthorityData = "" + spec.TLS.CertificateAuthorityDataSource = &authenticationv1alpha1.CABundleSource{ + Kind: "ConfigMap", + Name: caConfigmap.Name, + Key: "ca.crt", + } + }, + }, + } - // Create a client using the certificate and key returned by the token exchange. - validClient := testlib.NewClientsetWithCertAndKey(t, resp.Status.ClientCertificateData, resp.Status.ClientKeyData) + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + webhookSpec := defaultWebhook.DeepCopy() + if test.edit != nil { + test.edit(t, webhookSpec) + } + webhook := testlib.CreateTestWebhookAuthenticator(ctx, t, webhookSpec, authenticationv1alpha1.WebhookAuthenticatorPhaseReady) - // Make a version request, which should succeed even without any authorization. - _, err = validClient.Discovery().ServerVersion() - requireEventually.NoError(err) - }, 10*time.Second, 500*time.Millisecond) + // Use an invalid certificate/key to validate that the ServerVersion API fails like we assume. + invalidClient := testlib.NewClientsetWithCertAndKey(t, testCert, testKey) + _, err := invalidClient.Discovery().ServerVersion() + require.EqualError(t, err, "the server has asked for the client to provide credentials") + + // Using the CA bundle and host from the current (admin) kubeconfig, do the token exchange. + clientConfig := testlib.NewClientConfig(t) + client, err := conciergeclient.New( + conciergeclient.WithCABundle(string(clientConfig.CAData)), + conciergeclient.WithEndpoint(clientConfig.Host), + conciergeclient.WithAuthenticator("webhook", webhook.Name), + conciergeclient.WithAPIGroupSuffix(env.APIGroupSuffix), + ) + require.NoError(t, err) + + testlib.RequireEventually(t, func(requireEventually *require.Assertions) { + resp, err := client.ExchangeToken(ctx, env.TestUser.Token) + requireEventually.NoError(err) + requireEventually.NotNil(resp.Status.ExpirationTimestamp) + requireEventually.InDelta(5*time.Minute, time.Until(resp.Status.ExpirationTimestamp.Time), float64(time.Minute)) + + // Create a client using the certificate and key returned by the token exchange. + validClient := testlib.NewClientsetWithCertAndKey(t, resp.Status.ClientCertificateData, resp.Status.ClientKeyData) + + // Make a version request, which should succeed even without any authorization. + _, err = validClient.Discovery().ServerVersion() + requireEventually.NoError(err) + }, 10*time.Second, 500*time.Millisecond) + }) + } }