Add integration test for using WhoAmIRequest through impersonator

This commit is contained in:
Ryan Richard 2021-03-10 16:57:15 -08:00
parent 24396b6af1
commit d13bb07b3e
5 changed files with 174 additions and 146 deletions

4
go.mod
View File

@ -15,7 +15,7 @@ require (
github.com/google/go-cmp v0.5.5 github.com/google/go-cmp v0.5.5
github.com/google/gofuzz v1.2.0 github.com/google/gofuzz v1.2.0
github.com/gorilla/securecookie v1.1.1 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/oleiade/reflections v1.0.1 // indirect
github.com/onsi/ginkgo v1.13.0 // indirect github.com/onsi/ginkgo v1.13.0 // indirect
github.com/ory/fosite v0.38.0 github.com/ory/fosite v0.38.0
@ -27,7 +27,7 @@ require (
github.com/spf13/pflag v1.0.5 github.com/spf13/pflag v1.0.5
github.com/stretchr/testify v1.7.0 github.com/stretchr/testify v1.7.0
golang.org/x/crypto v0.0.0-20201217014255-9d1352758620 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/oauth2 v0.0.0-20200107190931-bf48bf16ab8d
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a golang.org/x/sync v0.0.0-20201207232520-09787c993a3a
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d // indirect golang.org/x/tools v0.0.0-20200825202427-b303f430e36d // indirect

View File

@ -22,7 +22,6 @@ import (
"time" "time"
"github.com/gorilla/websocket" "github.com/gorilla/websocket"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
v1 "k8s.io/api/authorization/v1" v1 "k8s.io/api/authorization/v1"
corev1 "k8s.io/api/core/v1" corev1 "k8s.io/api/core/v1"
@ -38,9 +37,11 @@ import (
"sigs.k8s.io/yaml" "sigs.k8s.io/yaml"
"go.pinniped.dev/generated/latest/apis/concierge/config/v1alpha1" "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" loginv1alpha1 "go.pinniped.dev/generated/latest/apis/concierge/login/v1alpha1"
"go.pinniped.dev/generated/latest/client/concierge/clientset/versioned" "go.pinniped.dev/generated/latest/client/concierge/clientset/versioned"
"go.pinniped.dev/internal/concierge/impersonator" "go.pinniped.dev/internal/concierge/impersonator"
"go.pinniped.dev/internal/kubeclient"
"go.pinniped.dev/internal/testutil" "go.pinniped.dev/internal/testutil"
"go.pinniped.dev/test/library" "go.pinniped.dev/test/library"
) )
@ -59,35 +60,20 @@ func TestImpersonationProxy(t *testing.T) { //nolint:gocyclo // yeah, it's compl
adminClient := library.NewKubernetesClientset(t) adminClient := library.NewKubernetesClientset(t)
adminConciergeClient := library.NewConciergeClientset(t) adminConciergeClient := library.NewConciergeClientset(t)
// Create a WebhookAuthenticator. // Create a WebhookAuthenticator and prepare a TokenCredentialRequestSpec using the authenticator for use later.
authenticator := library.CreateTestWebhookAuthenticator(ctx, t) 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). // 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) 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. // 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) serviceUnavailableViaSquidError := fmt.Sprintf(`Get "https://%s/api/v1/namespaces": Service Unavailable`, proxyServiceEndpoint)
credentialRequestSpecWithWorkingCredentials := loginv1alpha1.TokenCredentialRequestSpec{ var mostRecentTokenCredentialRequestResponse *loginv1alpha1.TokenCredentialRequest
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
refreshCredential := func() *loginv1alpha1.ClusterCredential { refreshCredential := func() *loginv1alpha1.ClusterCredential {
if tokenCredentialRequestResponse == nil || credentialAlmostExpired(tokenCredentialRequestResponse) { if mostRecentTokenCredentialRequestResponse == nil || credentialAlmostExpired(t, mostRecentTokenCredentialRequestResponse) {
var err error var err error
// Make a TokenCredentialRequest. This can either return a cert signed by the Kube API server's CA (e.g. on kind) // 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 // 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. // 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. // 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.NoError(t, err)
require.Nil(t, tokenCredentialRequestResponse.Status.Message, require.Nil(t, mostRecentTokenCredentialRequestResponse.Status.Message,
"expected no error message but got: %s", library.Sdump(tokenCredentialRequestResponse.Status.Message)) "expected no error message but got: %s", library.Sdump(mostRecentTokenCredentialRequestResponse.Status.Message))
require.NotEmpty(t, tokenCredentialRequestResponse.Status.Credential.ClientCertificateData) require.NotEmpty(t, mostRecentTokenCredentialRequestResponse.Status.Credential.ClientCertificateData)
require.NotEmpty(t, tokenCredentialRequestResponse.Status.Credential.ClientKeyData) 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 // 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. // 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 { impersonationProxyViaSquidKubeClientWithoutCredential := func() kubernetes.Interface {
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()
proxyURL := "https://" + proxyServiceEndpoint proxyURL := "https://" + proxyServiceEndpoint
kubeconfig := impersonationProxyRestConfig(&loginv1alpha1.ClusterCredential{}, proxyURL, nil, "") kubeconfig := impersonationProxyRestConfig(&loginv1alpha1.ClusterCredential{}, proxyURL, nil, "")
kubeconfig.Proxy = kubeconfigProxyFunc() kubeconfig.Proxy = kubeconfigProxyFunc(t, env.Proxy)
return library.NewKubeclient(t, kubeconfig).Kubernetes return library.NewKubeclient(t, kubeconfig).Kubernetes
} }
impersonationProxyViaLoadBalancerClient := func(proxyURL string, caData []byte, doubleImpersonateUser string) kubernetes.Interface { newImpersonationProxyClientWithCredentials := func(credentials *loginv1alpha1.ClusterCredential, impersonationProxyURL string, impersonationProxyCACertPEM []byte, doubleImpersonateUser string) *kubeclient.Client {
t.Helper() kubeconfig := impersonationProxyRestConfig(credentials, impersonationProxyURL, impersonationProxyCACertPEM, doubleImpersonateUser)
kubeconfig := impersonationProxyRestConfig(refreshCredential(), proxyURL, caData, doubleImpersonateUser) if !env.HasCapability(library.HasExternalLoadBalancerProvider) {
return library.NewKubeclient(t, kubeconfig).Kubernetes // Send traffic through the Squid proxy
kubeconfig.Proxy = kubeconfigProxyFunc(t, env.Proxy)
}
return library.NewKubeclient(t, kubeconfig)
} }
newImpersonationProxyClient := func(proxyURL string, impersonationProxyCACertPEM []byte, doubleImpersonateUser string) kubernetes.Interface { newImpersonationProxyClient := func(impersonationProxyURL string, impersonationProxyCACertPEM []byte, doubleImpersonateUser string) *kubeclient.Client {
if env.HasCapability(library.HasExternalLoadBalancerProvider) { refreshedCredentials := refreshCredential()
return impersonationProxyViaLoadBalancerClient(proxyURL, impersonationProxyCACertPEM, doubleImpersonateUser) 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)
} }
return impersonationProxyViaSquidClient(proxyURL, 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{}) 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) }, 10*time.Second, 500*time.Millisecond)
// Check that we can't use the impersonation proxy to execute kubectl commands yet. // 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) require.EqualError(t, err, serviceUnavailableViaSquidError)
// Create configuration to make the impersonation proxy turn on with a hard coded endpoint (without a load balancer). // 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 // credentials before they expire. Create a closure to capture the arguments to newImpersonationProxyClient
// so we don't have to keep repeating them. // 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. // 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 { impersonationProxyKubeClient := func() kubernetes.Interface {
return newImpersonationProxyClient(impersonationProxyURL, impersonationProxyCACertPEM, "") return newImpersonationProxyClient(impersonationProxyURL, impersonationProxyCACertPEM, "").Kubernetes
} }
// Test that the user can perform basic actions through the client with their username and group membership // Test that the user can perform basic actions through the client with their username and group membership
// influencing RBAC checks correctly. // influencing RBAC checks correctly.
t.Run( t.Run(
"access as user", "access as user",
library.AccessAsUserTest(ctx, env.TestUser.ExpectedUsername, impersonationProxyClient()), library.AccessAsUserTest(ctx, env.TestUser.ExpectedUsername, impersonationProxyKubeClient()),
) )
for _, group := range env.TestUser.ExpectedGroups { for _, group := range env.TestUser.ExpectedGroups {
group := group group := group
t.Run( t.Run(
"access as group "+group, "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. // Create and start informer to exercise the "watch" verb for us.
informerFactory := k8sinformers.NewSharedInformerFactoryWithOptions( informerFactory := k8sinformers.NewSharedInformerFactoryWithOptions(
impersonationProxyClient(), impersonationProxyKubeClient(),
0, 0,
k8sinformers.WithNamespace(namespace.Name)) k8sinformers.WithNamespace(namespace.Name))
informer := informerFactory.Core().V1().ConfigMaps() 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. // 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}}, &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Name: "configmap-1", Labels: configMapLabels}},
metav1.CreateOptions{}, metav1.CreateOptions{},
) )
require.NoError(t, err) 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}}, &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Name: "configmap-2", Labels: configMapLabels}},
metav1.CreateOptions{}, metav1.CreateOptions{},
) )
require.NoError(t, err) 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}}, &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Name: "configmap-3", Labels: configMapLabels}},
metav1.CreateOptions{}, metav1.CreateOptions{},
) )
@ -334,11 +286,11 @@ func TestImpersonationProxy(t *testing.T) { //nolint:gocyclo // yeah, it's compl
}, 10*time.Second, 50*time.Millisecond) }, 10*time.Second, 50*time.Millisecond)
// Test "get" verb through the impersonation proxy. // 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) require.NoError(t, err)
// Test "list" verb through the impersonation proxy. // 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(), LabelSelector: configMapLabels.String(),
}) })
require.NoError(t, err) 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. // Test "update" verb through the impersonation proxy.
configMap3.Data = map[string]string{"foo": "bar"} 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.NoError(t, err)
require.Equal(t, "bar", updateResult.Data["foo"]) 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) }, 10*time.Second, 50*time.Millisecond)
// Test "patch" verb through the impersonation proxy. // 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", "configmap-3",
types.MergePatchType, types.MergePatchType,
[]byte(`{"data":{"baz":"42"}}`), []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) }, 10*time.Second, 50*time.Millisecond)
// Test "delete" verb through the impersonation proxy. // 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) require.NoError(t, err)
// Make sure that the deleted ConfigMap shows up in the informer's cache. // 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) }, 10*time.Second, 50*time.Millisecond)
// Test "deletecollection" verb through the impersonation proxy. // 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) require.NoError(t, err)
// Make sure that the deleted ConfigMaps shows up in the informer's cache. // 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) }, 10*time.Second, 50*time.Millisecond)
// There should be no ConfigMaps left. // 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(), LabelSelector: configMapLabels.String(),
}) })
require.NoError(t, err) 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 // Make a client which will send requests through the impersonation proxy and will also add
// impersonate headers to the request. // 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. // 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. // 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) require.NoError(t, err)
// Now we'll see what happens when we add an impersonation header to the request. This should generate a // 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. // request similar to the one above, except that it will also have an impersonation header.
_, err = doubleImpersonationClient.CoreV1().Secrets(env.ConciergeNamespace).Get(ctx, impersonationProxyTLSSecretName(env), metav1.GetOptions{}) _, err = doubleImpersonationKubeClient.CoreV1().Secrets(env.ConciergeNamespace).Get(ctx, impersonationProxyTLSSecretName(env), metav1.GetOptions{})
// Double impersonation is not supported yet, so we should get an error. // Double impersonation is not supported yet, so we should get an error.
require.EqualError(t, err, fmt.Sprintf( require.EqualError(t, err, fmt.Sprintf(
`users "other-user-to-impersonate" is forbidden: `+ `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)) 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) { t.Run("kubectl as a client", func(t *testing.T) {
// Create an RBAC rule to allow this user to read/write everything. // Create an RBAC rule to allow this user to read/write everything.
library.CreateTestClusterRoleBinding(t, 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. // Try "kubectl exec" through the impersonation proxy.
echoString := "hello world" echoString := "hello world"
stdout, _, err := runKubectl("exec", "--namespace", env.ConciergeNamespace, podName, "--", "echo", echoString) 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) require.Equal(t, echoString+"\n", stdout)
// run the kubectl port-forward command // 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 // start, but don't wait for the command to finish
err = portForwardCmd.Start() err = portForwardCmd.Start()
require.NoError(t, err) require.NoError(t, err, `"kubectl port-forward" failed`)
// then run curl something against it // then run curl something against it
time.Sleep(time.Second) 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) c, r, err := dialer.Dial(dest.String(), nil)
if r != nil { if r != nil {
defer r.Body.Close() defer func() {
require.NoError(t, r.Body.Close())
}()
} }
if err != nil && r != nil { if err != nil && r != nil {
body, _ := ioutil.ReadAll(r.Body) 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 { require.Eventually(t, func() bool {
// It's okay if this returns RBAC errors because this user has no role bindings. // 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. // 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 return err.Error() == serviceUnavailableViaSquidError
}, 20*time.Second, 500*time.Millisecond) }, 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. // 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 // 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. // 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.NoError(t, err)
require.NotNil(t, tokenCredentialRequestResponse.Status.Message, "expected an error message but got nil") 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) { func performImpersonatorDiscovery(ctx context.Context, t *testing.T, env *library.TestEnv, adminConciergeClient versioned.Interface) (string, []byte) {
t.Helper() t.Helper()
var impersonationProxyURL string var impersonationProxyURL string
@ -767,6 +766,54 @@ func requireDisabledByConfigurationStrategy(ctx context.Context, t *testing.T, e
}, 1*time.Minute, 500*time.Millisecond) }, 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 { func configMapForConfig(t *testing.T, env *library.TestEnv, config impersonator.Config) corev1.ConfigMap {
t.Helper() t.Helper()
configString, err := yaml.Marshal(config) configString, err := yaml.Marshal(config)

View File

@ -443,11 +443,3 @@ func TestWhoAmI_ImpersonateDirectly(t *testing.T) {
whoAmIAnonymous, 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
}

View File

@ -28,6 +28,7 @@ import (
aggregatorclient "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset" aggregatorclient "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset"
auth1alpha1 "go.pinniped.dev/generated/latest/apis/concierge/authentication/v1alpha1" 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" configv1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/config/v1alpha1"
idpv1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/idp/v1alpha1" idpv1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/idp/v1alpha1"
conciergeclientset "go.pinniped.dev/generated/latest/client/concierge/clientset/versioned" 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 { func NewRestConfigFromKubeconfig(t *testing.T, kubeConfig string) *rest.Config {
kubeConfigFile, err := ioutil.TempFile("", "pinniped-cli-test-*") kubeConfigFile, err := ioutil.TempFile("", "pinniped-cli-test-*")
require.NoError(t, err) require.NoError(t, err)
defer os.Remove(kubeConfigFile.Name()) defer func() {
require.NoError(t, os.Remove(kubeConfigFile.Name()))
}()
_, err = kubeConfigFile.Write([]byte(kubeConfig)) _, err = kubeConfigFile.Write([]byte(kubeConfig))
require.NoError(t, err) require.NoError(t, err)
@ -423,6 +426,19 @@ func CreateTestClusterRoleBinding(t *testing.T, subject rbacv1.Subject, roleRef
return created 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) { func WaitForUserToHaveAccess(t *testing.T, user string, groups []string, shouldHaveAccessTo *authorizationv1.ResourceAttributes) {
t.Helper() t.Helper()
client := NewKubernetesClientset(t) client := NewKubernetesClientset(t)

View File

@ -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{},
)
}