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 {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("Error creating TokenCredentialRequest for impersonation proxy: %w", err)
|
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{
|
cred := &clientauthv1beta1.ExecCredential{
|
||||||
TypeMeta: metav1.TypeMeta{
|
TypeMeta: metav1.TypeMeta{
|
||||||
Kind: "ExecCredential",
|
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/http/httputil"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/go-logr/logr"
|
"github.com/go-logr/logr"
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
@ -34,6 +35,7 @@ var allowedHeaders = []string{
|
|||||||
"User-Agent",
|
"User-Agent",
|
||||||
"Connection",
|
"Connection",
|
||||||
"Upgrade",
|
"Upgrade",
|
||||||
|
"Content-Type",
|
||||||
}
|
}
|
||||||
|
|
||||||
type proxy struct {
|
type proxy struct {
|
||||||
@ -68,7 +70,7 @@ func newInternal(cache *authncache.Cache, jsonDecoder runtime.Decoder, log logr.
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("could not get in-cluster transport config: %w", err)
|
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)
|
kubeRoundTripper, err := transport.New(kubeTransportConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -77,6 +79,7 @@ func newInternal(cache *authncache.Cache, jsonDecoder runtime.Decoder, log logr.
|
|||||||
|
|
||||||
reverseProxy := httputil.NewSingleHostReverseProxy(serverURL)
|
reverseProxy := httputil.NewSingleHostReverseProxy(serverURL)
|
||||||
reverseProxy.Transport = kubeRoundTripper
|
reverseProxy.Transport = kubeRoundTripper
|
||||||
|
reverseProxy.FlushInterval = 200 * time.Millisecond // the "watch" verb will not work without this line
|
||||||
|
|
||||||
return &proxy{
|
return &proxy{
|
||||||
cache: cache,
|
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) {
|
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 {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("invalid base64 in encoded bearer token: %w", err)
|
return nil, fmt.Errorf("invalid base64 in encoded bearer token: %w", err)
|
||||||
}
|
}
|
||||||
|
@ -9,7 +9,6 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"net/url"
|
"net/url"
|
||||||
"reflect"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/golang/mock/gomock"
|
"github.com/golang/mock/gomock"
|
||||||
@ -22,7 +21,6 @@ import (
|
|||||||
"k8s.io/apiserver/pkg/authentication/user"
|
"k8s.io/apiserver/pkg/authentication/user"
|
||||||
"k8s.io/client-go/rest"
|
"k8s.io/client-go/rest"
|
||||||
"k8s.io/client-go/tools/clientcmd/api"
|
"k8s.io/client-go/tools/clientcmd/api"
|
||||||
"k8s.io/client-go/transport"
|
|
||||||
|
|
||||||
authenticationv1alpha1 "go.pinniped.dev/generated/latest/apis/concierge/authentication/v1alpha1"
|
authenticationv1alpha1 "go.pinniped.dev/generated/latest/apis/concierge/authentication/v1alpha1"
|
||||||
"go.pinniped.dev/generated/latest/apis/concierge/login"
|
"go.pinniped.dev/generated/latest/apis/concierge/login"
|
||||||
@ -48,54 +46,8 @@ func TestImpersonator(t *testing.T) {
|
|||||||
"extra-1": {"some", "extra", "stuff"},
|
"extra-1": {"some", "extra", "stuff"},
|
||||||
"extra-2": {"some", "more", "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")
|
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 {
|
newRequest := func(h http.Header) *http.Request {
|
||||||
r, err := http.NewRequestWithContext(context.Background(), http.MethodGet, validURL.String(), nil)
|
r, err := http.NewRequestWithContext(context.Background(), http.MethodGet, validURL.String(), nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@ -121,6 +73,8 @@ func TestImpersonator(t *testing.T) {
|
|||||||
wantHTTPBody string
|
wantHTTPBody string
|
||||||
wantHTTPStatus int
|
wantHTTPStatus int
|
||||||
wantLogs []string
|
wantLogs []string
|
||||||
|
wantKubeAPIServerRequestHeaders http.Header
|
||||||
|
wantKubeAPIServerStatusCode int
|
||||||
expectMockToken func(*testing.T, *mocktokenauthenticator.MockTokenMockRecorder)
|
expectMockToken func(*testing.T, *mocktokenauthenticator.MockTokenMockRecorder)
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
@ -162,7 +116,6 @@ func TestImpersonator(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Impersonate-User header already in request",
|
name: "Impersonate-User header already in request",
|
||||||
getKubeconfig: func() (*rest.Config, error) { return &testServerKubeconfig, nil },
|
|
||||||
request: newRequest(map[string][]string{"Impersonate-User": {"some-user"}}),
|
request: newRequest(map[string][]string{"Impersonate-User": {"some-user"}}),
|
||||||
wantHTTPBody: "impersonation header already exists\n",
|
wantHTTPBody: "impersonation header already exists\n",
|
||||||
wantHTTPStatus: http.StatusBadRequest,
|
wantHTTPStatus: http.StatusBadRequest,
|
||||||
@ -170,7 +123,6 @@ func TestImpersonator(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Impersonate-Group header already in request",
|
name: "Impersonate-Group header already in request",
|
||||||
getKubeconfig: func() (*rest.Config, error) { return &testServerKubeconfig, nil },
|
|
||||||
request: newRequest(map[string][]string{"Impersonate-Group": {"some-group"}}),
|
request: newRequest(map[string][]string{"Impersonate-Group": {"some-group"}}),
|
||||||
wantHTTPBody: "impersonation header already exists\n",
|
wantHTTPBody: "impersonation header already exists\n",
|
||||||
wantHTTPStatus: http.StatusBadRequest,
|
wantHTTPStatus: http.StatusBadRequest,
|
||||||
@ -178,7 +130,6 @@ func TestImpersonator(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Impersonate-Extra header already in request",
|
name: "Impersonate-Extra header already in request",
|
||||||
getKubeconfig: func() (*rest.Config, error) { return &testServerKubeconfig, nil },
|
|
||||||
request: newRequest(map[string][]string{"Impersonate-Extra-something": {"something"}}),
|
request: newRequest(map[string][]string{"Impersonate-Extra-something": {"something"}}),
|
||||||
wantHTTPBody: "impersonation header already exists\n",
|
wantHTTPBody: "impersonation header already exists\n",
|
||||||
wantHTTPStatus: http.StatusBadRequest,
|
wantHTTPStatus: http.StatusBadRequest,
|
||||||
@ -186,7 +137,6 @@ func TestImpersonator(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "missing authorization header",
|
name: "missing authorization header",
|
||||||
getKubeconfig: func() (*rest.Config, error) { return &testServerKubeconfig, nil },
|
|
||||||
request: newRequest(map[string][]string{}),
|
request: newRequest(map[string][]string{}),
|
||||||
wantHTTPBody: "invalid token encoding\n",
|
wantHTTPBody: "invalid token encoding\n",
|
||||||
wantHTTPStatus: http.StatusBadRequest,
|
wantHTTPStatus: http.StatusBadRequest,
|
||||||
@ -194,7 +144,6 @@ func TestImpersonator(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "authorization header missing bearer prefix",
|
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)}}),
|
request: newRequest(map[string][]string{"Authorization": {impersonationtoken.Make(t, "test-token", &goodAuthenticator, defaultAPIGroup)}}),
|
||||||
wantHTTPBody: "invalid token encoding\n",
|
wantHTTPBody: "invalid token encoding\n",
|
||||||
wantHTTPStatus: http.StatusBadRequest,
|
wantHTTPStatus: http.StatusBadRequest,
|
||||||
@ -202,7 +151,6 @@ func TestImpersonator(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "token is not base64 encoded",
|
name: "token is not base64 encoded",
|
||||||
getKubeconfig: func() (*rest.Config, error) { return &testServerKubeconfig, nil },
|
|
||||||
request: newRequest(map[string][]string{"Authorization": {"Bearer !!!"}}),
|
request: newRequest(map[string][]string{"Authorization": {"Bearer !!!"}}),
|
||||||
wantHTTPBody: "invalid token encoding\n",
|
wantHTTPBody: "invalid token encoding\n",
|
||||||
wantHTTPStatus: http.StatusBadRequest,
|
wantHTTPStatus: http.StatusBadRequest,
|
||||||
@ -210,16 +158,14 @@ func TestImpersonator(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "base64 encoded token is not valid json",
|
name: "base64 encoded token is not valid json",
|
||||||
getKubeconfig: func() (*rest.Config, error) { return &testServerKubeconfig, nil },
|
request: newRequest(map[string][]string{"Authorization": {"Bearer aGVsbG8gd29ybGQK"}}), // aGVsbG8gd29ybGQK is "hello world" base64 encoded
|
||||||
request: newRequest(map[string][]string{"Authorization": {"Bearer abc"}}),
|
|
||||||
wantHTTPBody: "invalid token encoding\n",
|
wantHTTPBody: "invalid token encoding\n",
|
||||||
wantHTTPStatus: http.StatusBadRequest,
|
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",
|
name: "base64 encoded token is encoded with default api group but we are expecting custom api group",
|
||||||
apiGroupOverride: customAPIGroup,
|
apiGroupOverride: customAPIGroup,
|
||||||
getKubeconfig: func() (*rest.Config, error) { return &testServerKubeconfig, nil },
|
|
||||||
request: newRequest(map[string][]string{"Authorization": {"Bearer " + impersonationtoken.Make(t, "test-token", &goodAuthenticator, defaultAPIGroup)}}),
|
request: newRequest(map[string][]string{"Authorization": {"Bearer " + impersonationtoken.Make(t, "test-token", &goodAuthenticator, defaultAPIGroup)}}),
|
||||||
wantHTTPBody: "invalid token encoding\n",
|
wantHTTPBody: "invalid token encoding\n",
|
||||||
wantHTTPStatus: http.StatusBadRequest,
|
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",
|
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)}}),
|
request: newRequest(map[string][]string{"Authorization": {"Bearer " + impersonationtoken.Make(t, "test-token", &goodAuthenticator, customAPIGroup)}}),
|
||||||
wantHTTPBody: "invalid token encoding\n",
|
wantHTTPBody: "invalid token encoding\n",
|
||||||
wantHTTPStatus: http.StatusBadRequest,
|
wantHTTPStatus: http.StatusBadRequest,
|
||||||
@ -235,7 +180,6 @@ func TestImpersonator(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "token could not be authenticated",
|
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)}}),
|
request: newRequest(map[string][]string{"Authorization": {"Bearer " + impersonationtoken.Make(t, "", &badAuthenticator, defaultAPIGroup)}}),
|
||||||
wantHTTPBody: "invalid token\n",
|
wantHTTPBody: "invalid token\n",
|
||||||
wantHTTPStatus: http.StatusUnauthorized,
|
wantHTTPStatus: http.StatusUnauthorized,
|
||||||
@ -243,7 +187,6 @@ func TestImpersonator(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "token authenticates as nil",
|
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)}}),
|
request: newRequest(map[string][]string{"Authorization": {"Bearer " + impersonationtoken.Make(t, "test-token", &goodAuthenticator, defaultAPIGroup)}}),
|
||||||
expectMockToken: func(t *testing.T, recorder *mocktokenauthenticator.MockTokenMockRecorder) {
|
expectMockToken: func(t *testing.T, recorder *mocktokenauthenticator.MockTokenMockRecorder) {
|
||||||
recorder.AuthenticateToken(gomock.Any(), "test-token").Return(nil, false, nil)
|
recorder.AuthenticateToken(gomock.Any(), "test-token").Return(nil, false, nil)
|
||||||
@ -255,7 +198,46 @@ func TestImpersonator(t *testing.T) {
|
|||||||
// happy path
|
// happy path
|
||||||
{
|
{
|
||||||
name: "token validates",
|
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{
|
request: newRequest(map[string][]string{
|
||||||
"Authorization": {"Bearer " + impersonationtoken.Make(t, "test-token", &goodAuthenticator, defaultAPIGroup)},
|
"Authorization": {"Bearer " + impersonationtoken.Make(t, "test-token", &goodAuthenticator, defaultAPIGroup)},
|
||||||
"Malicious-Header": {"test-header-value-1"},
|
"Malicious-Header": {"test-header-value-1"},
|
||||||
@ -271,14 +253,22 @@ func TestImpersonator(t *testing.T) {
|
|||||||
response := &authenticator.Response{User: &userInfo}
|
response := &authenticator.Response{User: &userInfo}
|
||||||
recorder.AuthenticateToken(gomock.Any(), "test-token").Return(response, true, nil)
|
recorder.AuthenticateToken(gomock.Any(), "test-token").Return(response, true, nil)
|
||||||
},
|
},
|
||||||
wantHTTPBody: "successful proxied response",
|
wantKubeAPIServerStatusCode: http.StatusNotFound,
|
||||||
wantHTTPStatus: http.StatusOK,
|
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\""},
|
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",
|
name: "token validates with custom api group",
|
||||||
apiGroupOverride: customAPIGroup,
|
apiGroupOverride: customAPIGroup,
|
||||||
getKubeconfig: func() (*rest.Config, error) { return &testServerKubeconfig, nil },
|
|
||||||
request: newRequest(map[string][]string{
|
request: newRequest(map[string][]string{
|
||||||
"Authorization": {"Bearer " + impersonationtoken.Make(t, "test-token", &goodAuthenticator, customAPIGroup)},
|
"Authorization": {"Bearer " + impersonationtoken.Make(t, "test-token", &goodAuthenticator, customAPIGroup)},
|
||||||
"Malicious-Header": {"test-header-value-1"},
|
"Malicious-Header": {"test-header-value-1"},
|
||||||
@ -294,6 +284,15 @@ func TestImpersonator(t *testing.T) {
|
|||||||
response := &authenticator.Response{User: &userInfo}
|
response := &authenticator.Response{User: &userInfo}
|
||||||
recorder.AuthenticateToken(gomock.Any(), "test-token").Return(response, true, nil)
|
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",
|
wantHTTPBody: "successful proxied response",
|
||||||
wantHTTPStatus: http.StatusOK,
|
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\""},
|
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
|
// stole this from cache_test, hopefully it is sufficient
|
||||||
cacheWithMockAuthenticator := authncache.New()
|
cacheWithMockAuthenticator := authncache.New()
|
||||||
ctrl := gomock.NewController(t)
|
ctrl := gomock.NewController(t)
|
||||||
@ -349,6 +374,13 @@ func TestImpersonator(t *testing.T) {
|
|||||||
if tt.wantLogs != nil {
|
if tt.wantLogs != nil {
|
||||||
require.Equal(t, tt.wantLogs, testLog.Lines())
|
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)
|
reqJSON, err := runtime.Encode(respInfo.PrettySerializer, &tokenCredentialRequest)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
return base64.RawURLEncoding.EncodeToString(reqJSON)
|
return base64.StdEncoding.EncodeToString(reqJSON)
|
||||||
}
|
}
|
||||||
|
@ -12,8 +12,12 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
v1 "k8s.io/api/authorization/v1"
|
||||||
corev1 "k8s.io/api/core/v1"
|
corev1 "k8s.io/api/core/v1"
|
||||||
|
rbacv1 "k8s.io/api/rbac/v1"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/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/kubernetes"
|
||||||
"k8s.io/client-go/rest"
|
"k8s.io/client-go/rest"
|
||||||
"sigs.k8s.io/yaml"
|
"sigs.k8s.io/yaml"
|
||||||
@ -124,12 +128,94 @@ func TestImpersonationProxy(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
t.Run("watching all the verbs", func(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.
|
// Create a namespace, because it will be easier to deletecollection if we have a namespace.
|
||||||
// t.Cleanup Delete the 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.
|
// "get" one them.
|
||||||
// "list" them all.
|
// "list" them all.
|
||||||
// "update" one of them.
|
// "update" one of them.
|
||||||
@ -137,7 +223,6 @@ func TestImpersonationProxy(t *testing.T) {
|
|||||||
// "delete" one of them.
|
// "delete" one of them.
|
||||||
// "deletecollection" all of them.
|
// "deletecollection" all of them.
|
||||||
// Make sure the watch sees all of those actions.
|
// Make sure the watch sees all of those actions.
|
||||||
// Close the informer.
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// Update configuration to force the proxy to disabled mode
|
// Update configuration to force the proxy to disabled mode
|
||||||
|
@ -23,6 +23,7 @@ import (
|
|||||||
|
|
||||||
coreosoidc "github.com/coreos/go-oidc/v3/oidc"
|
coreosoidc "github.com/coreos/go-oidc/v3/oidc"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
authorizationv1 "k8s.io/api/authorization/v1"
|
||||||
corev1 "k8s.io/api/core/v1"
|
corev1 "k8s.io/api/core/v1"
|
||||||
rbacv1 "k8s.io/api/rbac/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.Subject{Kind: rbacv1.UserKind, APIGroup: rbacv1.GroupName, Name: env.SupervisorTestUpstream.Username},
|
||||||
rbacv1.RoleRef{Kind: "ClusterRole", APIGroup: rbacv1.GroupName, Name: "view"},
|
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.
|
// Use a specific session cache for this test.
|
||||||
sessionCachePath := tempDir + "/sessions.yaml"
|
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
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
package library
|
package library
|
||||||
@ -13,6 +13,7 @@ import (
|
|||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
authorizationv1 "k8s.io/api/authorization/v1"
|
||||||
v1 "k8s.io/api/core/v1"
|
v1 "k8s.io/api/core/v1"
|
||||||
rbacv1 "k8s.io/api/rbac/v1"
|
rbacv1 "k8s.io/api/rbac/v1"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
@ -137,6 +138,12 @@ func addTestClusterUserCanViewEverythingRoleBinding(t *testing.T, testUsername s
|
|||||||
Name: "view",
|
Name: "view",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
WaitForUserToHaveAccess(t, testUsername, []string{}, &authorizationv1.ResourceAttributes{
|
||||||
|
Verb: "get",
|
||||||
|
Group: "",
|
||||||
|
Version: "v1",
|
||||||
|
Resource: "namespaces",
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func addTestClusterGroupCanViewEverythingRoleBinding(t *testing.T, testGroup string) {
|
func addTestClusterGroupCanViewEverythingRoleBinding(t *testing.T, testGroup string) {
|
||||||
@ -154,6 +161,12 @@ func addTestClusterGroupCanViewEverythingRoleBinding(t *testing.T, testGroup str
|
|||||||
Name: "view",
|
Name: "view",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
WaitForUserToHaveAccess(t, "", []string{testGroup}, &authorizationv1.ResourceAttributes{
|
||||||
|
Verb: "get",
|
||||||
|
Group: "",
|
||||||
|
Version: "v1",
|
||||||
|
Resource: "namespaces",
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func runKubectlGetNamespaces(t *testing.T, kubeConfigYAML string) (string, error) {
|
func runKubectlGetNamespaces(t *testing.T, kubeConfigYAML string) (string, error) {
|
||||||
|
@ -17,6 +17,7 @@ import (
|
|||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
authorizationv1 "k8s.io/api/authorization/v1"
|
||||||
corev1 "k8s.io/api/core/v1"
|
corev1 "k8s.io/api/core/v1"
|
||||||
rbacv1 "k8s.io/api/rbac/v1"
|
rbacv1 "k8s.io/api/rbac/v1"
|
||||||
k8serrors "k8s.io/apimachinery/pkg/api/errors"
|
k8serrors "k8s.io/apimachinery/pkg/api/errors"
|
||||||
@ -434,6 +435,27 @@ func CreateTestClusterRoleBinding(t *testing.T, subject rbacv1.Subject, roleRef
|
|||||||
return created
|
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 {
|
func testObjectMeta(t *testing.T, baseName string) metav1.ObjectMeta {
|
||||||
return metav1.ObjectMeta{
|
return metav1.ObjectMeta{
|
||||||
GenerateName: fmt.Sprintf("test-%s-", baseName),
|
GenerateName: fmt.Sprintf("test-%s-", baseName),
|
||||||
|
Loading…
Reference in New Issue
Block a user