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:
parent
b8592a361c
commit
80ff5c1f17
@ -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",
|
||||
|
@ -288,5 +288,5 @@ func impersonationProxyTestToken(token string) string {
|
||||
},
|
||||
},
|
||||
})
|
||||
return base64.RawURLEncoding.EncodeToString(reqJSON)
|
||||
return base64.StdEncoding.EncodeToString(reqJSON)
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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"
|
||||
|
@ -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) {
|
||||
|
@ -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),
|
||||
|
Loading…
Reference in New Issue
Block a user