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"` +}