diff --git a/cmd/pinniped/cmd/login_oidc.go b/cmd/pinniped/cmd/login_oidc.go index 8b18805a..1f101950 100644 --- a/cmd/pinniped/cmd/login_oidc.go +++ b/cmd/pinniped/cmd/login_oidc.go @@ -294,7 +294,7 @@ func execCredentialForImpersonationProxy( if err != nil { return nil, fmt.Errorf("Error creating TokenCredentialRequest for impersonation proxy: %w", err) } - encodedToken := base64.RawURLEncoding.EncodeToString(reqJSON) + encodedToken := base64.StdEncoding.EncodeToString(reqJSON) cred := &clientauthv1beta1.ExecCredential{ TypeMeta: metav1.TypeMeta{ Kind: "ExecCredential", diff --git a/cmd/pinniped/cmd/login_oidc_test.go b/cmd/pinniped/cmd/login_oidc_test.go index e9a47e8f..198b7519 100644 --- a/cmd/pinniped/cmd/login_oidc_test.go +++ b/cmd/pinniped/cmd/login_oidc_test.go @@ -288,5 +288,5 @@ func impersonationProxyTestToken(token string) string { }, }, }) - return base64.RawURLEncoding.EncodeToString(reqJSON) + return base64.StdEncoding.EncodeToString(reqJSON) } diff --git a/internal/concierge/impersonator/impersonator.go b/internal/concierge/impersonator/impersonator.go index d9275673..e4be50c4 100644 --- a/internal/concierge/impersonator/impersonator.go +++ b/internal/concierge/impersonator/impersonator.go @@ -11,6 +11,7 @@ import ( "net/http/httputil" "net/url" "strings" + "time" "github.com/go-logr/logr" "k8s.io/apimachinery/pkg/runtime" @@ -34,6 +35,7 @@ var allowedHeaders = []string{ "User-Agent", "Connection", "Upgrade", + "Content-Type", } type proxy struct { @@ -68,7 +70,7 @@ func newInternal(cache *authncache.Cache, jsonDecoder runtime.Decoder, log logr. if err != nil { return nil, fmt.Errorf("could not get in-cluster transport config: %w", err) } - kubeTransportConfig.TLS.NextProtos = []string{"http/1.1"} + kubeTransportConfig.TLS.NextProtos = []string{"http/1.1"} // TODO huh? kubeRoundTripper, err := transport.New(kubeTransportConfig) if err != nil { @@ -77,6 +79,7 @@ func newInternal(cache *authncache.Cache, jsonDecoder runtime.Decoder, log logr. reverseProxy := httputil.NewSingleHostReverseProxy(serverURL) reverseProxy.Transport = kubeRoundTripper + reverseProxy.FlushInterval = 200 * time.Millisecond // the "watch" verb will not work without this line return &proxy{ cache: cache, @@ -218,7 +221,7 @@ func getProxyHeaders(userInfo user.Info, requestHeaders http.Header) http.Header } func extractToken(token string, jsonDecoder runtime.Decoder) (*login.TokenCredentialRequest, error) { - tokenCredentialRequestJSON, err := base64.RawURLEncoding.DecodeString(token) + tokenCredentialRequestJSON, err := base64.StdEncoding.DecodeString(token) if err != nil { return nil, fmt.Errorf("invalid base64 in encoded bearer token: %w", err) } diff --git a/internal/concierge/impersonator/impersonator_test.go b/internal/concierge/impersonator/impersonator_test.go index fd86cd18..91136b49 100644 --- a/internal/concierge/impersonator/impersonator_test.go +++ b/internal/concierge/impersonator/impersonator_test.go @@ -9,7 +9,6 @@ import ( "net/http" "net/http/httptest" "net/url" - "reflect" "testing" "github.com/golang/mock/gomock" @@ -22,7 +21,6 @@ import ( "k8s.io/apiserver/pkg/authentication/user" "k8s.io/client-go/rest" "k8s.io/client-go/tools/clientcmd/api" - "k8s.io/client-go/transport" authenticationv1alpha1 "go.pinniped.dev/generated/latest/apis/concierge/authentication/v1alpha1" "go.pinniped.dev/generated/latest/apis/concierge/login" @@ -48,54 +46,8 @@ func TestImpersonator(t *testing.T) { "extra-1": {"some", "extra", "stuff"}, "extra-2": {"some", "more", "extra", "stuff"}, } - testExtraHeaders := map[string]string{ - "extra-1": transport.ImpersonateUserExtraHeaderPrefix + "extra-1", - "extra-2": transport.ImpersonateUserExtraHeaderPrefix + "extra-2", - } validURL, _ := url.Parse("http://pinniped.dev/blah") - testServerCA, testServerURL := testutil.TLSTestServer(t, func(w http.ResponseWriter, r *http.Request) { - // Expect that the request is authenticated based on the kubeconfig credential. - if r.Header.Get("Authorization") != "Bearer some-service-account-token" { - http.Error(w, "expected to see service account token", http.StatusForbidden) - return - } - // Fail if we see the malicious header passed through the proxy (it's not on the allowlist). - if r.Header.Get("Malicious-Header") != "" { - http.Error(w, "didn't expect to see malicious header", http.StatusForbidden) - return - } - // Expect to see the user agent header passed through. - if r.Header.Get("User-Agent") != "test-user-agent" { - http.Error(w, "got unexpected user agent header", http.StatusBadRequest) - return - } - // Ensure impersonation headers are set. - if values := r.Header.Values(transport.ImpersonateUserHeader); len(values) != 1 || values[0] != testUser { - message := fmt.Sprintf("got unexpected %q header: %q", transport.ImpersonateUserHeader, values) - http.Error(w, message, http.StatusBadRequest) - return - } - if values := r.Header.Values(transport.ImpersonateGroupHeader); !reflect.DeepEqual(testGroups, values) { - message := fmt.Sprintf("got unexpected %q headers: %q", transport.ImpersonateGroupHeader, values) - http.Error(w, message, http.StatusBadRequest) - return - } - for testExtraKey, testExtraValues := range testExtra { - header := testExtraHeaders[testExtraKey] - if values := r.Header.Values(header); !reflect.DeepEqual(testExtraValues, values) { - message := fmt.Sprintf("got unexpected %q headers: %q", header, values) - http.Error(w, message, http.StatusBadRequest) - return - } - } - _, _ = w.Write([]byte("successful proxied response")) - }) - testServerKubeconfig := rest.Config{ - Host: testServerURL, - BearerToken: "some-service-account-token", - TLSClientConfig: rest.TLSClientConfig{CAData: []byte(testServerCA)}, - } newRequest := func(h http.Header) *http.Request { r, err := http.NewRequestWithContext(context.Background(), http.MethodGet, validURL.String(), nil) require.NoError(t, err) @@ -113,15 +65,17 @@ func TestImpersonator(t *testing.T) { } tests := []struct { - name string - apiGroupOverride string - getKubeconfig func() (*rest.Config, error) - wantCreationErr string - request *http.Request - wantHTTPBody string - wantHTTPStatus int - wantLogs []string - expectMockToken func(*testing.T, *mocktokenauthenticator.MockTokenMockRecorder) + name string + apiGroupOverride string + getKubeconfig func() (*rest.Config, error) + wantCreationErr string + request *http.Request + wantHTTPBody string + wantHTTPStatus int + wantLogs []string + wantKubeAPIServerRequestHeaders http.Header + wantKubeAPIServerStatusCode int + expectMockToken func(*testing.T, *mocktokenauthenticator.MockTokenMockRecorder) }{ { name: "fail to get in-cluster config", @@ -162,7 +116,6 @@ func TestImpersonator(t *testing.T) { }, { name: "Impersonate-User header already in request", - getKubeconfig: func() (*rest.Config, error) { return &testServerKubeconfig, nil }, request: newRequest(map[string][]string{"Impersonate-User": {"some-user"}}), wantHTTPBody: "impersonation header already exists\n", wantHTTPStatus: http.StatusBadRequest, @@ -170,7 +123,6 @@ func TestImpersonator(t *testing.T) { }, { name: "Impersonate-Group header already in request", - getKubeconfig: func() (*rest.Config, error) { return &testServerKubeconfig, nil }, request: newRequest(map[string][]string{"Impersonate-Group": {"some-group"}}), wantHTTPBody: "impersonation header already exists\n", wantHTTPStatus: http.StatusBadRequest, @@ -178,7 +130,6 @@ func TestImpersonator(t *testing.T) { }, { name: "Impersonate-Extra header already in request", - getKubeconfig: func() (*rest.Config, error) { return &testServerKubeconfig, nil }, request: newRequest(map[string][]string{"Impersonate-Extra-something": {"something"}}), wantHTTPBody: "impersonation header already exists\n", wantHTTPStatus: http.StatusBadRequest, @@ -186,7 +137,6 @@ func TestImpersonator(t *testing.T) { }, { name: "missing authorization header", - getKubeconfig: func() (*rest.Config, error) { return &testServerKubeconfig, nil }, request: newRequest(map[string][]string{}), wantHTTPBody: "invalid token encoding\n", wantHTTPStatus: http.StatusBadRequest, @@ -194,7 +144,6 @@ func TestImpersonator(t *testing.T) { }, { name: "authorization header missing bearer prefix", - getKubeconfig: func() (*rest.Config, error) { return &testServerKubeconfig, nil }, request: newRequest(map[string][]string{"Authorization": {impersonationtoken.Make(t, "test-token", &goodAuthenticator, defaultAPIGroup)}}), wantHTTPBody: "invalid token encoding\n", wantHTTPStatus: http.StatusBadRequest, @@ -202,7 +151,6 @@ func TestImpersonator(t *testing.T) { }, { name: "token is not base64 encoded", - getKubeconfig: func() (*rest.Config, error) { return &testServerKubeconfig, nil }, request: newRequest(map[string][]string{"Authorization": {"Bearer !!!"}}), wantHTTPBody: "invalid token encoding\n", wantHTTPStatus: http.StatusBadRequest, @@ -210,16 +158,14 @@ func TestImpersonator(t *testing.T) { }, { name: "base64 encoded token is not valid json", - getKubeconfig: func() (*rest.Config, error) { return &testServerKubeconfig, nil }, - request: newRequest(map[string][]string{"Authorization": {"Bearer abc"}}), + request: newRequest(map[string][]string{"Authorization": {"Bearer aGVsbG8gd29ybGQK"}}), // aGVsbG8gd29ybGQK is "hello world" base64 encoded wantHTTPBody: "invalid token encoding\n", wantHTTPStatus: http.StatusBadRequest, - wantLogs: []string{"\"error\"=\"invalid object encoded in bearer token: couldn't get version/kind; json parse error: invalid character 'i' looking for beginning of value\" \"msg\"=\"invalid token encoding\" \"method\"=\"GET\" \"url\"=\"http://pinniped.dev/blah\""}, + wantLogs: []string{"\"error\"=\"invalid object encoded in bearer token: couldn't get version/kind; json parse error: invalid character 'h' looking for beginning of value\" \"msg\"=\"invalid token encoding\" \"method\"=\"GET\" \"url\"=\"http://pinniped.dev/blah\""}, }, { name: "base64 encoded token is encoded with default api group but we are expecting custom api group", apiGroupOverride: customAPIGroup, - getKubeconfig: func() (*rest.Config, error) { return &testServerKubeconfig, nil }, request: newRequest(map[string][]string{"Authorization": {"Bearer " + impersonationtoken.Make(t, "test-token", &goodAuthenticator, defaultAPIGroup)}}), wantHTTPBody: "invalid token encoding\n", wantHTTPStatus: http.StatusBadRequest, @@ -227,7 +173,6 @@ func TestImpersonator(t *testing.T) { }, { name: "base64 encoded token is encoded with custom api group but we are expecting default api group", - getKubeconfig: func() (*rest.Config, error) { return &testServerKubeconfig, nil }, request: newRequest(map[string][]string{"Authorization": {"Bearer " + impersonationtoken.Make(t, "test-token", &goodAuthenticator, customAPIGroup)}}), wantHTTPBody: "invalid token encoding\n", wantHTTPStatus: http.StatusBadRequest, @@ -235,16 +180,14 @@ func TestImpersonator(t *testing.T) { }, { name: "token could not be authenticated", - getKubeconfig: func() (*rest.Config, error) { return &testServerKubeconfig, nil }, request: newRequest(map[string][]string{"Authorization": {"Bearer " + impersonationtoken.Make(t, "", &badAuthenticator, defaultAPIGroup)}}), wantHTTPBody: "invalid token\n", wantHTTPStatus: http.StatusUnauthorized, wantLogs: []string{"\"error\"=\"no such authenticator\" \"msg\"=\"received invalid token\" \"authenticator\"={\"apiGroup\":\"authentication.concierge.pinniped.dev\",\"kind\":\"\",\"name\":\"\"} \"method\"=\"GET\" \"url\"=\"http://pinniped.dev/blah\""}, }, { - name: "token authenticates as nil", - getKubeconfig: func() (*rest.Config, error) { return &testServerKubeconfig, nil }, - request: newRequest(map[string][]string{"Authorization": {"Bearer " + impersonationtoken.Make(t, "test-token", &goodAuthenticator, defaultAPIGroup)}}), + name: "token authenticates as nil", + request: newRequest(map[string][]string{"Authorization": {"Bearer " + impersonationtoken.Make(t, "test-token", &goodAuthenticator, defaultAPIGroup)}}), expectMockToken: func(t *testing.T, recorder *mocktokenauthenticator.MockTokenMockRecorder) { recorder.AuthenticateToken(gomock.Any(), "test-token").Return(nil, false, nil) }, @@ -254,8 +197,47 @@ func TestImpersonator(t *testing.T) { }, // happy path { - name: "token validates", - getKubeconfig: func() (*rest.Config, error) { return &testServerKubeconfig, nil }, + name: "token validates", + request: newRequest(map[string][]string{ + "Authorization": {"Bearer " + impersonationtoken.Make(t, "test-token", &goodAuthenticator, defaultAPIGroup)}, + "User-Agent": {"test-user-agent"}, + "Accept": {"some-accepted-format"}, + "Accept-Encoding": {"some-accepted-encoding"}, + "Connection": {"Upgrade"}, // the value "Upgrade" is handled in a special way by `httputil.NewSingleHostReverseProxy` + "Upgrade": {"some-upgrade"}, + "Content-Type": {"some-type"}, + "Content-Length": {"some-length"}, + "Malicious-Header": {"test-header-value-1"}, // this header should not be forwarded to the Kube API server + }), + expectMockToken: func(t *testing.T, recorder *mocktokenauthenticator.MockTokenMockRecorder) { + userInfo := user.DefaultInfo{ + Name: testUser, + Groups: testGroups, + UID: "test-uid", + Extra: testExtra, + } + response := &authenticator.Response{User: &userInfo} + recorder.AuthenticateToken(gomock.Any(), "test-token").Return(response, true, nil) + }, + wantKubeAPIServerRequestHeaders: map[string][]string{ + "Authorization": {"Bearer some-service-account-token"}, + "Impersonate-Extra-Extra-1": {"some", "extra", "stuff"}, + "Impersonate-Extra-Extra-2": {"some", "more", "extra", "stuff"}, + "Impersonate-Group": {"test-group-1", "test-group-2"}, + "Impersonate-User": {"test-user"}, + "User-Agent": {"test-user-agent"}, + "Accept": {"some-accepted-format"}, + "Accept-Encoding": {"some-accepted-encoding"}, + "Connection": {"Upgrade"}, + "Upgrade": {"some-upgrade"}, + "Content-Type": {"some-type"}, + }, + wantHTTPBody: "successful proxied response", + wantHTTPStatus: http.StatusOK, + wantLogs: []string{"\"level\"=0 \"msg\"=\"proxying authenticated request\" \"authenticator\"={\"apiGroup\":\"authentication.concierge.pinniped.dev\",\"kind\":\"\",\"name\":\"authenticator-one\"} \"method\"=\"GET\" \"url\"=\"http://pinniped.dev/blah\" \"userID\"=\"test-uid\""}, + }, + { + name: "token validates and the kube API request returns an error", request: newRequest(map[string][]string{ "Authorization": {"Bearer " + impersonationtoken.Make(t, "test-token", &goodAuthenticator, defaultAPIGroup)}, "Malicious-Header": {"test-header-value-1"}, @@ -271,14 +253,22 @@ func TestImpersonator(t *testing.T) { response := &authenticator.Response{User: &userInfo} recorder.AuthenticateToken(gomock.Any(), "test-token").Return(response, true, nil) }, - wantHTTPBody: "successful proxied response", - wantHTTPStatus: http.StatusOK, + wantKubeAPIServerStatusCode: http.StatusNotFound, + wantKubeAPIServerRequestHeaders: map[string][]string{ + "Accept-Encoding": {"gzip"}, // because the rest client used in this test does not disable compression + "Authorization": {"Bearer some-service-account-token"}, + "Impersonate-Extra-Extra-1": {"some", "extra", "stuff"}, + "Impersonate-Extra-Extra-2": {"some", "more", "extra", "stuff"}, + "Impersonate-Group": {"test-group-1", "test-group-2"}, + "Impersonate-User": {"test-user"}, + "User-Agent": {"test-user-agent"}, + }, + wantHTTPStatus: http.StatusNotFound, wantLogs: []string{"\"level\"=0 \"msg\"=\"proxying authenticated request\" \"authenticator\"={\"apiGroup\":\"authentication.concierge.pinniped.dev\",\"kind\":\"\",\"name\":\"authenticator-one\"} \"method\"=\"GET\" \"url\"=\"http://pinniped.dev/blah\" \"userID\"=\"test-uid\""}, }, { name: "token validates with custom api group", apiGroupOverride: customAPIGroup, - getKubeconfig: func() (*rest.Config, error) { return &testServerKubeconfig, nil }, request: newRequest(map[string][]string{ "Authorization": {"Bearer " + impersonationtoken.Make(t, "test-token", &goodAuthenticator, customAPIGroup)}, "Malicious-Header": {"test-header-value-1"}, @@ -294,6 +284,15 @@ func TestImpersonator(t *testing.T) { response := &authenticator.Response{User: &userInfo} recorder.AuthenticateToken(gomock.Any(), "test-token").Return(response, true, nil) }, + wantKubeAPIServerRequestHeaders: map[string][]string{ + "Accept-Encoding": {"gzip"}, // because the rest client used in this test does not disable compression + "Authorization": {"Bearer some-service-account-token"}, + "Impersonate-Extra-Extra-1": {"some", "extra", "stuff"}, + "Impersonate-Extra-Extra-2": {"some", "more", "extra", "stuff"}, + "Impersonate-Group": {"test-group-1", "test-group-2"}, + "Impersonate-User": {"test-user"}, + "User-Agent": {"test-user-agent"}, + }, wantHTTPBody: "successful proxied response", wantHTTPStatus: http.StatusOK, wantLogs: []string{"\"level\"=0 \"msg\"=\"proxying authenticated request\" \"authenticator\"={\"apiGroup\":\"authentication.concierge.pinniped.dev\",\"kind\":\"\",\"name\":\"authenticator-one\"} \"method\"=\"GET\" \"url\"=\"http://pinniped.dev/blah\" \"userID\"=\"test-uid\""}, @@ -312,6 +311,32 @@ func TestImpersonator(t *testing.T) { } }() + if tt.wantKubeAPIServerStatusCode == 0 { + tt.wantKubeAPIServerStatusCode = http.StatusOK + } + + serverWasCalled := false + serverSawHeaders := http.Header{} + testServerCA, testServerURL := testutil.TLSTestServer(t, func(w http.ResponseWriter, r *http.Request) { + serverWasCalled = true + serverSawHeaders = r.Header + if tt.wantKubeAPIServerStatusCode != http.StatusOK { + w.WriteHeader(tt.wantKubeAPIServerStatusCode) + } else { + _, _ = w.Write([]byte("successful proxied response")) + } + }) + testServerKubeconfig := rest.Config{ + Host: testServerURL, + BearerToken: "some-service-account-token", + TLSClientConfig: rest.TLSClientConfig{CAData: []byte(testServerCA)}, + } + if tt.getKubeconfig == nil { + tt.getKubeconfig = func() (*rest.Config, error) { + return &testServerKubeconfig, nil + } + } + // stole this from cache_test, hopefully it is sufficient cacheWithMockAuthenticator := authncache.New() ctrl := gomock.NewController(t) @@ -349,6 +374,13 @@ func TestImpersonator(t *testing.T) { if tt.wantLogs != nil { require.Equal(t, tt.wantLogs, testLog.Lines()) } + + if tt.wantHTTPStatus == http.StatusOK || tt.wantKubeAPIServerStatusCode != http.StatusOK { + require.True(t, serverWasCalled, "Should have proxied the request to the Kube API server, but didn't") + require.Equal(t, tt.wantKubeAPIServerRequestHeaders, serverSawHeaders) + } else { + require.False(t, serverWasCalled, "Should not have proxied the request to the Kube API server, but did") + } }) } } diff --git a/internal/testutil/impersonationtoken/impersonationtoken.go b/internal/testutil/impersonationtoken/impersonationtoken.go index e2c47dd8..d9bcc69e 100644 --- a/internal/testutil/impersonationtoken/impersonationtoken.go +++ b/internal/testutil/impersonationtoken/impersonationtoken.go @@ -63,5 +63,5 @@ func Make( reqJSON, err := runtime.Encode(respInfo.PrettySerializer, &tokenCredentialRequest) require.NoError(t, err) - return base64.RawURLEncoding.EncodeToString(reqJSON) + return base64.StdEncoding.EncodeToString(reqJSON) } diff --git a/test/integration/concierge_impersonation_proxy_test.go b/test/integration/concierge_impersonation_proxy_test.go index 637f2002..935a1b5e 100644 --- a/test/integration/concierge_impersonation_proxy_test.go +++ b/test/integration/concierge_impersonation_proxy_test.go @@ -12,8 +12,12 @@ import ( "time" "github.com/stretchr/testify/require" + v1 "k8s.io/api/authorization/v1" corev1 "k8s.io/api/core/v1" + rbacv1 "k8s.io/api/rbac/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + k8sinformers "k8s.io/client-go/informers" "k8s.io/client-go/kubernetes" "k8s.io/client-go/rest" "sigs.k8s.io/yaml" @@ -124,12 +128,94 @@ func TestImpersonationProxy(t *testing.T) { } t.Run("watching all the verbs", func(t *testing.T) { - // Start a watch in a informer. - // Create an RBAC rule to allow this user to read/write everything. - // t.Cleanup Delete the RBAC rule. // Create a namespace, because it will be easier to deletecollection if we have a namespace. // t.Cleanup Delete the namespace. - // Then "create" several Secrets. + namespace, err := adminClient.CoreV1().Namespaces().Create(ctx, &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{GenerateName: "impersonation-integration-test-"}, + }, metav1.CreateOptions{}) + require.NoError(t, err) + 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, + rbacv1.Subject{ + Kind: rbacv1.UserKind, + APIGroup: rbacv1.GroupName, + Name: env.TestUser.ExpectedUsername, + }, + rbacv1.RoleRef{ + Kind: "ClusterRole", + APIGroup: rbacv1.GroupName, + Name: "cluster-admin", + }, + ) + library.WaitForUserToHaveAccess(t, env.TestUser.ExpectedUsername, []string{}, &v1.ResourceAttributes{ + Namespace: namespace.Name, + Verb: "create", + Group: "", + Version: "v1", + Resource: "configmaps", + }) + + // Create and start informer. + informerFactory := k8sinformers.NewSharedInformerFactoryWithOptions( + impersonationProxyClient, + 0, + k8sinformers.WithNamespace(namespace.Name)) + informer := informerFactory.Core().V1().ConfigMaps() + informer.Informer() // makes sure that the informer will cache + stopChannel := make(chan struct{}) + informerFactory.Start(stopChannel) + t.Cleanup(func() { + stopChannel <- struct{}{} + }) + informerFactory.WaitForCacheSync(ctx.Done()) + + // Test "create" verb. + _, err = impersonationProxyClient.CoreV1().ConfigMaps(namespace.Name).Create( + ctx, + &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Name: "configmap-1"}}, + metav1.CreateOptions{}, + ) + require.NoError(t, err) + require.Eventually(t, func() bool { + _, err = informer.Lister().ConfigMaps(namespace.Name).Get("configmap-1") + return err == nil + }, 10*time.Second, 500*time.Millisecond) + + _, err = impersonationProxyClient.CoreV1().ConfigMaps(namespace.Name).Create( + ctx, + &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Name: "configmap-2"}}, + metav1.CreateOptions{}, + ) + require.NoError(t, err) + require.Eventually(t, func() bool { + _, err = informer.Lister().ConfigMaps(namespace.Name).Get("configmap-2") + return err == nil + }, 10*time.Second, 500*time.Millisecond) + + _, err = impersonationProxyClient.CoreV1().ConfigMaps(namespace.Name).Create( + ctx, + &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Name: "configmap-3"}}, + metav1.CreateOptions{}, + ) + require.NoError(t, err) + require.Eventually(t, func() bool { + _, err = informer.Lister().ConfigMaps(namespace.Name).Get("configmap-3") + return err == nil + }, 10*time.Second, 500*time.Millisecond) + + require.Eventually(t, func() bool { + configmaps, err := informer.Lister().ConfigMaps(namespace.Name).List(labels.Everything()) + return err == nil && len(configmaps) == 3 + }, 10*time.Second, 500*time.Millisecond) + + // TODO, test more verbs // "get" one them. // "list" them all. // "update" one of them. @@ -137,7 +223,6 @@ func TestImpersonationProxy(t *testing.T) { // "delete" one of them. // "deletecollection" all of them. // Make sure the watch sees all of those actions. - // Close the informer. }) // Update configuration to force the proxy to disabled mode diff --git a/test/integration/e2e_test.go b/test/integration/e2e_test.go index e0b2cc75..092d46a7 100644 --- a/test/integration/e2e_test.go +++ b/test/integration/e2e_test.go @@ -23,6 +23,7 @@ import ( coreosoidc "github.com/coreos/go-oidc/v3/oidc" "github.com/stretchr/testify/require" + authorizationv1 "k8s.io/api/authorization/v1" corev1 "k8s.io/api/core/v1" rbacv1 "k8s.io/api/rbac/v1" @@ -135,6 +136,12 @@ func TestE2EFullIntegration(t *testing.T) { rbacv1.Subject{Kind: rbacv1.UserKind, APIGroup: rbacv1.GroupName, Name: env.SupervisorTestUpstream.Username}, rbacv1.RoleRef{Kind: "ClusterRole", APIGroup: rbacv1.GroupName, Name: "view"}, ) + library.WaitForUserToHaveAccess(t, env.SupervisorTestUpstream.Username, []string{}, &authorizationv1.ResourceAttributes{ + Verb: "get", + Group: "", + Version: "v1", + Resource: "namespaces", + }) // Use a specific session cache for this test. sessionCachePath := tempDir + "/sessions.yaml" diff --git a/test/library/access.go b/test/library/access.go index 7aacf6b1..e953cff8 100644 --- a/test/library/access.go +++ b/test/library/access.go @@ -1,4 +1,4 @@ -// Copyright 2020 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package library @@ -13,6 +13,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + authorizationv1 "k8s.io/api/authorization/v1" v1 "k8s.io/api/core/v1" rbacv1 "k8s.io/api/rbac/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -137,6 +138,12 @@ func addTestClusterUserCanViewEverythingRoleBinding(t *testing.T, testUsername s Name: "view", }, ) + WaitForUserToHaveAccess(t, testUsername, []string{}, &authorizationv1.ResourceAttributes{ + Verb: "get", + Group: "", + Version: "v1", + Resource: "namespaces", + }) } func addTestClusterGroupCanViewEverythingRoleBinding(t *testing.T, testGroup string) { @@ -154,6 +161,12 @@ func addTestClusterGroupCanViewEverythingRoleBinding(t *testing.T, testGroup str Name: "view", }, ) + WaitForUserToHaveAccess(t, "", []string{testGroup}, &authorizationv1.ResourceAttributes{ + Verb: "get", + Group: "", + Version: "v1", + Resource: "namespaces", + }) } func runKubectlGetNamespaces(t *testing.T, kubeConfigYAML string) (string, error) { diff --git a/test/library/client.go b/test/library/client.go index 6d7facb6..fb5a4b63 100644 --- a/test/library/client.go +++ b/test/library/client.go @@ -17,6 +17,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + authorizationv1 "k8s.io/api/authorization/v1" corev1 "k8s.io/api/core/v1" rbacv1 "k8s.io/api/rbac/v1" k8serrors "k8s.io/apimachinery/pkg/api/errors" @@ -434,6 +435,27 @@ func CreateTestClusterRoleBinding(t *testing.T, subject rbacv1.Subject, roleRef return created } +func WaitForUserToHaveAccess(t *testing.T, user string, groups []string, shouldHaveAccessTo *authorizationv1.ResourceAttributes) { + t.Helper() + client := NewKubernetesClientset(t) + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + RequireEventuallyWithoutError(t, func() (bool, error) { + subjectAccessReview, err := client.AuthorizationV1().SubjectAccessReviews().Create(ctx, + &authorizationv1.SubjectAccessReview{ + Spec: authorizationv1.SubjectAccessReviewSpec{ + ResourceAttributes: shouldHaveAccessTo, + User: user, + Groups: groups, + }}, metav1.CreateOptions{}) + if err != nil { + return false, err + } + return subjectAccessReview.Status.Allowed, nil + }, 10*time.Second, 500*time.Millisecond) +} + func testObjectMeta(t *testing.T, baseName string) metav1.ObjectMeta { return metav1.ObjectMeta{ GenerateName: fmt.Sprintf("test-%s-", baseName),