diff --git a/internal/controller/authenticator/webhookcachefiller/webhookcachefiller.go b/internal/controller/authenticator/webhookcachefiller/webhookcachefiller.go index b3344914e..592e04e9b 100644 --- a/internal/controller/authenticator/webhookcachefiller/webhookcachefiller.go +++ b/internal/controller/authenticator/webhookcachefiller/webhookcachefiller.go @@ -39,24 +39,24 @@ import ( ) const ( - controllerName = "webhookcachefiller-controller" - typeReady = "Ready" - typeTLSBundleValid = "TLSBundleValid" - typeTLSConnetionNegotiationValid = "TLSConnetionNegotiationValid" - typeEndpointURLValid = "EndpointURLValid" - typeAuthenticatorValid = "AuthenticatorValid" - reasonSuccess = "Success" - reasonNotReady = "NotReady" - reasonUnableToValidate = "UnableToValidate" - reasonUnableToCreateTempFile = "UnableToCreateTempFile" - reasonUnableToMarshallKubeconfig = "UnableToMarshallKubeconfig" - reasonUnableToLoadKubeconfig = "UnableToLoadKubeconfig" - reasonUnableToInstantiateWebhook = "UnableToInstantiateWebhook" - reasonInvalidTLSConfiguration = "InvalidTLSConfiguration" - reasonInvalidEndpointURL = "InvalidEndpointURL" - reasonInvalidEndpointURLScheme = "InvalidEndpointURLScheme" - reasonUnableToDialServer = "UnableToDialServer" - msgUnableToValidate = "unable to validate; see other conditions for details" + controllerName = "webhookcachefiller-controller" + typeReady = "Ready" + typeTLSConfigurationValid = "TLSConfigurationValid" + typeTLSConnectionNegotiationValid = "TLSConnectionNegotiationValid" + typeEndpointURLValid = "EndpointURLValid" + typeAuthenticatorValid = "AuthenticatorValid" + reasonSuccess = "Success" + reasonNotReady = "NotReady" + reasonUnableToValidate = "UnableToValidate" + reasonUnableToCreateTempFile = "UnableToCreateTempFile" + reasonUnableToMarshallKubeconfig = "UnableToMarshallKubeconfig" + reasonUnableToLoadKubeconfig = "UnableToLoadKubeconfig" + reasonUnableToInstantiateWebhook = "UnableToInstantiateWebhook" + reasonInvalidTLSConfiguration = "InvalidTLSConfiguration" + reasonInvalidEndpointURL = "InvalidEndpointURL" + reasonInvalidEndpointURLScheme = "InvalidEndpointURLScheme" + reasonUnableToDialServer = "UnableToDialServer" + msgUnableToValidate = "unable to validate; see other conditions for details" ) // New instantiates a new controllerlib.Controller which will populate the provided authncache.Cache. @@ -281,7 +281,7 @@ func newWebhookAuthenticator( func (c *webhookCacheFillerController) validateTLSNegotiation(certPool *x509.CertPool, endpointURL *url.URL, conditions []*metav1.Condition, prereqOk bool) ([]*metav1.Condition, error) { if !prereqOk { conditions = append(conditions, &metav1.Condition{ - Type: typeTLSConnetionNegotiationValid, + Type: typeTLSConnectionNegotiationValid, Status: metav1.ConditionUnknown, Reason: reasonUnableToValidate, Message: msgUnableToValidate, @@ -307,7 +307,7 @@ func (c *webhookCacheFillerController) validateTLSNegotiation(certPool *x509.Cer errText := "cannot dial server" msg := fmt.Sprintf("%s: %s", errText, dialErr.Error()) conditions = append(conditions, &metav1.Condition{ - Type: typeTLSConnetionNegotiationValid, + Type: typeTLSConnectionNegotiationValid, Status: metav1.ConditionFalse, Reason: reasonUnableToDialServer, Message: msg, @@ -322,7 +322,7 @@ func (c *webhookCacheFillerController) validateTLSNegotiation(certPool *x509.Cer } conditions = append(conditions, &metav1.Condition{ - Type: typeTLSConnetionNegotiationValid, + Type: typeTLSConnectionNegotiationValid, Status: metav1.ConditionTrue, Reason: reasonSuccess, Message: "tls verified", @@ -335,7 +335,7 @@ func (c *webhookCacheFillerController) validateTLSBundle(tlsSpec *auth1alpha1.TL if err != nil { msg := fmt.Sprintf("%s: %s", "invalid TLS configuration", err.Error()) conditions = append(conditions, &metav1.Condition{ - Type: typeTLSBundleValid, + Type: typeTLSConfigurationValid, Status: metav1.ConditionFalse, Reason: reasonInvalidTLSConfiguration, Message: msg, @@ -347,7 +347,7 @@ func (c *webhookCacheFillerController) validateTLSBundle(tlsSpec *auth1alpha1.TL msg = "no CA bundle specified" } conditions = append(conditions, &metav1.Condition{ - Type: typeTLSBundleValid, + Type: typeTLSConfigurationValid, Status: metav1.ConditionTrue, Reason: reasonSuccess, Message: msg, diff --git a/internal/controller/authenticator/webhookcachefiller/webhookcachefiller_test.go b/internal/controller/authenticator/webhookcachefiller/webhookcachefiller_test.go index c18ba7a2e..e21e7b45c 100644 --- a/internal/controller/authenticator/webhookcachefiller/webhookcachefiller_test.go +++ b/internal/controller/authenticator/webhookcachefiller/webhookcachefiller_test.go @@ -240,9 +240,9 @@ func TestController(t *testing.T) { } } - happyTLSBundleValidCAParsed := func(time metav1.Time, observedGeneration int64) metav1.Condition { + happyTLSConfigurationValidCAParsed := func(time metav1.Time, observedGeneration int64) metav1.Condition { return metav1.Condition{ - Type: "TLSBundleValid", + Type: "TLSConfigurationValid", Status: "True", ObservedGeneration: observedGeneration, LastTransitionTime: time, @@ -250,9 +250,9 @@ func TestController(t *testing.T) { Message: "successfully parsed specified CA bundle", } } - happyTLSBundleValidNoCA := func(time metav1.Time, observedGeneration int64) metav1.Condition { + happyTLSConfigurationValidNoCA := func(time metav1.Time, observedGeneration int64) metav1.Condition { return metav1.Condition{ - Type: "TLSBundleValid", + Type: "TLSConfigurationValid", Status: "True", ObservedGeneration: observedGeneration, LastTransitionTime: time, @@ -260,9 +260,9 @@ func TestController(t *testing.T) { Message: "no CA bundle specified", } } - sadTLSBundleValid := func(time metav1.Time, observedGeneration int64) metav1.Condition { + sadTLSConfigurationValid := func(time metav1.Time, observedGeneration int64) metav1.Condition { return metav1.Condition{ - Type: "TLSBundleValid", + Type: "TLSConfigurationValid", Status: "False", ObservedGeneration: observedGeneration, LastTransitionTime: time, @@ -271,9 +271,9 @@ func TestController(t *testing.T) { } } - happyTLSConnetionNegotiationValid := func(time metav1.Time, observedGeneration int64) metav1.Condition { + happyTLSConnectionNegotiationValid := func(time metav1.Time, observedGeneration int64) metav1.Condition { return metav1.Condition{ - Type: "TLSConnetionNegotiationValid", + Type: "TLSConnectionNegotiationValid", Status: "True", ObservedGeneration: observedGeneration, LastTransitionTime: time, @@ -281,9 +281,9 @@ func TestController(t *testing.T) { Message: "tls verified", } } - unknownTLSConnetionNegotiationValid := func(time metav1.Time, observedGeneration int64) metav1.Condition { + unknownTLSConnectionNegotiationValid := func(time metav1.Time, observedGeneration int64) metav1.Condition { return metav1.Condition{ - Type: "TLSConnetionNegotiationValid", + Type: "TLSConnectionNegotiationValid", Status: "Unknown", ObservedGeneration: observedGeneration, LastTransitionTime: time, @@ -291,9 +291,9 @@ func TestController(t *testing.T) { Message: "unable to validate; see other conditions for details", } } - sadTLSConnetionNegotiationValid := func(time metav1.Time, observedGeneration int64) metav1.Condition { + sadTLSConnectionNegotiationValid := func(time metav1.Time, observedGeneration int64) metav1.Condition { return metav1.Condition{ - Type: "TLSConnetionNegotiationValid", + Type: "TLSConnectionNegotiationValid", Status: "False", ObservedGeneration: observedGeneration, LastTransitionTime: time, @@ -301,9 +301,9 @@ func TestController(t *testing.T) { Message: "cannot dial server: tls: failed to verify certificate: x509: certificate signed by unknown authority", } } - sadTLSConnetionNegotiationNoIPSANs := func(time metav1.Time, observedGeneration int64) metav1.Condition { + sadTLSConnectionNegotiationNoIPSANs := func(time metav1.Time, observedGeneration int64) metav1.Condition { return metav1.Condition{ - Type: "TLSConnetionNegotiationValid", + Type: "TLSConnectionNegotiationValid", Status: "False", ObservedGeneration: observedGeneration, LastTransitionTime: time, @@ -345,9 +345,9 @@ func TestController(t *testing.T) { allHappyConditionsSuccess := func(endpoint string, someTime metav1.Time, observedGeneration int64) []metav1.Condition { return conditionstestutil.SortByType([]metav1.Condition{ - happyTLSBundleValidCAParsed(someTime, observedGeneration), + happyTLSConfigurationValidCAParsed(someTime, observedGeneration), happyEndpointURLValid(someTime, observedGeneration), - happyTLSConnetionNegotiationValid(someTime, observedGeneration), + happyTLSConnectionNegotiationValid(someTime, observedGeneration), happyAuthenticatorValid(someTime, observedGeneration), happyReadyCondition(someTime, observedGeneration), }) @@ -551,8 +551,8 @@ func TestController(t *testing.T) { Conditions: conditionstestutil.Replace( allHappyConditionsSuccess(goodEndpoint, frozenMetav1Now, 0), []metav1.Condition{ - happyTLSBundleValidNoCA(frozenMetav1Now, 0), - sadTLSConnetionNegotiationValid(frozenMetav1Now, 0), + happyTLSConfigurationValidNoCA(frozenMetav1Now, 0), + sadTLSConnectionNegotiationValid(frozenMetav1Now, 0), sadReadyCondition(frozenMetav1Now, 0), unknownAuthenticatorValid(frozenMetav1Now, 0), }, @@ -591,8 +591,8 @@ func TestController(t *testing.T) { Conditions: conditionstestutil.Replace( allHappyConditionsSuccess(goodEndpoint, frozenMetav1Now, 0), []metav1.Condition{ - sadTLSBundleValid(frozenMetav1Now, 0), - unknownTLSConnetionNegotiationValid(frozenMetav1Now, 0), + sadTLSConfigurationValid(frozenMetav1Now, 0), + unknownTLSConnectionNegotiationValid(frozenMetav1Now, 0), unknownAuthenticatorValid(frozenMetav1Now, 0), sadReadyCondition(frozenMetav1Now, 0), }, @@ -634,9 +634,9 @@ func TestController(t *testing.T) { Conditions: conditionstestutil.Replace( allHappyConditionsSuccess(goodEndpoint, frozenMetav1Now, 0), []metav1.Condition{ - happyTLSBundleValidNoCA(frozenMetav1Now, 0), + happyTLSConfigurationValidNoCA(frozenMetav1Now, 0), sadEndpointURLValid("https://.café .com/café/café/café/coffee", frozenMetav1Now, 0), - unknownTLSConnetionNegotiationValid(frozenMetav1Now, 0), + unknownTLSConnectionNegotiationValid(frozenMetav1Now, 0), unknownAuthenticatorValid(frozenMetav1Now, 0), sadReadyCondition(frozenMetav1Now, 0), }, @@ -677,9 +677,9 @@ func TestController(t *testing.T) { Conditions: conditionstestutil.Replace( allHappyConditionsSuccess(goodEndpoint, frozenMetav1Now, 0), []metav1.Condition{ - happyTLSBundleValidNoCA(frozenMetav1Now, 0), + happyTLSConfigurationValidNoCA(frozenMetav1Now, 0), sadEndpointURLValidHTTPS("http://localhost", frozenMetav1Now, 0), - unknownTLSConnetionNegotiationValid(frozenMetav1Now, 0), + unknownTLSConnectionNegotiationValid(frozenMetav1Now, 0), unknownAuthenticatorValid(frozenMetav1Now, 0), sadReadyCondition(frozenMetav1Now, 0), }, @@ -720,7 +720,7 @@ func TestController(t *testing.T) { []metav1.Condition{ unknownAuthenticatorValid(frozenMetav1Now, 0), sadReadyCondition(frozenMetav1Now, 0), - sadTLSConnetionNegotiationValid(frozenMetav1Now, 0), + sadTLSConnectionNegotiationValid(frozenMetav1Now, 0), }, ), Phase: "Error", @@ -878,7 +878,7 @@ func TestController(t *testing.T) { Conditions: conditionstestutil.Replace( allHappyConditionsSuccess(localWithExampleDotComCertServer.URL, frozenMetav1Now, 0), []metav1.Condition{ - sadTLSConnetionNegotiationNoIPSANs(frozenMetav1Now, 0), + sadTLSConnectionNegotiationNoIPSANs(frozenMetav1Now, 0), unknownAuthenticatorValid(frozenMetav1Now, 0), sadReadyCondition(frozenMetav1Now, 0), }, diff --git a/internal/testutil/conciergetestutil/tlstestutil.go b/internal/testutil/conciergetestutil/tlstestutil.go new file mode 100644 index 000000000..0dc412e61 --- /dev/null +++ b/internal/testutil/conciergetestutil/tlstestutil.go @@ -0,0 +1,28 @@ +// Copyright 2024 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package conciergetestutil + +import ( + "crypto/tls" + "encoding/base64" + "encoding/pem" + + auth1alpha1 "go.pinniped.dev/generated/latest/apis/concierge/authentication/v1alpha1" +) + +func TlsSpecFromTLSConfig(tls *tls.Config) *auth1alpha1.TLSSpec { + pemData := make([]byte, 0) + for _, certificate := range tls.Certificates { + // this is the public part of the certificate, the private is the certificate.PrivateKey + for _, reallyCertificate := range certificate.Certificate { + pemData = append(pemData, pem.EncodeToMemory(&pem.Block{ + Type: "CERTIFICATE", + Bytes: reallyCertificate, + })...) + } + } + return &auth1alpha1.TLSSpec{ + CertificateAuthorityData: base64.StdEncoding.EncodeToString(pemData), + } +} diff --git a/test/integration/cli_test.go b/test/integration/cli_test.go index 968b7d065..394424da2 100644 --- a/test/integration/cli_test.go +++ b/test/integration/cli_test.go @@ -1,4 +1,4 @@ -// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package integration @@ -26,6 +26,7 @@ import ( "k8s.io/apimachinery/pkg/runtime/serializer" clientauthenticationv1beta1 "k8s.io/client-go/pkg/apis/clientauthentication/v1beta1" + "go.pinniped.dev/generated/latest/apis/concierge/authentication/v1alpha1" identityv1alpha1 "go.pinniped.dev/generated/latest/apis/concierge/identity/v1alpha1" conciergescheme "go.pinniped.dev/internal/concierge/scheme" "go.pinniped.dev/pkg/oidcclient" @@ -42,7 +43,7 @@ func TestCLIGetKubeconfigStaticToken_Parallel(t *testing.T) { ctx, cancelFunc := context.WithTimeout(context.Background(), 5*time.Minute) defer cancelFunc() - authenticator := testlib.CreateTestWebhookAuthenticator(ctx, t) + authenticator := testlib.CreateTestWebhookAuthenticator(ctx, t, nil, v1alpha1.WebhookAuthenticatorPhaseReady) // Build pinniped CLI. pinnipedExe := testlib.PinnipedCLIPath(t) diff --git a/test/integration/concierge_api_serving_certs_test.go b/test/integration/concierge_api_serving_certs_test.go index 1162d03c8..839a31719 100644 --- a/test/integration/concierge_api_serving_certs_test.go +++ b/test/integration/concierge_api_serving_certs_test.go @@ -1,4 +1,4 @@ -// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package integration @@ -12,6 +12,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes" + "go.pinniped.dev/generated/latest/apis/concierge/authentication/v1alpha1" loginv1alpha1 "go.pinniped.dev/generated/latest/apis/concierge/login/v1alpha1" "go.pinniped.dev/internal/testutil" "go.pinniped.dev/test/testlib" @@ -83,7 +84,7 @@ func TestAPIServingCertificateAutoCreationAndRotation_Disruptive(t *testing.T) { // Create a testWebhook so we have a legitimate authenticator to pass to the // TokenCredentialRequest API. - testWebhook := testlib.CreateTestWebhookAuthenticator(ctx, t) + testWebhook := testlib.CreateTestWebhookAuthenticator(ctx, t, nil, v1alpha1.WebhookAuthenticatorPhaseReady) // Get the initial auto-generated version of the Secret. secret, err := kubeClient.CoreV1().Secrets(env.ConciergeNamespace).Get(ctx, defaultServingCertResourceName, metav1.GetOptions{}) diff --git a/test/integration/concierge_client_test.go b/test/integration/concierge_client_test.go index 8a36f7f44..824d50068 100644 --- a/test/integration/concierge_client_test.go +++ b/test/integration/concierge_client_test.go @@ -1,4 +1,4 @@ -// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package integration @@ -11,6 +11,7 @@ import ( "github.com/stretchr/testify/require" + "go.pinniped.dev/generated/latest/apis/concierge/authentication/v1alpha1" "go.pinniped.dev/internal/here" "go.pinniped.dev/pkg/conciergeclient" "go.pinniped.dev/test/testlib" @@ -58,7 +59,7 @@ func TestClient(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), time.Minute) defer cancel() - webhook := testlib.CreateTestWebhookAuthenticator(ctx, t) + webhook := testlib.CreateTestWebhookAuthenticator(ctx, t, nil, v1alpha1.WebhookAuthenticatorPhaseReady) // Use an invalid certificate/key to validate that the ServerVersion API fails like we assume. invalidClient := testlib.NewClientsetWithCertAndKey(t, testCert, testKey) diff --git a/test/integration/concierge_credentialrequest_test.go b/test/integration/concierge_credentialrequest_test.go index f05c93134..c7625dcba 100644 --- a/test/integration/concierge_credentialrequest_test.go +++ b/test/integration/concierge_credentialrequest_test.go @@ -60,8 +60,15 @@ func TestSuccessfulCredentialRequest_Browser(t *testing.T) { token func(t *testing.T) (token string, username string, groups []string) }{ { - name: "webhook", - authenticator: testlib.CreateTestWebhookAuthenticator, + name: "webhook", + authenticator: func(ctx context.Context, t *testing.T) corev1.TypedLocalObjectReference { + authenticator := testlib.CreateTestWebhookAuthenticator(ctx, t, nil, auth1alpha1.WebhookAuthenticatorPhaseReady) + return corev1.TypedLocalObjectReference{ + APIGroup: &auth1alpha1.SchemeGroupVersion.Group, + Kind: "WebhookAuthenticator", + Name: authenticator.Name, + } + }, token: func(t *testing.T) (string, string, []string) { return testlib.IntegrationEnv(t).TestUser.Token, env.TestUser.ExpectedUsername, env.TestUser.ExpectedGroups }, @@ -148,7 +155,7 @@ func TestFailedCredentialRequestWhenTheRequestIsValidButTheTokenDoesNotAuthentic // TokenCredentialRequest API. ctx, cancel := context.WithTimeout(context.Background(), time.Minute) defer cancel() - testWebhook := testlib.CreateTestWebhookAuthenticator(ctx, t) + testWebhook := testlib.CreateTestWebhookAuthenticator(ctx, t, nil, auth1alpha1.WebhookAuthenticatorPhaseReady) response, err := testlib.CreateTokenCredentialRequest(context.Background(), t, loginv1alpha1.TokenCredentialRequestSpec{Token: "not a good token", Authenticator: testWebhook}, @@ -169,7 +176,7 @@ func TestCredentialRequest_ShouldFailWhenRequestDoesNotIncludeToken_Parallel(t * // TokenCredentialRequest API. ctx, cancel := context.WithTimeout(context.Background(), time.Minute) defer cancel() - testWebhook := testlib.CreateTestWebhookAuthenticator(ctx, t) + testWebhook := testlib.CreateTestWebhookAuthenticator(ctx, t, nil, auth1alpha1.WebhookAuthenticatorPhaseReady) response, err := testlib.CreateTokenCredentialRequest(context.Background(), t, loginv1alpha1.TokenCredentialRequestSpec{Token: "", Authenticator: testWebhook}, diff --git a/test/integration/concierge_impersonation_proxy_test.go b/test/integration/concierge_impersonation_proxy_test.go index 8df6ad238..d8263881a 100644 --- a/test/integration/concierge_impersonation_proxy_test.go +++ b/test/integration/concierge_impersonation_proxy_test.go @@ -61,6 +61,7 @@ import ( "k8s.io/client-go/util/retry" "k8s.io/utils/ptr" + "go.pinniped.dev/generated/latest/apis/concierge/authentication/v1alpha1" conciergev1alpha "go.pinniped.dev/generated/latest/apis/concierge/config/v1alpha1" identityv1alpha1 "go.pinniped.dev/generated/latest/apis/concierge/identity/v1alpha1" loginv1alpha1 "go.pinniped.dev/generated/latest/apis/concierge/login/v1alpha1" @@ -120,7 +121,7 @@ func TestImpersonationProxy(t *testing.T) { //nolint:gocyclo // yeah, it's compl // Create a WebhookAuthenticator and prepare a TokenCredentialRequestSpec using the authenticator for use later. credentialRequestSpecWithWorkingCredentials := loginv1alpha1.TokenCredentialRequestSpec{ Token: env.TestUser.Token, - Authenticator: testlib.CreateTestWebhookAuthenticator(ctx, t), + Authenticator: testlib.CreateTestWebhookAuthenticator(ctx, t, nil, v1alpha1.WebhookAuthenticatorPhaseReady), } // The address of the ClusterIP service that points at the impersonation proxy's port (used when there is no load balancer). diff --git a/test/integration/concierge_webhookauthenticator_status_test.go b/test/integration/concierge_webhookauthenticator_status_test.go index 558217660..29935a0e7 100644 --- a/test/integration/concierge_webhookauthenticator_status_test.go +++ b/test/integration/concierge_webhookauthenticator_status_test.go @@ -3,8 +3,294 @@ package integration -// TODO: for integration tests, not unit tests.... -// env.APIGroupSuffix for log messages/conditions when relevant, ie if "pinniped.dev" appears -// env.CLIUpstreamOIDC.Issuer if a real endpoint is needed as we shouldn't actually make requests -// to example.com -// goodEndpoint := "https://example.com" +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/require" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "go.pinniped.dev/generated/latest/apis/concierge/authentication/v1alpha1" + "go.pinniped.dev/test/testlib" +) + +func TestConciergeWebhookAuthenticatorStatus_Parallel(t *testing.T) { + testEnv := testlib.IntegrationEnv(t) + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute) + t.Cleanup(cancel) + + tests := []struct { + name string + run func(t *testing.T) + }{ + { + name: "Basic test to see if the WebhookAuthenticator wakes up or not.", + run: func(t *testing.T) { + webhookAuthenticator := testlib.CreateTestWebhookAuthenticator( + ctx, + t, + nil, + v1alpha1.WebhookAuthenticatorPhaseReady) + + testlib.WaitForWebhookAuthenticatorStatusConditions( + ctx, t, + webhookAuthenticator.Name, + allSuccessfulWebhookAuthenticatorConditions()) + }, + }, { + name: "valid spec with invalid CA in TLS config will result in a WebhookAuthenticator that is not ready", + run: func(t *testing.T) { + caBundleString := "invalid base64-encoded data" + webhookSpec := testEnv.TestWebhook.DeepCopy() + webhookSpec.TLS = &v1alpha1.TLSSpec{ + CertificateAuthorityData: caBundleString, + } + + webhookAuthenticator := testlib.CreateTestWebhookAuthenticator( + ctx, + t, + webhookSpec, + v1alpha1.WebhookAuthenticatorPhaseError) + + testlib.WaitForWebhookAuthenticatorStatusConditions( + ctx, t, + webhookAuthenticator.Name, + replaceSomeConditions( + allSuccessfulWebhookAuthenticatorConditions(), + []metav1.Condition{ + { + Type: "Ready", + Status: "False", + Reason: "NotReady", + Message: "the WebhookAuthenticator is not ready: see other conditions for details", + }, { + Type: "AuthenticatorValid", + Status: "Unknown", + Reason: "UnableToValidate", + Message: "unable to validate; see other conditions for details", + }, { + Type: "TLSConfigurationValid", + Status: "False", + Reason: "InvalidTLSConfiguration", + Message: "invalid TLS configuration: illegal base64 data at input byte 7", + }, { + Type: "TLSConnectionNegotiationValid", + Status: "Unknown", + Reason: "UnableToValidate", + Message: "unable to validate; see other conditions for details", + }, + }, + )) + }, + }, { + name: "valid spec with valid CA in TLS config but does not match issuer server will result in a WebhookAuthenticator that is not ready", + run: func(t *testing.T) { + caBundleSomePivotalCA := "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURVVENDQWptZ0F3SUJBZ0lWQUpzNStTbVRtaTJXeUI0bGJJRXBXaUs5a1RkUE1BMEdDU3FHU0liM0RRRUIKQ3dVQU1COHhDekFKQmdOVkJBWVRBbFZUTVJBd0RnWURWUVFLREFkUWFYWnZkR0ZzTUI0WERUSXdNRFV3TkRFMgpNamMxT0ZvWERUSTBNRFV3TlRFMk1qYzFPRm93SHpFTE1Ba0dBMVVFQmhNQ1ZWTXhFREFPQmdOVkJBb01CMUJwCmRtOTBZV3d3Z2dFaU1BMEdDU3FHU0liM0RRRUJBUVVBQTRJQkR3QXdnZ0VLQW9JQkFRRERZWmZvWGR4Z2NXTEMKZEJtbHB5a0tBaG9JMlBuUWtsVFNXMno1cGcwaXJjOGFRL1E3MXZzMTRZYStmdWtFTGlvOTRZYWw4R01DdVFrbApMZ3AvUEE5N1VYelhQNDBpK25iNXcwRGpwWWd2dU9KQXJXMno2MFRnWE5NSFh3VHk4ME1SZEhpUFVWZ0VZd0JpCmtkNThzdEFVS1Y1MnBQTU1reTJjNy9BcFhJNmRXR2xjalUvaFBsNmtpRzZ5dEw2REtGYjJQRWV3MmdJM3pHZ2IKOFVVbnA1V05DZDd2WjNVY0ZHNXlsZEd3aGc3cnZ4U1ZLWi9WOEhCMGJmbjlxamlrSVcxWFM4dzdpUUNlQmdQMApYZWhKZmVITlZJaTJtZlczNlVQbWpMdnVKaGpqNDIrdFBQWndvdDkzdWtlcEgvbWpHcFJEVm9wamJyWGlpTUYrCkYxdnlPNGMxQWdNQkFBR2pnWU13Z1lBd0hRWURWUjBPQkJZRUZNTWJpSXFhdVkwajRVWWphWDl0bDJzby9LQ1IKTUI4R0ExVWRJd1FZTUJhQUZNTWJpSXFhdVkwajRVWWphWDl0bDJzby9LQ1JNQjBHQTFVZEpRUVdNQlFHQ0NzRwpBUVVGQndNQ0JnZ3JCZ0VGQlFjREFUQVBCZ05WSFJNQkFmOEVCVEFEQVFIL01BNEdBMVVkRHdFQi93UUVBd0lCCkJqQU5CZ2txaGtpRzl3MEJBUXNGQUFPQ0FRRUFYbEh4M2tIMDZwY2NDTDlEVE5qTnBCYnlVSytGd2R6T2IwWFYKcmpNaGtxdHVmdEpUUnR5T3hKZ0ZKNXhUR3pCdEtKamcrVU1pczBOV0t0VDBNWThVMU45U2c5SDl0RFpHRHBjVQpxMlVRU0Y4dXRQMVR3dnJIUzIrdzB2MUoxdHgrTEFiU0lmWmJCV0xXQ21EODUzRlVoWlFZekkvYXpFM28vd0p1CmlPUklMdUpNUk5vNlBXY3VLZmRFVkhaS1RTWnk3a25FcHNidGtsN3EwRE91eUFWdG9HVnlkb3VUR0FOdFhXK2YKczNUSTJjKzErZXg3L2RZOEJGQTFzNWFUOG5vZnU3T1RTTzdiS1kzSkRBUHZOeFQzKzVZUXJwNGR1Nmh0YUFMbAppOHNaRkhidmxpd2EzdlhxL3p1Y2JEaHEzQzBhZnAzV2ZwRGxwSlpvLy9QUUFKaTZLQT09Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K" + webhookSpec := testEnv.TestWebhook.DeepCopy() + webhookSpec.TLS = &v1alpha1.TLSSpec{ + CertificateAuthorityData: caBundleSomePivotalCA, + } + + webhookAuthenticator := testlib.CreateTestWebhookAuthenticator( + ctx, + t, + webhookSpec, + v1alpha1.WebhookAuthenticatorPhaseError) + + testlib.WaitForWebhookAuthenticatorStatusConditions( + ctx, t, + webhookAuthenticator.Name, + replaceSomeConditions( + allSuccessfulWebhookAuthenticatorConditions(), + []metav1.Condition{ + { + Type: "Ready", + Status: "False", + Reason: "NotReady", + Message: "the WebhookAuthenticator is not ready: see other conditions for details", + }, { + Type: "AuthenticatorValid", + Status: "Unknown", + Reason: "UnableToValidate", + Message: "unable to validate; see other conditions for details", + }, { + Type: "TLSConnectionNegotiationValid", + Status: "False", + Reason: "UnableToDialServer", + Message: "cannot dial server: tls: failed to verify certificate: x509: certificate signed by unknown authority", + }, + }, + )) + }, + }, { + name: "invalid with unresponsive endpoint will result in a WebhookAuthenticator that is not ready", + run: func(t *testing.T) { + caBundleSomePivotalCA := "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURVVENDQWptZ0F3SUJBZ0lWQUpzNStTbVRtaTJXeUI0bGJJRXBXaUs5a1RkUE1BMEdDU3FHU0liM0RRRUIKQ3dVQU1COHhDekFKQmdOVkJBWVRBbFZUTVJBd0RnWURWUVFLREFkUWFYWnZkR0ZzTUI0WERUSXdNRFV3TkRFMgpNamMxT0ZvWERUSTBNRFV3TlRFMk1qYzFPRm93SHpFTE1Ba0dBMVVFQmhNQ1ZWTXhFREFPQmdOVkJBb01CMUJwCmRtOTBZV3d3Z2dFaU1BMEdDU3FHU0liM0RRRUJBUVVBQTRJQkR3QXdnZ0VLQW9JQkFRRERZWmZvWGR4Z2NXTEMKZEJtbHB5a0tBaG9JMlBuUWtsVFNXMno1cGcwaXJjOGFRL1E3MXZzMTRZYStmdWtFTGlvOTRZYWw4R01DdVFrbApMZ3AvUEE5N1VYelhQNDBpK25iNXcwRGpwWWd2dU9KQXJXMno2MFRnWE5NSFh3VHk4ME1SZEhpUFVWZ0VZd0JpCmtkNThzdEFVS1Y1MnBQTU1reTJjNy9BcFhJNmRXR2xjalUvaFBsNmtpRzZ5dEw2REtGYjJQRWV3MmdJM3pHZ2IKOFVVbnA1V05DZDd2WjNVY0ZHNXlsZEd3aGc3cnZ4U1ZLWi9WOEhCMGJmbjlxamlrSVcxWFM4dzdpUUNlQmdQMApYZWhKZmVITlZJaTJtZlczNlVQbWpMdnVKaGpqNDIrdFBQWndvdDkzdWtlcEgvbWpHcFJEVm9wamJyWGlpTUYrCkYxdnlPNGMxQWdNQkFBR2pnWU13Z1lBd0hRWURWUjBPQkJZRUZNTWJpSXFhdVkwajRVWWphWDl0bDJzby9LQ1IKTUI4R0ExVWRJd1FZTUJhQUZNTWJpSXFhdVkwajRVWWphWDl0bDJzby9LQ1JNQjBHQTFVZEpRUVdNQlFHQ0NzRwpBUVVGQndNQ0JnZ3JCZ0VGQlFjREFUQVBCZ05WSFJNQkFmOEVCVEFEQVFIL01BNEdBMVVkRHdFQi93UUVBd0lCCkJqQU5CZ2txaGtpRzl3MEJBUXNGQUFPQ0FRRUFYbEh4M2tIMDZwY2NDTDlEVE5qTnBCYnlVSytGd2R6T2IwWFYKcmpNaGtxdHVmdEpUUnR5T3hKZ0ZKNXhUR3pCdEtKamcrVU1pczBOV0t0VDBNWThVMU45U2c5SDl0RFpHRHBjVQpxMlVRU0Y4dXRQMVR3dnJIUzIrdzB2MUoxdHgrTEFiU0lmWmJCV0xXQ21EODUzRlVoWlFZekkvYXpFM28vd0p1CmlPUklMdUpNUk5vNlBXY3VLZmRFVkhaS1RTWnk3a25FcHNidGtsN3EwRE91eUFWdG9HVnlkb3VUR0FOdFhXK2YKczNUSTJjKzErZXg3L2RZOEJGQTFzNWFUOG5vZnU3T1RTTzdiS1kzSkRBUHZOeFQzKzVZUXJwNGR1Nmh0YUFMbAppOHNaRkhidmxpd2EzdlhxL3p1Y2JEaHEzQzBhZnAzV2ZwRGxwSlpvLy9QUUFKaTZLQT09Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K" + webhookSpec := testEnv.TestWebhook.DeepCopy() + webhookSpec.TLS = &v1alpha1.TLSSpec{ + CertificateAuthorityData: caBundleSomePivotalCA, + } + webhookSpec.Endpoint = "https://127.0.0.1:443/some-fake-endpoint" + + webhookAuthenticator := testlib.CreateTestWebhookAuthenticator( + ctx, + t, + webhookSpec, + v1alpha1.WebhookAuthenticatorPhaseError) + + testlib.WaitForWebhookAuthenticatorStatusConditions( + ctx, t, + webhookAuthenticator.Name, + replaceSomeConditions( + allSuccessfulWebhookAuthenticatorConditions(), + []metav1.Condition{ + { + Type: "Ready", + Status: "False", + Reason: "NotReady", + Message: "the WebhookAuthenticator is not ready: see other conditions for details", + }, { + Type: "AuthenticatorValid", + Status: "Unknown", + Reason: "UnableToValidate", + Message: "unable to validate; see other conditions for details", + }, { + Type: "TLSConnectionNegotiationValid", + Status: "False", + Reason: "UnableToDialServer", + Message: "cannot dial server: dial tcp 127.0.0.1:443: connect: connection refused", + }, + }, + )) + }, + }, + } + for _, test := range tests { + tt := test + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + tt.run(t) + }) + } +} + +func TestConciergeWebhookAuthenticatorCRDValidations_Parallel(t *testing.T) { + env := testlib.IntegrationEnv(t) + webhookAuthenticatorClient := testlib.NewConciergeClientset(t).AuthenticationV1alpha1().WebhookAuthenticators() + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute) + t.Cleanup(cancel) + + objectMeta := testlib.ObjectMetaWithRandomName(t, "webhook-authenticator") + tests := []struct { + name string + webhookAuthenticator *v1alpha1.WebhookAuthenticator + wantErr string + }{ + { + name: "endpoint can not be empty string", + webhookAuthenticator: &v1alpha1.WebhookAuthenticator{ + ObjectMeta: objectMeta, + Spec: v1alpha1.WebhookAuthenticatorSpec{ + Endpoint: "", + }, + }, + wantErr: `WebhookAuthenticator.authentication.concierge.` + env.APIGroupSuffix + ` "` + objectMeta.Name + `" is invalid: ` + + `spec.endpoint: Invalid value: "": spec.endpoint in body should be at least 1 chars long`, + }, + { + name: "endpoint must be https", + webhookAuthenticator: &v1alpha1.WebhookAuthenticator{ + ObjectMeta: objectMeta, + Spec: v1alpha1.WebhookAuthenticatorSpec{ + Endpoint: "http://www.example.com", + }, + }, + wantErr: `WebhookAuthenticator.authentication.concierge.` + env.APIGroupSuffix + ` "` + objectMeta.Name + `" is invalid: ` + + `spec.endpoint: Invalid value: "http://www.example.com": spec.endpoint in body should match '^https://'`, + }, + { + name: "minimum valid authenticator", + webhookAuthenticator: &v1alpha1.WebhookAuthenticator{ + ObjectMeta: testlib.ObjectMetaWithRandomName(t, "webhook"), + Spec: v1alpha1.WebhookAuthenticatorSpec{ + Endpoint: "https://localhost/webhook-isnt-actually-here", + }, + }, + }, + { + name: "valid authenticator can have empty TLS block", + webhookAuthenticator: &v1alpha1.WebhookAuthenticator{ + ObjectMeta: testlib.ObjectMetaWithRandomName(t, "webhook"), + Spec: v1alpha1.WebhookAuthenticatorSpec{ + Endpoint: "https://localhost/webhook-isnt-actually-here", + TLS: &v1alpha1.TLSSpec{}, + }, + }, + }, + { + name: "valid authenticator can have empty TLS CertificateAuthorityData", + webhookAuthenticator: &v1alpha1.WebhookAuthenticator{ + ObjectMeta: testlib.ObjectMetaWithRandomName(t, "jwtauthenticator"), + Spec: v1alpha1.WebhookAuthenticatorSpec{ + Endpoint: "https://localhost/webhook-isnt-actually-here", + TLS: &v1alpha1.TLSSpec{ + CertificateAuthorityData: "pretend-this-is-a-certificate", + }, + }, + }, + }, + } + for _, test := range tests { + tt := test + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + _, createErr := webhookAuthenticatorClient.Create(ctx, tt.webhookAuthenticator, metav1.CreateOptions{}) + + t.Cleanup(func() { + // delete if it exists + delErr := webhookAuthenticatorClient.Delete(ctx, tt.webhookAuthenticator.Name, metav1.DeleteOptions{}) + if !errors.IsNotFound(delErr) { + require.NoError(t, delErr) + } + }) + + if tt.wantErr != "" { + wantErr := tt.wantErr + require.EqualError(t, createErr, wantErr) + } else { + require.NoError(t, createErr) + } + }) + } +} +func allSuccessfulWebhookAuthenticatorConditions() []metav1.Condition { + return []metav1.Condition{{ + Type: "AuthenticatorValid", + Status: "True", + Reason: "Success", + Message: "authenticator initialized", + }, { + Type: "EndpointURLValid", + Status: "True", + Reason: "Success", + Message: "endpoint is a valid URL", + }, { + Type: "Ready", + Status: "True", + Reason: "Success", + Message: "the WebhookAuthenticator is ready", + }, { + Type: "TLSConfigurationValid", + Status: "True", + Reason: "Success", + Message: "successfully parsed specified CA bundle", + }, { + Type: "TLSConnectionNegotiationValid", + Status: "True", + Reason: "Success", + Message: "tls verified", + }} +} diff --git a/test/integration/kube_api_discovery_test.go b/test/integration/kube_api_discovery_test.go index f7756f393..327c31a45 100644 --- a/test/integration/kube_api_discovery_test.go +++ b/test/integration/kube_api_discovery_test.go @@ -438,7 +438,7 @@ func TestGetAPIResourceList(t *testing.T) { //nolint:gocyclo // each t.Run is pr } // manually update this value whenever you add additional fields to an API resource and then run the generator - totalExpectedAPIFields := 260 + totalExpectedAPIFields := 261 // Because we are parsing text from `kubectl explain` and because the format of that text can change // over time, make a rudimentary assertion that this test exercised the whole tree of all fields of all diff --git a/test/testlib/client.go b/test/testlib/client.go index 4af9c52c9..486e3a92d 100644 --- a/test/testlib/client.go +++ b/test/testlib/client.go @@ -36,7 +36,6 @@ import ( supervisorclientset "go.pinniped.dev/generated/latest/client/supervisor/clientset/versioned" "go.pinniped.dev/internal/groupsuffix" "go.pinniped.dev/internal/kubeclient" - // Import to initialize client auth plugins - the kubeconfig that we use for // testing may use gcloud, az, oidc, etc. _ "k8s.io/client-go/plugin/pkg/client/auth" @@ -171,7 +170,11 @@ func NewKubeclient(t *testing.T, config *rest.Config) *kubeclient.Client { // CreateTestWebhookAuthenticator creates and returns a test WebhookAuthenticator in $PINNIPED_TEST_CONCIERGE_NAMESPACE, which will be // automatically deleted at the end of the current test's lifetime. It returns a corev1.TypedLocalObjectReference which // describes the test webhook authenticator within the test namespace. -func CreateTestWebhookAuthenticator(ctx context.Context, t *testing.T) corev1.TypedLocalObjectReference { +func CreateTestWebhookAuthenticator( + ctx context.Context, + t *testing.T, + webhookSpec *auth1alpha1.WebhookAuthenticatorSpec, + expectedStatus auth1alpha1.WebhookAuthenticatorPhase) corev1.TypedLocalObjectReference { t.Helper() testEnv := IntegrationEnv(t) @@ -181,9 +184,13 @@ func CreateTestWebhookAuthenticator(ctx context.Context, t *testing.T) corev1.Ty createContext, cancel := context.WithTimeout(ctx, time.Minute) defer cancel() + if webhookSpec == nil { + webhookSpec = &testEnv.TestWebhook + } + webhook, err := webhooks.Create(createContext, &auth1alpha1.WebhookAuthenticator{ ObjectMeta: testObjectMeta(t, "webhook"), - Spec: testEnv.TestWebhook, + Spec: *webhookSpec, }, metav1.CreateOptions{}) require.NoError(t, err, "could not create test WebhookAuthenticator") t.Logf("created test WebhookAuthenticator %s", webhook.Name) @@ -197,6 +204,10 @@ func CreateTestWebhookAuthenticator(ctx context.Context, t *testing.T) corev1.Ty require.NoErrorf(t, err, "could not cleanup test WebhookAuthenticator %s/%s", webhook.Namespace, webhook.Name) }) + if expectedStatus != "" { + WaitForWebhookAuthenticatorStatusPhase(ctx, t, webhook.Name, expectedStatus) + } + return corev1.TypedLocalObjectReference{ APIGroup: &auth1alpha1.SchemeGroupVersion.Group, Kind: "WebhookAuthenticator", @@ -204,6 +215,47 @@ func CreateTestWebhookAuthenticator(ctx context.Context, t *testing.T) corev1.Ty } } +func WaitForWebhookAuthenticatorStatusPhase( + ctx context.Context, + t *testing.T, + webhookName string, + expectPhase auth1alpha1.WebhookAuthenticatorPhase) { + t.Helper() + webhookAuthenticatorClientSet := NewConciergeClientset(t).AuthenticationV1alpha1().WebhookAuthenticators() + + RequireEventuallyf(t, func(requireEventually *require.Assertions) { + webhookA, err := webhookAuthenticatorClientSet.Get(ctx, webhookName, metav1.GetOptions{}) + requireEventually.NoError(err) + requireEventually.Equalf(expectPhase, webhookA.Status.Phase, "actual status conditions were: %#v", webhookA.Status.Conditions) + }, 60*time.Second, 1*time.Second, "expected the WebhookAuthenticator to have status %q", expectPhase) +} + +func WaitForWebhookAuthenticatorStatusConditions(ctx context.Context, t *testing.T, webhookName string, expectConditions []metav1.Condition) { + t.Helper() + webhookClient := NewConciergeClientset(t).AuthenticationV1alpha1().WebhookAuthenticators() + RequireEventuallyf(t, func(requireEventually *require.Assertions) { + fd, err := webhookClient.Get(ctx, webhookName, metav1.GetOptions{}) + requireEventually.NoError(err) + + requireEventually.Lenf(fd.Status.Conditions, len(expectConditions), + "wanted status conditions: %#v", expectConditions) + + for i, wantCond := range expectConditions { + actualCond := fd.Status.Conditions[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, fd.Status.Conditions, i) + } + }, 60*time.Second, 1*time.Second, "wanted WebhookAuthenticator conditions") +} + // CreateTestJWTAuthenticatorForCLIUpstream creates and returns a test JWTAuthenticator which will be automatically // deleted at the end of the current test's lifetime. //