From d13bb07b3e18aef6ec100fd0429630daaea3777f Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Wed, 10 Mar 2021 16:57:15 -0800 Subject: [PATCH] Add integration test for using WhoAmIRequest through impersonator --- go.mod | 4 +- .../concierge_impersonation_proxy_test.go | 263 +++++++++++------- test/integration/whoami_test.go | 8 - test/library/client.go | 18 +- test/library/credential_request.go | 27 -- 5 files changed, 174 insertions(+), 146 deletions(-) delete mode 100644 test/library/credential_request.go diff --git a/go.mod b/go.mod index bb3b8a9a..c6cc4589 100644 --- a/go.mod +++ b/go.mod @@ -15,7 +15,7 @@ require ( github.com/google/go-cmp v0.5.5 github.com/google/gofuzz v1.2.0 github.com/gorilla/securecookie v1.1.1 - github.com/gorilla/websocket v1.4.2 // indirect + github.com/gorilla/websocket v1.4.2 github.com/oleiade/reflections v1.0.1 // indirect github.com/onsi/ginkgo v1.13.0 // indirect github.com/ory/fosite v0.38.0 @@ -27,7 +27,7 @@ require ( github.com/spf13/pflag v1.0.5 github.com/stretchr/testify v1.7.0 golang.org/x/crypto v0.0.0-20201217014255-9d1352758620 - golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 + golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 // indirect golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d golang.org/x/sync v0.0.0-20201207232520-09787c993a3a golang.org/x/tools v0.0.0-20200825202427-b303f430e36d // indirect diff --git a/test/integration/concierge_impersonation_proxy_test.go b/test/integration/concierge_impersonation_proxy_test.go index 93f6f343..520eda31 100644 --- a/test/integration/concierge_impersonation_proxy_test.go +++ b/test/integration/concierge_impersonation_proxy_test.go @@ -22,7 +22,6 @@ import ( "time" "github.com/gorilla/websocket" - "github.com/stretchr/testify/require" v1 "k8s.io/api/authorization/v1" corev1 "k8s.io/api/core/v1" @@ -38,9 +37,11 @@ import ( "sigs.k8s.io/yaml" "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" "go.pinniped.dev/generated/latest/client/concierge/clientset/versioned" "go.pinniped.dev/internal/concierge/impersonator" + "go.pinniped.dev/internal/kubeclient" "go.pinniped.dev/internal/testutil" "go.pinniped.dev/test/library" ) @@ -59,35 +60,20 @@ func TestImpersonationProxy(t *testing.T) { //nolint:gocyclo // yeah, it's compl adminClient := library.NewKubernetesClientset(t) adminConciergeClient := library.NewConciergeClientset(t) - // Create a WebhookAuthenticator. - authenticator := library.CreateTestWebhookAuthenticator(ctx, t) + // Create a WebhookAuthenticator and prepare a TokenCredentialRequestSpec using the authenticator for use later. + credentialRequestSpecWithWorkingCredentials := loginv1alpha1.TokenCredentialRequestSpec{ + Token: env.TestUser.Token, + Authenticator: library.CreateTestWebhookAuthenticator(ctx, t), + } // The address of the ClusterIP service that points at the impersonation proxy's port (used when there is no load balancer). proxyServiceEndpoint := fmt.Sprintf("%s-proxy.%s.svc.cluster.local", env.ConciergeAppName, env.ConciergeNamespace) // The error message that will be returned by squid when the impersonation proxy port inside the cluster is not listening. serviceUnavailableViaSquidError := fmt.Sprintf(`Get "https://%s/api/v1/namespaces": Service Unavailable`, proxyServiceEndpoint) - credentialRequestSpecWithWorkingCredentials := loginv1alpha1.TokenCredentialRequestSpec{ - Token: env.TestUser.Token, - Authenticator: authenticator, - } - - credentialAlmostExpired := func(credential *loginv1alpha1.TokenCredentialRequest) bool { - pemBlock, _ := pem.Decode([]byte(credential.Status.Credential.ClientCertificateData)) - parsedCredential, err := x509.ParseCertificate(pemBlock.Bytes) - require.NoError(t, err) - timeRemaining := time.Until(parsedCredential.NotAfter) - if timeRemaining < 2*time.Minute { - t.Logf("The TokenCredentialRequest cred is almost expired and needs to be refreshed. Expires in %s.", timeRemaining) - return true - } - t.Logf("The TokenCredentialRequest cred is good for some more time (%s) so using it.", timeRemaining) - return false - } - - var tokenCredentialRequestResponse *loginv1alpha1.TokenCredentialRequest + var mostRecentTokenCredentialRequestResponse *loginv1alpha1.TokenCredentialRequest refreshCredential := func() *loginv1alpha1.ClusterCredential { - if tokenCredentialRequestResponse == nil || credentialAlmostExpired(tokenCredentialRequestResponse) { + if mostRecentTokenCredentialRequestResponse == nil || credentialAlmostExpired(t, mostRecentTokenCredentialRequestResponse) { var err error // Make a TokenCredentialRequest. This can either return a cert signed by the Kube API server's CA (e.g. on kind) // or a cert signed by the impersonator's signing CA (e.g. on GKE). Either should be accepted by the impersonation @@ -95,80 +81,46 @@ func TestImpersonationProxy(t *testing.T) { //nolint:gocyclo // yeah, it's compl // // However, we issue short-lived certs, so this cert will only be valid for a few minutes. // Cache it until it is almost expired and then refresh it whenever it is close to expired. - tokenCredentialRequestResponse, err = library.CreateTokenCredentialRequest(ctx, t, credentialRequestSpecWithWorkingCredentials) + mostRecentTokenCredentialRequestResponse, err = library.CreateTokenCredentialRequest(ctx, t, credentialRequestSpecWithWorkingCredentials) require.NoError(t, err) - require.Nil(t, tokenCredentialRequestResponse.Status.Message, - "expected no error message but got: %s", library.Sdump(tokenCredentialRequestResponse.Status.Message)) - require.NotEmpty(t, tokenCredentialRequestResponse.Status.Credential.ClientCertificateData) - require.NotEmpty(t, tokenCredentialRequestResponse.Status.Credential.ClientKeyData) + require.Nil(t, mostRecentTokenCredentialRequestResponse.Status.Message, + "expected no error message but got: %s", library.Sdump(mostRecentTokenCredentialRequestResponse.Status.Message)) + require.NotEmpty(t, mostRecentTokenCredentialRequestResponse.Status.Credential.ClientCertificateData) + require.NotEmpty(t, mostRecentTokenCredentialRequestResponse.Status.Credential.ClientKeyData) // At the moment the credential request should not have returned a token. In the future, if we make it return // tokens, we should revisit this test's rest config below. - require.Empty(t, tokenCredentialRequestResponse.Status.Credential.Token) + require.Empty(t, mostRecentTokenCredentialRequestResponse.Status.Credential.Token) } - return tokenCredentialRequestResponse.Status.Credential + return mostRecentTokenCredentialRequestResponse.Status.Credential } - impersonationProxyRestConfig := func(credential *loginv1alpha1.ClusterCredential, host string, caData []byte, doubleImpersonateUser string) *rest.Config { - config := rest.Config{ - Host: host, - TLSClientConfig: rest.TLSClientConfig{ - Insecure: caData == nil, - CAData: caData, - CertData: []byte(credential.ClientCertificateData), - KeyData: []byte(credential.ClientKeyData), - }, - // kubectl would set both the client cert and the token, so we'll do that too. - // The Kube API server will ignore the token if the client cert successfully authenticates. - // Only if the client cert is not present or fails to authenticate will it use the token. - // Historically, it works that way because some web browsers will always send your - // corporate-assigned client cert even if it is not valid, and it doesn't want to treat - // that as a failure if you also sent a perfectly good token. - // We would like the impersonation proxy to imitate that behavior, so we test it here. - BearerToken: "this is not valid", - } - if doubleImpersonateUser != "" { - config.Impersonate = rest.ImpersonationConfig{UserName: doubleImpersonateUser} - } - return &config - } - - kubeconfigProxyFunc := func() func(req *http.Request) (*url.URL, error) { - return func(req *http.Request) (*url.URL, error) { - proxyURL, err := url.Parse(env.Proxy) - require.NoError(t, err) - t.Logf("passing request for %s through proxy %s", req.URL, proxyURL.String()) - return proxyURL, nil - } - } - - impersonationProxyViaSquidClient := func(proxyURL string, caData []byte, doubleImpersonateUser string) kubernetes.Interface { - t.Helper() - kubeconfig := impersonationProxyRestConfig(refreshCredential(), proxyURL, caData, doubleImpersonateUser) - kubeconfig.Proxy = kubeconfigProxyFunc() - return library.NewKubeclient(t, kubeconfig).Kubernetes - } - - impersonationProxyViaSquidClientWithoutCredential := func() kubernetes.Interface { - t.Helper() + impersonationProxyViaSquidKubeClientWithoutCredential := func() kubernetes.Interface { proxyURL := "https://" + proxyServiceEndpoint kubeconfig := impersonationProxyRestConfig(&loginv1alpha1.ClusterCredential{}, proxyURL, nil, "") - kubeconfig.Proxy = kubeconfigProxyFunc() + kubeconfig.Proxy = kubeconfigProxyFunc(t, env.Proxy) return library.NewKubeclient(t, kubeconfig).Kubernetes } - impersonationProxyViaLoadBalancerClient := func(proxyURL string, caData []byte, doubleImpersonateUser string) kubernetes.Interface { - t.Helper() - kubeconfig := impersonationProxyRestConfig(refreshCredential(), proxyURL, caData, doubleImpersonateUser) - return library.NewKubeclient(t, kubeconfig).Kubernetes - } - - newImpersonationProxyClient := func(proxyURL string, impersonationProxyCACertPEM []byte, doubleImpersonateUser string) kubernetes.Interface { - if env.HasCapability(library.HasExternalLoadBalancerProvider) { - return impersonationProxyViaLoadBalancerClient(proxyURL, impersonationProxyCACertPEM, doubleImpersonateUser) + newImpersonationProxyClientWithCredentials := func(credentials *loginv1alpha1.ClusterCredential, impersonationProxyURL string, impersonationProxyCACertPEM []byte, doubleImpersonateUser string) *kubeclient.Client { + kubeconfig := impersonationProxyRestConfig(credentials, impersonationProxyURL, impersonationProxyCACertPEM, doubleImpersonateUser) + if !env.HasCapability(library.HasExternalLoadBalancerProvider) { + // Send traffic through the Squid proxy + kubeconfig.Proxy = kubeconfigProxyFunc(t, env.Proxy) } - return impersonationProxyViaSquidClient(proxyURL, impersonationProxyCACertPEM, doubleImpersonateUser) + return library.NewKubeclient(t, kubeconfig) + } + + newImpersonationProxyClient := func(impersonationProxyURL string, impersonationProxyCACertPEM []byte, doubleImpersonateUser string) *kubeclient.Client { + refreshedCredentials := refreshCredential() + refreshedCredentials.Token = "not a valid token" // demonstrates that client certs take precedence over tokens by setting both on the requests + return newImpersonationProxyClientWithCredentials(refreshedCredentials, impersonationProxyURL, impersonationProxyCACertPEM, doubleImpersonateUser) + } + + newAnonymousImpersonationProxyClient := func(impersonationProxyURL string, impersonationProxyCACertPEM []byte, doubleImpersonateUser string) *kubeclient.Client { + emptyCredentials := &loginv1alpha1.ClusterCredential{} + return newImpersonationProxyClientWithCredentials(emptyCredentials, impersonationProxyURL, impersonationProxyCACertPEM, doubleImpersonateUser) } oldConfigMap, err := adminClient.CoreV1().ConfigMaps(env.ConciergeNamespace).Get(ctx, impersonationProxyConfigMapName(env), metav1.GetOptions{}) @@ -216,7 +168,7 @@ func TestImpersonationProxy(t *testing.T) { //nolint:gocyclo // yeah, it's compl }, 10*time.Second, 500*time.Millisecond) // Check that we can't use the impersonation proxy to execute kubectl commands yet. - _, err = impersonationProxyViaSquidClientWithoutCredential().CoreV1().Namespaces().List(ctx, metav1.ListOptions{}) + _, err = impersonationProxyViaSquidKubeClientWithoutCredential().CoreV1().Namespaces().List(ctx, metav1.ListOptions{}) require.EqualError(t, err, serviceUnavailableViaSquidError) // Create configuration to make the impersonation proxy turn on with a hard coded endpoint (without a load balancer). @@ -244,21 +196,21 @@ func TestImpersonationProxy(t *testing.T) { //nolint:gocyclo // yeah, it's compl // credentials before they expire. Create a closure to capture the arguments to newImpersonationProxyClient // so we don't have to keep repeating them. // This client performs TLS checks, so it also provides test coverage that the impersonation proxy server is generating TLS certs correctly. - impersonationProxyClient := func() kubernetes.Interface { - return newImpersonationProxyClient(impersonationProxyURL, impersonationProxyCACertPEM, "") + impersonationProxyKubeClient := func() kubernetes.Interface { + return newImpersonationProxyClient(impersonationProxyURL, impersonationProxyCACertPEM, "").Kubernetes } // Test that the user can perform basic actions through the client with their username and group membership // influencing RBAC checks correctly. t.Run( "access as user", - library.AccessAsUserTest(ctx, env.TestUser.ExpectedUsername, impersonationProxyClient()), + library.AccessAsUserTest(ctx, env.TestUser.ExpectedUsername, impersonationProxyKubeClient()), ) for _, group := range env.TestUser.ExpectedGroups { group := group t.Run( "access as group "+group, - library.AccessAsGroupTest(ctx, group, impersonationProxyClient()), + library.AccessAsGroupTest(ctx, group, impersonationProxyKubeClient()), ) } @@ -288,7 +240,7 @@ func TestImpersonationProxy(t *testing.T) { //nolint:gocyclo // yeah, it's compl // Create and start informer to exercise the "watch" verb for us. informerFactory := k8sinformers.NewSharedInformerFactoryWithOptions( - impersonationProxyClient(), + impersonationProxyKubeClient(), 0, k8sinformers.WithNamespace(namespace.Name)) informer := informerFactory.Core().V1().ConfigMaps() @@ -308,17 +260,17 @@ func TestImpersonationProxy(t *testing.T) { //nolint:gocyclo // yeah, it's compl } // Test "create" verb through the impersonation proxy. - _, err = impersonationProxyClient().CoreV1().ConfigMaps(namespace.Name).Create(ctx, + _, err = impersonationProxyKubeClient().CoreV1().ConfigMaps(namespace.Name).Create(ctx, &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Name: "configmap-1", Labels: configMapLabels}}, metav1.CreateOptions{}, ) require.NoError(t, err) - _, err = impersonationProxyClient().CoreV1().ConfigMaps(namespace.Name).Create(ctx, + _, err = impersonationProxyKubeClient().CoreV1().ConfigMaps(namespace.Name).Create(ctx, &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Name: "configmap-2", Labels: configMapLabels}}, metav1.CreateOptions{}, ) require.NoError(t, err) - _, err = impersonationProxyClient().CoreV1().ConfigMaps(namespace.Name).Create(ctx, + _, err = impersonationProxyKubeClient().CoreV1().ConfigMaps(namespace.Name).Create(ctx, &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Name: "configmap-3", Labels: configMapLabels}}, metav1.CreateOptions{}, ) @@ -334,11 +286,11 @@ func TestImpersonationProxy(t *testing.T) { //nolint:gocyclo // yeah, it's compl }, 10*time.Second, 50*time.Millisecond) // Test "get" verb through the impersonation proxy. - configMap3, err := impersonationProxyClient().CoreV1().ConfigMaps(namespace.Name).Get(ctx, "configmap-3", metav1.GetOptions{}) + configMap3, err := impersonationProxyKubeClient().CoreV1().ConfigMaps(namespace.Name).Get(ctx, "configmap-3", metav1.GetOptions{}) require.NoError(t, err) // Test "list" verb through the impersonation proxy. - listResult, err := impersonationProxyClient().CoreV1().ConfigMaps(namespace.Name).List(ctx, metav1.ListOptions{ + listResult, err := impersonationProxyKubeClient().CoreV1().ConfigMaps(namespace.Name).List(ctx, metav1.ListOptions{ LabelSelector: configMapLabels.String(), }) require.NoError(t, err) @@ -346,7 +298,7 @@ func TestImpersonationProxy(t *testing.T) { //nolint:gocyclo // yeah, it's compl // Test "update" verb through the impersonation proxy. configMap3.Data = map[string]string{"foo": "bar"} - updateResult, err := impersonationProxyClient().CoreV1().ConfigMaps(namespace.Name).Update(ctx, configMap3, metav1.UpdateOptions{}) + updateResult, err := impersonationProxyKubeClient().CoreV1().ConfigMaps(namespace.Name).Update(ctx, configMap3, metav1.UpdateOptions{}) require.NoError(t, err) require.Equal(t, "bar", updateResult.Data["foo"]) @@ -357,7 +309,7 @@ func TestImpersonationProxy(t *testing.T) { //nolint:gocyclo // yeah, it's compl }, 10*time.Second, 50*time.Millisecond) // Test "patch" verb through the impersonation proxy. - patchResult, err := impersonationProxyClient().CoreV1().ConfigMaps(namespace.Name).Patch(ctx, + patchResult, err := impersonationProxyKubeClient().CoreV1().ConfigMaps(namespace.Name).Patch(ctx, "configmap-3", types.MergePatchType, []byte(`{"data":{"baz":"42"}}`), @@ -374,7 +326,7 @@ func TestImpersonationProxy(t *testing.T) { //nolint:gocyclo // yeah, it's compl }, 10*time.Second, 50*time.Millisecond) // Test "delete" verb through the impersonation proxy. - err = impersonationProxyClient().CoreV1().ConfigMaps(namespace.Name).Delete(ctx, "configmap-3", metav1.DeleteOptions{}) + err = impersonationProxyKubeClient().CoreV1().ConfigMaps(namespace.Name).Delete(ctx, "configmap-3", metav1.DeleteOptions{}) require.NoError(t, err) // Make sure that the deleted ConfigMap shows up in the informer's cache. @@ -385,7 +337,7 @@ func TestImpersonationProxy(t *testing.T) { //nolint:gocyclo // yeah, it's compl }, 10*time.Second, 50*time.Millisecond) // Test "deletecollection" verb through the impersonation proxy. - err = impersonationProxyClient().CoreV1().ConfigMaps(namespace.Name).DeleteCollection(ctx, metav1.DeleteOptions{}, metav1.ListOptions{}) + err = impersonationProxyKubeClient().CoreV1().ConfigMaps(namespace.Name).DeleteCollection(ctx, metav1.DeleteOptions{}, metav1.ListOptions{}) require.NoError(t, err) // Make sure that the deleted ConfigMaps shows up in the informer's cache. @@ -395,7 +347,7 @@ func TestImpersonationProxy(t *testing.T) { //nolint:gocyclo // yeah, it's compl }, 10*time.Second, 50*time.Millisecond) // There should be no ConfigMaps left. - listResult, err = impersonationProxyClient().CoreV1().ConfigMaps(namespace.Name).List(ctx, metav1.ListOptions{ + listResult, err = impersonationProxyKubeClient().CoreV1().ConfigMaps(namespace.Name).List(ctx, metav1.ListOptions{ LabelSelector: configMapLabels.String(), }) require.NoError(t, err) @@ -415,16 +367,16 @@ func TestImpersonationProxy(t *testing.T) { //nolint:gocyclo // yeah, it's compl // Make a client which will send requests through the impersonation proxy and will also add // impersonate headers to the request. - doubleImpersonationClient := newImpersonationProxyClient(impersonationProxyURL, impersonationProxyCACertPEM, "other-user-to-impersonate") + doubleImpersonationKubeClient := newImpersonationProxyClient(impersonationProxyURL, impersonationProxyCACertPEM, "other-user-to-impersonate").Kubernetes // Check that we can get some resource through the impersonation proxy without any impersonation headers on the request. // We could use any resource for this, but we happen to know that this one should exist. - _, err = impersonationProxyClient().CoreV1().Secrets(env.ConciergeNamespace).Get(ctx, impersonationProxyTLSSecretName(env), metav1.GetOptions{}) + _, err = impersonationProxyKubeClient().CoreV1().Secrets(env.ConciergeNamespace).Get(ctx, impersonationProxyTLSSecretName(env), metav1.GetOptions{}) require.NoError(t, err) // Now we'll see what happens when we add an impersonation header to the request. This should generate a - // request similar to the one above, except that it will have an impersonation header. - _, err = doubleImpersonationClient.CoreV1().Secrets(env.ConciergeNamespace).Get(ctx, impersonationProxyTLSSecretName(env), metav1.GetOptions{}) + // request similar to the one above, except that it will also have an impersonation header. + _, err = doubleImpersonationKubeClient.CoreV1().Secrets(env.ConciergeNamespace).Get(ctx, impersonationProxyTLSSecretName(env), metav1.GetOptions{}) // Double impersonation is not supported yet, so we should get an error. require.EqualError(t, err, fmt.Sprintf( `users "other-user-to-impersonate" is forbidden: `+ @@ -433,6 +385,36 @@ func TestImpersonationProxy(t *testing.T) { //nolint:gocyclo // yeah, it's compl env.TestUser.ExpectedUsername)) }) + t.Run("using service account tokens to authenticate to impersonation proxy", func(t *testing.T) { + // TODO: test that this is not currently allowed + }) + + t.Run("WhoAmIRequests through the impersonation proxy", func(t *testing.T) { + // Test using the TokenCredentialRequest for authentication. + impersonationProxyPinnipedConciergeClient := newImpersonationProxyClient( + impersonationProxyURL, impersonationProxyCACertPEM, "", + ).PinnipedConcierge + whoAmI, err := impersonationProxyPinnipedConciergeClient.IdentityV1alpha1().WhoAmIRequests(). + Create(ctx, &identityv1alpha1.WhoAmIRequest{}, metav1.CreateOptions{}) + require.NoError(t, err) + require.Equal(t, + expectedWhoAmIRequestResponse(env.TestUser.ExpectedUsername, append(env.TestUser.ExpectedGroups, "system:authenticated")), + whoAmI, + ) + + // Test an unauthenticated request which does not include any credentials. + impersonationProxyAnonymousPinnipedConciergeClient := newAnonymousImpersonationProxyClient( + impersonationProxyURL, impersonationProxyCACertPEM, "", + ).PinnipedConcierge + whoAmI, err = impersonationProxyAnonymousPinnipedConciergeClient.IdentityV1alpha1().WhoAmIRequests(). + Create(ctx, &identityv1alpha1.WhoAmIRequest{}, metav1.CreateOptions{}) + require.NoError(t, err) + require.Equal(t, + expectedWhoAmIRequestResponse("system:anonymous", []string{"system:unauthenticated"}), + whoAmI, + ) + }) + t.Run("kubectl as a client", func(t *testing.T) { // Create an RBAC rule to allow this user to read/write everything. library.CreateTestClusterRoleBinding(t, @@ -526,7 +508,7 @@ func TestImpersonationProxy(t *testing.T) { //nolint:gocyclo // yeah, it's compl // Try "kubectl exec" through the impersonation proxy. echoString := "hello world" stdout, _, err := runKubectl("exec", "--namespace", env.ConciergeNamespace, podName, "--", "echo", echoString) - require.NoError(t, err) + require.NoError(t, err, `"kubectl exec" failed`) require.Equal(t, echoString+"\n", stdout) // run the kubectl port-forward command @@ -537,7 +519,7 @@ func TestImpersonationProxy(t *testing.T) { //nolint:gocyclo // yeah, it's compl // start, but don't wait for the command to finish err = portForwardCmd.Start() - require.NoError(t, err) + require.NoError(t, err, `"kubectl port-forward" failed`) // then run curl something against it time.Sleep(time.Second) @@ -589,7 +571,9 @@ func TestImpersonationProxy(t *testing.T) { //nolint:gocyclo // yeah, it's compl } c, r, err := dialer.Dial(dest.String(), nil) if r != nil { - defer r.Body.Close() + defer func() { + require.NoError(t, r.Body.Close()) + }() } if err != nil && r != nil { body, _ := ioutil.ReadAll(r.Body) @@ -656,7 +640,7 @@ func TestImpersonationProxy(t *testing.T) { //nolint:gocyclo // yeah, it's compl require.Eventually(t, func() bool { // It's okay if this returns RBAC errors because this user has no role bindings. // What we want to see is that the proxy eventually shuts down entirely. - _, err = impersonationProxyViaSquidClientWithoutCredential().CoreV1().Namespaces().List(ctx, metav1.ListOptions{}) + _, err = impersonationProxyViaSquidKubeClientWithoutCredential().CoreV1().Namespaces().List(ctx, metav1.ListOptions{}) return err.Error() == serviceUnavailableViaSquidError }, 20*time.Second, 500*time.Millisecond) } @@ -683,7 +667,7 @@ func TestImpersonationProxy(t *testing.T) { //nolint:gocyclo // yeah, it's compl // impersonation strategy, we should be left with no working strategies. // Given that there are no working strategies, a TokenCredentialRequest which would otherwise work should now // fail, because there is no point handing out credentials that are not going to work for any strategy. - tokenCredentialRequestResponse, err = library.CreateTokenCredentialRequest(ctx, t, credentialRequestSpecWithWorkingCredentials) + tokenCredentialRequestResponse, err := library.CreateTokenCredentialRequest(ctx, t, credentialRequestSpecWithWorkingCredentials) require.NoError(t, err) require.NotNil(t, tokenCredentialRequestResponse.Status.Message, "expected an error message but got nil") @@ -693,6 +677,21 @@ func TestImpersonationProxy(t *testing.T) { //nolint:gocyclo // yeah, it's compl }) } +func expectedWhoAmIRequestResponse(username string, groups []string) *identityv1alpha1.WhoAmIRequest { + return &identityv1alpha1.WhoAmIRequest{ + Status: identityv1alpha1.WhoAmIRequestStatus{ + KubernetesUserInfo: identityv1alpha1.KubernetesUserInfo{ + User: identityv1alpha1.UserInfo{ + Username: username, + UID: "", // no way to impersonate UID: https://github.com/kubernetes/kubernetes/issues/93699 + Groups: groups, + Extra: nil, + }, + }, + }, + } +} + func performImpersonatorDiscovery(ctx context.Context, t *testing.T, env *library.TestEnv, adminConciergeClient versioned.Interface) (string, []byte) { t.Helper() var impersonationProxyURL string @@ -767,6 +766,54 @@ func requireDisabledByConfigurationStrategy(ctx context.Context, t *testing.T, e }, 1*time.Minute, 500*time.Millisecond) } +func credentialAlmostExpired(t *testing.T, credential *loginv1alpha1.TokenCredentialRequest) bool { + t.Helper() + pemBlock, _ := pem.Decode([]byte(credential.Status.Credential.ClientCertificateData)) + parsedCredential, err := x509.ParseCertificate(pemBlock.Bytes) + require.NoError(t, err) + timeRemaining := time.Until(parsedCredential.NotAfter) + if timeRemaining < 2*time.Minute { + t.Logf("The TokenCredentialRequest cred is almost expired and needs to be refreshed. Expires in %s.", timeRemaining) + return true + } + t.Logf("The TokenCredentialRequest cred is good for some more time (%s) so using it.", timeRemaining) + return false +} + +func impersonationProxyRestConfig(credential *loginv1alpha1.ClusterCredential, host string, caData []byte, doubleImpersonateUser string) *rest.Config { + config := rest.Config{ + Host: host, + TLSClientConfig: rest.TLSClientConfig{ + Insecure: caData == nil, + CAData: caData, + CertData: []byte(credential.ClientCertificateData), + KeyData: []byte(credential.ClientKeyData), + }, + // kubectl would set both the client cert and the token, so we'll do that too. + // The Kube API server will ignore the token if the client cert successfully authenticates. + // Only if the client cert is not present or fails to authenticate will it use the token. + // Historically, it works that way because some web browsers will always send your + // corporate-assigned client cert even if it is not valid, and it doesn't want to treat + // that as a failure if you also sent a perfectly good token. + // We would like the impersonation proxy to imitate that behavior, so we test it here. + BearerToken: credential.Token, + } + if doubleImpersonateUser != "" { + config.Impersonate = rest.ImpersonationConfig{UserName: doubleImpersonateUser} + } + return &config +} + +func kubeconfigProxyFunc(t *testing.T, squidProxyURL string) func(req *http.Request) (*url.URL, error) { + return func(req *http.Request) (*url.URL, error) { + t.Helper() + parsedSquidProxyURL, err := url.Parse(squidProxyURL) + require.NoError(t, err) + t.Logf("passing request for %s through proxy %s", req.URL, parsedSquidProxyURL.String()) + return parsedSquidProxyURL, nil + } +} + func configMapForConfig(t *testing.T, env *library.TestEnv, config impersonator.Config) corev1.ConfigMap { t.Helper() configString, err := yaml.Marshal(config) diff --git a/test/integration/whoami_test.go b/test/integration/whoami_test.go index 345347d8..f13c7cc2 100644 --- a/test/integration/whoami_test.go +++ b/test/integration/whoami_test.go @@ -443,11 +443,3 @@ func TestWhoAmI_ImpersonateDirectly(t *testing.T) { whoAmIAnonymous, ) } - -func TestWhoAmI_ImpersonateViaProxy(t *testing.T) { - _ = library.IntegrationEnv(t) - - // TODO: add this test after the impersonation proxy is done - // this should test all forms of auth understood by the proxy (certs, SA token, token cred req, anonymous, etc) - // remember that impersonation does not support UID: https://github.com/kubernetes/kubernetes/issues/93699 -} diff --git a/test/library/client.go b/test/library/client.go index 1d5fd9e8..7371b974 100644 --- a/test/library/client.go +++ b/test/library/client.go @@ -28,6 +28,7 @@ import ( aggregatorclient "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset" auth1alpha1 "go.pinniped.dev/generated/latest/apis/concierge/authentication/v1alpha1" + "go.pinniped.dev/generated/latest/apis/concierge/login/v1alpha1" configv1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/config/v1alpha1" idpv1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/idp/v1alpha1" conciergeclientset "go.pinniped.dev/generated/latest/client/concierge/clientset/versioned" @@ -54,7 +55,9 @@ func NewClientsetForKubeConfig(t *testing.T, kubeConfig string) kubernetes.Inter func NewRestConfigFromKubeconfig(t *testing.T, kubeConfig string) *rest.Config { kubeConfigFile, err := ioutil.TempFile("", "pinniped-cli-test-*") require.NoError(t, err) - defer os.Remove(kubeConfigFile.Name()) + defer func() { + require.NoError(t, os.Remove(kubeConfigFile.Name())) + }() _, err = kubeConfigFile.Write([]byte(kubeConfig)) require.NoError(t, err) @@ -423,6 +426,19 @@ func CreateTestClusterRoleBinding(t *testing.T, subject rbacv1.Subject, roleRef return created } +func CreateTokenCredentialRequest(ctx context.Context, t *testing.T, spec v1alpha1.TokenCredentialRequestSpec) (*v1alpha1.TokenCredentialRequest, error) { + t.Helper() + + client := NewAnonymousConciergeClientset(t) + + ctx, cancel := context.WithTimeout(ctx, time.Minute) + defer cancel() + + return client.LoginV1alpha1().TokenCredentialRequests().Create(ctx, + &v1alpha1.TokenCredentialRequest{Spec: spec}, metav1.CreateOptions{}, + ) +} + func WaitForUserToHaveAccess(t *testing.T, user string, groups []string, shouldHaveAccessTo *authorizationv1.ResourceAttributes) { t.Helper() client := NewKubernetesClientset(t) diff --git a/test/library/credential_request.go b/test/library/credential_request.go deleted file mode 100644 index 44aeaff2..00000000 --- a/test/library/credential_request.go +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright 2021 the Pinniped contributors. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -package library - -import ( - "context" - "testing" - "time" - - v1 "k8s.io/apimachinery/pkg/apis/meta/v1" - - "go.pinniped.dev/generated/latest/apis/concierge/login/v1alpha1" -) - -func CreateTokenCredentialRequest(ctx context.Context, t *testing.T, spec v1alpha1.TokenCredentialRequestSpec) (*v1alpha1.TokenCredentialRequest, error) { - t.Helper() - - client := NewAnonymousConciergeClientset(t) - - ctx, cancel := context.WithTimeout(ctx, time.Minute) - defer cancel() - - return client.LoginV1alpha1().TokenCredentialRequests().Create(ctx, - &v1alpha1.TokenCredentialRequest{Spec: spec}, v1.CreateOptions{}, - ) -}