Fix bug which prevented watches from working through impersonator

Also:
- Changed base64 encoding of impersonator bearer tokens to use
  `base64.StdEncoding` to make it easier for users to manually
  create a token using the unix `base64` command
- Test the headers which are and are not passed through to the Kube API
  by the impersonator more carefully in the unit tests
- More WIP on concierge_impersonation_proxy_test.go

Signed-off-by: Margo Crawford <margaretc@vmware.com>
This commit is contained in:
Ryan Richard 2021-02-22 17:23:11 -08:00 committed by Margo Crawford
parent b8592a361c
commit 80ff5c1f17
9 changed files with 250 additions and 88 deletions

View File

@ -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",

View File

@ -288,5 +288,5 @@ func impersonationProxyTestToken(token string) string {
},
},
})
return base64.RawURLEncoding.EncodeToString(reqJSON)
return base64.StdEncoding.EncodeToString(reqJSON)
}

View File

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

View File

@ -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)
@ -121,6 +73,8 @@ func TestImpersonator(t *testing.T) {
wantHTTPBody string
wantHTTPStatus int
wantLogs []string
wantKubeAPIServerRequestHeaders http.Header
wantKubeAPIServerStatusCode int
expectMockToken func(*testing.T, *mocktokenauthenticator.MockTokenMockRecorder)
}{
{
@ -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,7 +180,6 @@ 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,
@ -243,7 +187,6 @@ func TestImpersonator(t *testing.T) {
},
{
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)}}),
expectMockToken: func(t *testing.T, recorder *mocktokenauthenticator.MockTokenMockRecorder) {
recorder.AuthenticateToken(gomock.Any(), "test-token").Return(nil, false, nil)
@ -255,7 +198,46 @@ func TestImpersonator(t *testing.T) {
// happy path
{
name: "token validates",
getKubeconfig: func() (*rest.Config, error) { return &testServerKubeconfig, nil },
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")
}
})
}
}

View File

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

View File

@ -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

View File

@ -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"

View File

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

View File

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