impersonator: test UID impersonation and header canonicalization

Signed-off-by: Monis Khan <mok@vmware.com>
This commit is contained in:
Monis Khan 2021-03-16 12:59:07 -04:00
parent 6887d0aca2
commit 236dbdb2c4
No known key found for this signature in database
GPG Key ID: 52C90ADA01B269B8
2 changed files with 69 additions and 14 deletions

View File

@ -14,7 +14,7 @@ import (
"time"
"github.com/stretchr/testify/require"
v1 "k8s.io/api/core/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/serializer"
@ -31,6 +31,7 @@ import (
"go.pinniped.dev/internal/certauthority"
"go.pinniped.dev/internal/dynamiccert"
"go.pinniped.dev/internal/here"
"go.pinniped.dev/internal/httputil/roundtripper"
"go.pinniped.dev/internal/kubeclient"
"go.pinniped.dev/internal/testutil"
)
@ -68,6 +69,7 @@ func TestImpersonator(t *testing.T) {
name string
clientCert *clientCert
clientImpersonateUser rest.ImpersonationConfig
clientMutateHeaders func(http.Header)
kubeAPIServerClientBearerTokenFile string
kubeAPIServerStatusCode int
wantKubeAPIServerRequestHeaders http.Header
@ -144,11 +146,42 @@ func TestImpersonator(t *testing.T) {
name: "no bearer token file in Kube API server client config",
wantConstructionError: "invalid impersonator loopback rest config has wrong bearer token semantics",
},
{
name: "header canonicalization user header",
clientCert: newClientCert(t, ca, "test-username", []string{"test-group1", "test-group2"}),
clientMutateHeaders: func(header http.Header) {
header.Set("imPerSonaTE-USer", "PANDA")
},
kubeAPIServerClientBearerTokenFile: "required-to-be-set",
wantError: `users "PANDA" is forbidden: User "test-username" ` +
`cannot impersonate resource "users" in API group "" at the cluster scope: impersonation is not allowed or invalid verb`,
},
{
name: "header canonicalization future UID header",
clientCert: newClientCert(t, ca, "test-username", []string{"test-group1", "test-group2"}),
clientMutateHeaders: func(header http.Header) {
header.Set("imPerSonaTE-uid", "007")
},
kubeAPIServerClientBearerTokenFile: "required-to-be-set",
wantError: "Internal error occurred: invalid impersonation",
},
{
name: "future UID header",
clientCert: newClientCert(t, ca, "test-username", []string{"test-group1", "test-group2"}),
clientMutateHeaders: func(header http.Header) {
header.Set("Impersonate-Uid", "008")
},
kubeAPIServerClientBearerTokenFile: "required-to-be-set",
wantError: "Internal error occurred: invalid impersonation",
},
}
for _, tt := range tests {
tt := tt
// This is a serial test because the production code binds to the port.
t.Run(tt.name, func(t *testing.T) {
// After failing to start and after shutdown, the impersonator port should be available again.
defer requireCanBindToPort(t, port)
if tt.kubeAPIServerStatusCode == 0 {
tt.kubeAPIServerStatusCode = http.StatusOK
}
@ -156,7 +189,7 @@ func TestImpersonator(t *testing.T) {
// Set up a fake Kube API server which will stand in for the real one. The impersonator
// will proxy incoming calls to this fake server.
testKubeAPIServerWasCalled := false
testKubeAPIServerSawHeaders := http.Header{}
var testKubeAPIServerSawHeaders http.Header
testKubeAPIServerCA, testKubeAPIServerURL := testutil.TLSTestServer(t, func(w http.ResponseWriter, r *http.Request) {
require.Equal(t, http.MethodGet, r.Method)
switch r.URL.Path {
@ -203,8 +236,6 @@ func TestImpersonator(t *testing.T) {
if len(tt.wantConstructionError) > 0 {
require.EqualError(t, constructionErr, tt.wantConstructionError)
require.Nil(t, runner)
// After failing to start, the impersonator port should be available again.
requireCanBindToPort(t, port)
// The rest of the test doesn't make sense when you expect a construction error, so stop here.
return
}
@ -232,6 +263,17 @@ func TestImpersonator(t *testing.T) {
// and it should not passed into the impersonator handler func as an authorization header.
BearerToken: "must-be-ignored",
Impersonate: tt.clientImpersonateUser,
WrapTransport: func(rt http.RoundTripper) http.RoundTripper {
if tt.clientMutateHeaders == nil {
return rt
}
return roundtripper.Func(func(req *http.Request) (*http.Response, error) {
req = req.Clone(req.Context())
tt.clientMutateHeaders(req.Header)
return rt.RoundTrip(req)
})
},
}
// Create a real Kube client to make API requests to the impersonator.
@ -243,28 +285,27 @@ func TestImpersonator(t *testing.T) {
listResponse, err := client.Kubernetes.CoreV1().Namespaces().List(context.Background(), metav1.ListOptions{})
if len(tt.wantError) > 0 {
require.EqualError(t, err, tt.wantError)
require.Equal(t, &corev1.NamespaceList{}, listResponse)
} else {
require.NoError(t, err)
require.Equal(t, &v1.NamespaceList{
Items: []v1.Namespace{
require.Equal(t, &corev1.NamespaceList{
Items: []corev1.Namespace{
{ObjectMeta: metav1.ObjectMeta{Name: "namespace1"}},
{ObjectMeta: metav1.ObjectMeta{Name: "namespace2"}},
},
}, listResponse)
// The impersonator should have proxied the request to the fake Kube API server, which should have seen
// the headers of the original request mutated by the impersonator.
require.True(t, testKubeAPIServerWasCalled)
require.Equal(t, tt.wantKubeAPIServerRequestHeaders, testKubeAPIServerSawHeaders)
}
// If we expect to see some headers, then the fake KAS should have been called.
require.Equal(t, len(tt.wantKubeAPIServerRequestHeaders) != 0, testKubeAPIServerWasCalled)
// If the impersonator proxied the request to the fake Kube API server, we should see the headers
// of the original request mutated by the impersonator. Otherwise the headers should be nil.
require.Equal(t, tt.wantKubeAPIServerRequestHeaders, testKubeAPIServerSawHeaders)
// Stop the impersonator server.
close(stopCh)
exitErr := <-errCh
require.NoError(t, exitErr)
// After shutdown, the impersonator port should be available again.
requireCanBindToPort(t, port)
})
}
}

View File

@ -0,0 +1,14 @@
// Copyright 2021 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package roundtripper
import "net/http"
var _ http.RoundTripper = Func(nil)
type Func func(*http.Request) (*http.Response, error)
func (f Func) RoundTrip(req *http.Request) (*http.Response, error) {
return f(req)
}