impersonator: test UID impersonation and header canonicalization
Signed-off-by: Monis Khan <mok@vmware.com>
This commit is contained in:
parent
6887d0aca2
commit
236dbdb2c4
@ -14,7 +14,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
"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"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
"k8s.io/apimachinery/pkg/runtime/serializer"
|
"k8s.io/apimachinery/pkg/runtime/serializer"
|
||||||
@ -31,6 +31,7 @@ import (
|
|||||||
"go.pinniped.dev/internal/certauthority"
|
"go.pinniped.dev/internal/certauthority"
|
||||||
"go.pinniped.dev/internal/dynamiccert"
|
"go.pinniped.dev/internal/dynamiccert"
|
||||||
"go.pinniped.dev/internal/here"
|
"go.pinniped.dev/internal/here"
|
||||||
|
"go.pinniped.dev/internal/httputil/roundtripper"
|
||||||
"go.pinniped.dev/internal/kubeclient"
|
"go.pinniped.dev/internal/kubeclient"
|
||||||
"go.pinniped.dev/internal/testutil"
|
"go.pinniped.dev/internal/testutil"
|
||||||
)
|
)
|
||||||
@ -68,6 +69,7 @@ func TestImpersonator(t *testing.T) {
|
|||||||
name string
|
name string
|
||||||
clientCert *clientCert
|
clientCert *clientCert
|
||||||
clientImpersonateUser rest.ImpersonationConfig
|
clientImpersonateUser rest.ImpersonationConfig
|
||||||
|
clientMutateHeaders func(http.Header)
|
||||||
kubeAPIServerClientBearerTokenFile string
|
kubeAPIServerClientBearerTokenFile string
|
||||||
kubeAPIServerStatusCode int
|
kubeAPIServerStatusCode int
|
||||||
wantKubeAPIServerRequestHeaders http.Header
|
wantKubeAPIServerRequestHeaders http.Header
|
||||||
@ -144,11 +146,42 @@ func TestImpersonator(t *testing.T) {
|
|||||||
name: "no bearer token file in Kube API server client config",
|
name: "no bearer token file in Kube API server client config",
|
||||||
wantConstructionError: "invalid impersonator loopback rest config has wrong bearer token semantics",
|
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 {
|
for _, tt := range tests {
|
||||||
tt := tt
|
tt := tt
|
||||||
// This is a serial test because the production code binds to the port.
|
// This is a serial test because the production code binds to the port.
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
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 {
|
if tt.kubeAPIServerStatusCode == 0 {
|
||||||
tt.kubeAPIServerStatusCode = http.StatusOK
|
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
|
// 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.
|
// will proxy incoming calls to this fake server.
|
||||||
testKubeAPIServerWasCalled := false
|
testKubeAPIServerWasCalled := false
|
||||||
testKubeAPIServerSawHeaders := http.Header{}
|
var testKubeAPIServerSawHeaders http.Header
|
||||||
testKubeAPIServerCA, testKubeAPIServerURL := testutil.TLSTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
testKubeAPIServerCA, testKubeAPIServerURL := testutil.TLSTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||||
require.Equal(t, http.MethodGet, r.Method)
|
require.Equal(t, http.MethodGet, r.Method)
|
||||||
switch r.URL.Path {
|
switch r.URL.Path {
|
||||||
@ -203,8 +236,6 @@ func TestImpersonator(t *testing.T) {
|
|||||||
if len(tt.wantConstructionError) > 0 {
|
if len(tt.wantConstructionError) > 0 {
|
||||||
require.EqualError(t, constructionErr, tt.wantConstructionError)
|
require.EqualError(t, constructionErr, tt.wantConstructionError)
|
||||||
require.Nil(t, runner)
|
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.
|
// The rest of the test doesn't make sense when you expect a construction error, so stop here.
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -232,6 +263,17 @@ func TestImpersonator(t *testing.T) {
|
|||||||
// and it should not passed into the impersonator handler func as an authorization header.
|
// and it should not passed into the impersonator handler func as an authorization header.
|
||||||
BearerToken: "must-be-ignored",
|
BearerToken: "must-be-ignored",
|
||||||
Impersonate: tt.clientImpersonateUser,
|
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.
|
// 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{})
|
listResponse, err := client.Kubernetes.CoreV1().Namespaces().List(context.Background(), metav1.ListOptions{})
|
||||||
if len(tt.wantError) > 0 {
|
if len(tt.wantError) > 0 {
|
||||||
require.EqualError(t, err, tt.wantError)
|
require.EqualError(t, err, tt.wantError)
|
||||||
|
require.Equal(t, &corev1.NamespaceList{}, listResponse)
|
||||||
} else {
|
} else {
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, &v1.NamespaceList{
|
require.Equal(t, &corev1.NamespaceList{
|
||||||
Items: []v1.Namespace{
|
Items: []corev1.Namespace{
|
||||||
{ObjectMeta: metav1.ObjectMeta{Name: "namespace1"}},
|
{ObjectMeta: metav1.ObjectMeta{Name: "namespace1"}},
|
||||||
{ObjectMeta: metav1.ObjectMeta{Name: "namespace2"}},
|
{ObjectMeta: metav1.ObjectMeta{Name: "namespace2"}},
|
||||||
},
|
},
|
||||||
}, listResponse)
|
}, 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.
|
// Stop the impersonator server.
|
||||||
close(stopCh)
|
close(stopCh)
|
||||||
exitErr := <-errCh
|
exitErr := <-errCh
|
||||||
require.NoError(t, exitErr)
|
require.NoError(t, exitErr)
|
||||||
|
|
||||||
// After shutdown, the impersonator port should be available again.
|
|
||||||
requireCanBindToPort(t, port)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
14
internal/httputil/roundtripper/roundtripper.go
Normal file
14
internal/httputil/roundtripper/roundtripper.go
Normal 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)
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user