From c853707889ef5b9e0c69d4fe003b1b88fa94e83a Mon Sep 17 00:00:00 2001 From: Margo Crawford Date: Tue, 9 Mar 2021 16:58:44 -0800 Subject: [PATCH] Added integration test for using websockets via the impersonation proxy Tested that this test passed when using the kube api server directly, so it's just the impersonation proxy that must be improved. --- go.mod | 1 + go.sum | 2 + .../concierge_impersonation_proxy_test.go | 83 ++++++++++++++++--- 3 files changed, 75 insertions(+), 11 deletions(-) diff --git a/go.mod b/go.mod index a1c0e7e2..0d15fb2a 100644 --- a/go.mod +++ b/go.mod @@ -26,6 +26,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 // 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/go.sum b/go.sum index 041d4e99..33664938 100644 --- a/go.sum +++ b/go.sum @@ -1178,6 +1178,8 @@ golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81R golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210119194325-5f4716e94777 h1:003p0dJM77cxMSyCPFphvZf/Y5/NXf5fzg6ufd1/Oew= golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 h1:qWPm9rbaAMKs8Bq/9LRpbMqxWRVUAQwMI9fVrssnTfw= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181003184128-c57b0facaced/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= diff --git a/test/integration/concierge_impersonation_proxy_test.go b/test/integration/concierge_impersonation_proxy_test.go index d9c2742d..06f0a55d 100644 --- a/test/integration/concierge_impersonation_proxy_test.go +++ b/test/integration/concierge_impersonation_proxy_test.go @@ -6,7 +6,10 @@ package integration import ( "bytes" "context" + "crypto/tls" + "crypto/x509" "encoding/base64" + "encoding/json" "fmt" "io/ioutil" "net/http" @@ -18,6 +21,8 @@ import ( "testing" "time" + "golang.org/x/net/websocket" + "github.com/stretchr/testify/require" v1 "k8s.io/api/authorization/v1" corev1 "k8s.io/api/core/v1" @@ -26,6 +31,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/watch" k8sinformers "k8s.io/client-go/informers" "k8s.io/client-go/kubernetes" "k8s.io/client-go/rest" @@ -181,19 +187,20 @@ func TestImpersonationProxy(t *testing.T) { ) } + // Create a namespace, because it will be easier to exercise "deletecollection" if we have a namespace. + namespace, err := adminClient.CoreV1().Namespaces().Create(ctx, &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{GenerateName: "impersonation-integration-test-"}, + }, metav1.CreateOptions{}) + require.NoError(t, err) + // Schedule the namespace for cleanup. + t.Cleanup(func() { + t.Logf("cleaning up test namespace %s", namespace.Name) + err = adminClient.CoreV1().Namespaces().Delete(context.Background(), namespace.Name, metav1.DeleteOptions{}) + require.NoError(t, err) + }) + // Try more Kube API verbs through the impersonation proxy. t.Run("watching all the basic verbs", func(t *testing.T) { - // Create a namespace, because it will be easier to exercise "deletecollection" if we have a namespace. - namespace, err := adminClient.CoreV1().Namespaces().Create(ctx, &corev1.Namespace{ - ObjectMeta: metav1.ObjectMeta{GenerateName: "impersonation-integration-test-"}, - }, metav1.CreateOptions{}) - require.NoError(t, err) - // Schedule the namespace for cleanup. - t.Cleanup(func() { - t.Logf("cleaning up test namespace %s", namespace.Name) - err = adminClient.CoreV1().Namespaces().Delete(context.Background(), namespace.Name, metav1.DeleteOptions{}) - require.NoError(t, err) - }) // Create an RBAC rule to allow this user to read/write everything. library.CreateTestClusterRoleBinding(t, @@ -478,6 +485,54 @@ func TestImpersonationProxy(t *testing.T) { require.Contains(t, curlStdOut.String(), "\"forbidden: User \\\"system:anonymous\\\" cannot get path \\\"/\\\"\"") }) + t.Run("websocket client", func(t *testing.T) { + dest, _ := url.Parse(impersonationProxyURL) + dest.Scheme = "wss" + dest.Path = "/api/v1/namespaces/" + namespace.Name + "/configmaps" + dest.RawQuery = "watch=1&resourceVersion=0" + origin, _ := url.Parse("http://localhost") + + rootCAs := x509.NewCertPool() + rootCAs.AppendCertsFromPEM(impersonationProxyCACertPEM) + tlsConfig := &tls.Config{ + RootCAs: rootCAs, + } + + websocketConfig := websocket.Config{ + Location: dest, + Origin: origin, + TlsConfig: tlsConfig, + Version: 13, + Header: http.Header(make(map[string][]string)), + } + ws, err := websocket.DialConfig(&websocketConfig) + if err != nil { + t.Fatalf("failed to dial websocket: %v", err) + } + + // perform a create through the admin client + _, err = adminClient.CoreV1().ConfigMaps(namespace.Name).Create(ctx, + &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Name: "configmap-1"}}, + metav1.CreateOptions{}, + ) + require.NoError(t, err) + + // see if the websocket client received an event for the create + var got watchJSON + err = websocket.JSON.Receive(ws, &got) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + if got.Type != watch.Added { + t.Errorf("Unexpected type: %v", got.Type) + } + + var createConfigMap corev1.ConfigMap + err = json.Unmarshal(got.Object, &createConfigMap) + require.NoError(t, err) + require.Equal(t, "configmap-1", createConfigMap.Name) + }) + // Update configuration to force the proxy to disabled mode configMap := configMapForConfig(t, env, impersonator.Config{Mode: impersonator.ModeDisabled}) if env.HasCapability(library.HasExternalLoadBalancerProvider) { @@ -649,3 +704,9 @@ func impersonationProxyLoadBalancerName(env *library.TestEnv) string { func credentialIssuerName(env *library.TestEnv) string { return env.ConciergeAppName + "-config" } + +// watchJSON defines the expected JSON wire equivalent of watch.Event +type watchJSON struct { + Type watch.EventType `json:"type,omitempty"` + Object json.RawMessage `json:"object,omitempty"` +}