2021-01-20 00:37:02 +00:00
|
|
|
// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved.
|
|
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
|
|
|
|
package impersonator
|
|
|
|
|
|
|
|
import (
|
2021-02-09 18:25:24 +00:00
|
|
|
"context"
|
2021-03-10 18:30:06 +00:00
|
|
|
"net"
|
2021-01-20 00:37:02 +00:00
|
|
|
"net/http"
|
|
|
|
"net/http/httptest"
|
|
|
|
"net/url"
|
2021-03-10 18:30:06 +00:00
|
|
|
"strconv"
|
2021-01-20 00:37:02 +00:00
|
|
|
"testing"
|
2021-03-10 18:30:06 +00:00
|
|
|
"time"
|
2021-01-20 00:37:02 +00:00
|
|
|
|
|
|
|
"github.com/stretchr/testify/require"
|
2021-03-12 00:27:16 +00:00
|
|
|
v1 "k8s.io/api/core/v1"
|
|
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
2021-01-20 00:37:02 +00:00
|
|
|
"k8s.io/apiserver/pkg/authentication/user"
|
2021-03-10 18:30:06 +00:00
|
|
|
"k8s.io/apiserver/pkg/endpoints/request"
|
|
|
|
"k8s.io/apiserver/pkg/features"
|
|
|
|
genericoptions "k8s.io/apiserver/pkg/server/options"
|
|
|
|
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
2021-01-20 00:37:02 +00:00
|
|
|
"k8s.io/client-go/rest"
|
|
|
|
"k8s.io/client-go/tools/clientcmd/api"
|
2021-03-10 18:30:06 +00:00
|
|
|
featuregatetesting "k8s.io/component-base/featuregate/testing"
|
2021-01-20 00:37:02 +00:00
|
|
|
|
2021-03-10 18:30:06 +00:00
|
|
|
"go.pinniped.dev/internal/certauthority"
|
2021-03-11 21:20:25 +00:00
|
|
|
"go.pinniped.dev/internal/dynamiccert"
|
2021-03-12 00:27:16 +00:00
|
|
|
"go.pinniped.dev/internal/here"
|
2021-03-10 18:30:06 +00:00
|
|
|
"go.pinniped.dev/internal/kubeclient"
|
2021-01-20 00:37:02 +00:00
|
|
|
"go.pinniped.dev/internal/testutil"
|
|
|
|
)
|
|
|
|
|
2021-03-12 00:27:16 +00:00
|
|
|
func TestImpersonator(t *testing.T) {
|
|
|
|
const port = 9444
|
2021-03-10 18:30:06 +00:00
|
|
|
|
2021-03-13 00:09:16 +00:00
|
|
|
ca, err := certauthority.New("ca", time.Hour)
|
2021-03-10 18:30:06 +00:00
|
|
|
require.NoError(t, err)
|
2021-03-11 21:20:25 +00:00
|
|
|
caKey, err := ca.PrivateKeyToPEM()
|
|
|
|
require.NoError(t, err)
|
|
|
|
caContent := dynamiccert.New("ca")
|
|
|
|
err = caContent.SetCertKeyContent(ca.Bundle(), caKey)
|
2021-03-10 18:30:06 +00:00
|
|
|
require.NoError(t, err)
|
2021-03-11 21:20:25 +00:00
|
|
|
|
2021-03-13 00:09:16 +00:00
|
|
|
cert, key, err := ca.IssueServerCertPEM(nil, []net.IP{net.ParseIP("127.0.0.1")}, time.Hour)
|
2021-03-10 18:30:06 +00:00
|
|
|
require.NoError(t, err)
|
2021-03-11 21:20:25 +00:00
|
|
|
certKeyContent := dynamiccert.New("cert-key")
|
|
|
|
err = certKeyContent.SetCertKeyContent(cert, key)
|
2021-03-10 18:30:06 +00:00
|
|
|
require.NoError(t, err)
|
|
|
|
|
2021-03-13 00:09:16 +00:00
|
|
|
unrelatedCA, err := certauthority.New("ca", time.Hour)
|
2021-03-12 01:11:38 +00:00
|
|
|
require.NoError(t, err)
|
|
|
|
|
2021-03-10 18:30:06 +00:00
|
|
|
// Punch out just enough stuff to make New actually run without error.
|
|
|
|
recOpts := func(options *genericoptions.RecommendedOptions) {
|
|
|
|
options.Authentication.RemoteKubeConfigFileOptional = true
|
|
|
|
options.Authorization.RemoteKubeConfigFileOptional = true
|
|
|
|
options.CoreAPI = nil
|
|
|
|
options.Admission = nil
|
|
|
|
}
|
|
|
|
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.APIPriorityAndFairness, false)()
|
|
|
|
|
|
|
|
tests := []struct {
|
2021-03-12 00:27:16 +00:00
|
|
|
name string
|
2021-03-12 01:24:52 +00:00
|
|
|
clientCert *clientCert
|
2021-03-12 00:27:16 +00:00
|
|
|
clientImpersonateUser rest.ImpersonationConfig
|
|
|
|
kubeAPIServerClientBearerTokenFile string
|
|
|
|
kubeAPIServerStatusCode int
|
2021-03-12 01:11:38 +00:00
|
|
|
wantKubeAPIServerRequestHeaders http.Header
|
2021-03-12 00:27:16 +00:00
|
|
|
wantError string
|
|
|
|
wantConstructionError string
|
2021-03-10 18:30:06 +00:00
|
|
|
}{
|
|
|
|
{
|
2021-03-12 00:27:16 +00:00
|
|
|
name: "happy path",
|
2021-03-12 01:24:52 +00:00
|
|
|
clientCert: newClientCert(t, ca, "test-username", []string{"test-group1", "test-group2"}),
|
2021-03-12 00:27:16 +00:00
|
|
|
kubeAPIServerClientBearerTokenFile: "required-to-be-set",
|
2021-03-12 01:11:38 +00:00
|
|
|
wantKubeAPIServerRequestHeaders: http.Header{
|
|
|
|
"Impersonate-User": {"test-username"},
|
|
|
|
"Impersonate-Group": {"test-group1", "test-group2", "system:authenticated"},
|
|
|
|
"Authorization": {"Bearer some-service-account-token"},
|
|
|
|
"User-Agent": {"test-agent"},
|
|
|
|
"Accept": {"application/vnd.kubernetes.protobuf,application/json"},
|
|
|
|
"Accept-Encoding": {"gzip"},
|
|
|
|
"X-Forwarded-For": {"127.0.0.1"},
|
|
|
|
},
|
2021-03-12 00:27:16 +00:00
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "user is authenticated but the kube API request returns an error",
|
|
|
|
kubeAPIServerStatusCode: http.StatusNotFound,
|
2021-03-12 01:24:52 +00:00
|
|
|
clientCert: newClientCert(t, ca, "test-username", []string{"test-group1", "test-group2"}),
|
2021-03-12 00:27:16 +00:00
|
|
|
kubeAPIServerClientBearerTokenFile: "required-to-be-set",
|
|
|
|
wantError: `the server could not find the requested resource (get namespaces)`,
|
2021-03-12 01:11:38 +00:00
|
|
|
wantKubeAPIServerRequestHeaders: http.Header{
|
|
|
|
"Impersonate-User": {"test-username"},
|
|
|
|
"Impersonate-Group": {"test-group1", "test-group2", "system:authenticated"},
|
|
|
|
"Authorization": {"Bearer some-service-account-token"},
|
|
|
|
"User-Agent": {"test-agent"},
|
|
|
|
"Accept": {"application/vnd.kubernetes.protobuf,application/json"},
|
|
|
|
"Accept-Encoding": {"gzip"},
|
|
|
|
"X-Forwarded-For": {"127.0.0.1"},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "when there is no client cert on request, it is an anonymous request",
|
2021-03-12 01:24:52 +00:00
|
|
|
clientCert: &clientCert{},
|
2021-03-12 01:11:38 +00:00
|
|
|
kubeAPIServerClientBearerTokenFile: "required-to-be-set",
|
|
|
|
wantKubeAPIServerRequestHeaders: http.Header{
|
|
|
|
"Impersonate-User": {"system:anonymous"},
|
|
|
|
"Impersonate-Group": {"system:unauthenticated"},
|
|
|
|
"Authorization": {"Bearer some-service-account-token"},
|
|
|
|
"User-Agent": {"test-agent"},
|
|
|
|
"Accept": {"application/vnd.kubernetes.protobuf,application/json"},
|
|
|
|
"Accept-Encoding": {"gzip"},
|
|
|
|
"X-Forwarded-For": {"127.0.0.1"},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "failed client cert authentication",
|
2021-03-12 01:24:52 +00:00
|
|
|
clientCert: newClientCert(t, unrelatedCA, "test-username", []string{"test-group1"}),
|
2021-03-12 01:11:38 +00:00
|
|
|
kubeAPIServerClientBearerTokenFile: "required-to-be-set",
|
|
|
|
wantError: "Unauthorized",
|
2021-03-12 00:27:16 +00:00
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "double impersonation is not allowed by regular users",
|
2021-03-12 01:24:52 +00:00
|
|
|
clientCert: newClientCert(t, ca, "test-username", []string{"test-group1", "test-group2"}),
|
2021-03-12 00:27:16 +00:00
|
|
|
clientImpersonateUser: rest.ImpersonationConfig{UserName: "some-other-username"},
|
|
|
|
kubeAPIServerClientBearerTokenFile: "required-to-be-set",
|
|
|
|
wantError: `users "some-other-username" is forbidden: User "test-username" ` +
|
|
|
|
`cannot impersonate resource "users" in API group "" at the cluster scope: impersonation is not allowed or invalid verb`,
|
2021-03-10 18:30:06 +00:00
|
|
|
},
|
|
|
|
{
|
2021-03-12 00:27:16 +00:00
|
|
|
name: "double impersonation is not allowed by admin users",
|
2021-03-12 01:24:52 +00:00
|
|
|
clientCert: newClientCert(t, ca, "test-admin", []string{"system:masters", "test-group2"}),
|
2021-03-12 00:27:16 +00:00
|
|
|
clientImpersonateUser: rest.ImpersonationConfig{UserName: "some-other-username"},
|
|
|
|
kubeAPIServerClientBearerTokenFile: "required-to-be-set",
|
|
|
|
wantError: `users "some-other-username" is forbidden: User "test-admin" ` +
|
|
|
|
`cannot impersonate resource "users" in API group "" at the cluster scope: impersonation is not allowed or invalid verb`,
|
2021-03-10 18:30:06 +00:00
|
|
|
},
|
2021-03-12 01:11:38 +00:00
|
|
|
{
|
|
|
|
name: "no bearer token file in Kube API server client config",
|
|
|
|
wantConstructionError: "invalid impersonator loopback rest config has wrong bearer token semantics",
|
|
|
|
},
|
2021-03-10 18:30:06 +00:00
|
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
|
|
tt := tt
|
2021-03-12 00:27:16 +00:00
|
|
|
// This is a serial test because the production code binds to the port.
|
2021-03-10 18:30:06 +00:00
|
|
|
t.Run(tt.name, func(t *testing.T) {
|
2021-03-12 00:27:16 +00:00
|
|
|
if tt.kubeAPIServerStatusCode == 0 {
|
|
|
|
tt.kubeAPIServerStatusCode = http.StatusOK
|
|
|
|
}
|
2021-03-10 18:30:06 +00:00
|
|
|
|
2021-03-12 00:27:16 +00:00
|
|
|
// 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{}
|
|
|
|
testKubeAPIServerCA, testKubeAPIServerURL := testutil.TLSTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
require.Equal(t, http.MethodGet, r.Method)
|
|
|
|
switch r.URL.Path {
|
|
|
|
case "/api/v1/namespaces/kube-system/configmaps":
|
|
|
|
// The production code uses NewDynamicCAFromConfigMapController which fetches a ConfigMap,
|
|
|
|
// so treat that differently. It wants to read the Kube API server CA from that ConfigMap
|
|
|
|
// to use it to validate client certs. We don't need it for this test, so return NotFound.
|
|
|
|
http.NotFound(w, r)
|
|
|
|
return
|
|
|
|
case "/api/v1/namespaces":
|
|
|
|
testKubeAPIServerWasCalled = true
|
|
|
|
testKubeAPIServerSawHeaders = r.Header
|
|
|
|
if tt.kubeAPIServerStatusCode != http.StatusOK {
|
|
|
|
w.WriteHeader(tt.kubeAPIServerStatusCode)
|
|
|
|
} else {
|
|
|
|
w.Header().Add("Content-Type", "application/json; charset=UTF-8")
|
|
|
|
_, _ = w.Write([]byte(here.Doc(`
|
|
|
|
{
|
|
|
|
"kind": "NamespaceList",
|
|
|
|
"apiVersion":"v1",
|
|
|
|
"items": [
|
|
|
|
{"metadata":{"name": "namespace1"}},
|
|
|
|
{"metadata":{"name": "namespace2"}}
|
|
|
|
]
|
|
|
|
}
|
|
|
|
`)))
|
|
|
|
}
|
|
|
|
default:
|
|
|
|
require.Fail(t, "fake Kube API server got an unexpected request")
|
2021-03-10 18:30:06 +00:00
|
|
|
}
|
2021-03-12 00:27:16 +00:00
|
|
|
})
|
2021-03-12 00:44:08 +00:00
|
|
|
|
|
|
|
// Create the client config that the impersonation server should use to talk to the Kube API server.
|
2021-03-12 00:27:16 +00:00
|
|
|
testKubeAPIServerKubeconfig := rest.Config{
|
|
|
|
Host: testKubeAPIServerURL,
|
|
|
|
BearerToken: "some-service-account-token",
|
|
|
|
TLSClientConfig: rest.TLSClientConfig{CAData: []byte(testKubeAPIServerCA)},
|
|
|
|
BearerTokenFile: tt.kubeAPIServerClientBearerTokenFile,
|
|
|
|
}
|
|
|
|
clientOpts := []kubeclient.Option{kubeclient.WithConfig(&testKubeAPIServerKubeconfig)}
|
2021-03-10 18:30:06 +00:00
|
|
|
|
2021-03-12 00:27:16 +00:00
|
|
|
// Create an impersonator.
|
|
|
|
runner, constructionErr := newInternal(port, certKeyContent, caContent, clientOpts, recOpts)
|
|
|
|
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
|
2021-03-10 18:30:06 +00:00
|
|
|
}
|
2021-03-12 00:27:16 +00:00
|
|
|
require.NoError(t, constructionErr)
|
|
|
|
require.NotNil(t, runner)
|
2021-03-10 18:30:06 +00:00
|
|
|
|
2021-03-12 00:27:16 +00:00
|
|
|
// Start the impersonator.
|
|
|
|
stopCh := make(chan struct{})
|
|
|
|
errCh := make(chan error)
|
|
|
|
go func() {
|
|
|
|
stopErr := runner(stopCh)
|
|
|
|
errCh <- stopErr
|
2021-03-10 18:30:06 +00:00
|
|
|
}()
|
|
|
|
|
2021-03-12 00:27:16 +00:00
|
|
|
// Create a kubeconfig to talk to the impersonator as a client.
|
|
|
|
clientKubeconfig := &rest.Config{
|
|
|
|
Host: "https://127.0.0.1:" + strconv.Itoa(port),
|
|
|
|
TLSClientConfig: rest.TLSClientConfig{
|
|
|
|
CAData: ca.Bundle(),
|
2021-03-12 01:24:52 +00:00
|
|
|
CertData: tt.clientCert.certPEM,
|
|
|
|
KeyData: tt.clientCert.keyPEM,
|
2021-03-12 00:27:16 +00:00
|
|
|
},
|
2021-03-12 00:44:08 +00:00
|
|
|
UserAgent: "test-agent",
|
2021-03-12 01:11:38 +00:00
|
|
|
// BearerToken should be ignored during auth when there are valid client certs,
|
2021-03-12 00:44:08 +00:00
|
|
|
// and it should not passed into the impersonator handler func as an authorization header.
|
|
|
|
BearerToken: "must-be-ignored",
|
2021-03-12 00:27:16 +00:00
|
|
|
Impersonate: tt.clientImpersonateUser,
|
|
|
|
}
|
|
|
|
|
|
|
|
// Create a real Kube client to make API requests to the impersonator.
|
|
|
|
client, err := kubeclient.New(kubeclient.WithConfig(clientKubeconfig))
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
// The fake Kube API server knows how to to list namespaces, so make that request using the client
|
|
|
|
// through the impersonator.
|
|
|
|
listResponse, err := client.Kubernetes.CoreV1().Namespaces().List(context.Background(), metav1.ListOptions{})
|
|
|
|
if len(tt.wantError) > 0 {
|
|
|
|
require.EqualError(t, err, tt.wantError)
|
|
|
|
} else {
|
|
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, &v1.NamespaceList{
|
|
|
|
Items: []v1.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)
|
2021-03-12 01:11:38 +00:00
|
|
|
require.Equal(t, tt.wantKubeAPIServerRequestHeaders, testKubeAPIServerSawHeaders)
|
2021-03-12 00:27:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Stop the impersonator server.
|
|
|
|
close(stopCh)
|
|
|
|
exitErr := <-errCh
|
|
|
|
require.NoError(t, exitErr)
|
|
|
|
|
|
|
|
// After shutdown, the impersonator port should be available again.
|
|
|
|
requireCanBindToPort(t, port)
|
2021-03-10 18:30:06 +00:00
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
2021-02-16 14:09:54 +00:00
|
|
|
|
2021-03-12 00:27:16 +00:00
|
|
|
func TestImpersonatorHTTPHandler(t *testing.T) {
|
2021-03-10 18:30:06 +00:00
|
|
|
const testUser = "test-user"
|
2021-02-15 23:00:10 +00:00
|
|
|
|
2021-02-16 14:09:54 +00:00
|
|
|
testGroups := []string{"test-group-1", "test-group-2"}
|
|
|
|
testExtra := map[string][]string{
|
|
|
|
"extra-1": {"some", "extra", "stuff"},
|
|
|
|
"extra-2": {"some", "more", "extra", "stuff"},
|
|
|
|
}
|
|
|
|
|
2021-01-20 00:37:02 +00:00
|
|
|
validURL, _ := url.Parse("http://pinniped.dev/blah")
|
2021-03-10 18:30:06 +00:00
|
|
|
newRequest := func(h http.Header, userInfo user.Info) *http.Request {
|
|
|
|
ctx := context.Background()
|
|
|
|
if userInfo != nil {
|
|
|
|
ctx = request.WithUser(ctx, userInfo)
|
|
|
|
}
|
|
|
|
r, err := http.NewRequestWithContext(ctx, http.MethodGet, validURL.String(), nil)
|
2021-02-09 18:25:24 +00:00
|
|
|
require.NoError(t, err)
|
|
|
|
r.Header = h
|
|
|
|
return r
|
|
|
|
}
|
2021-01-20 00:37:02 +00:00
|
|
|
|
|
|
|
tests := []struct {
|
2021-02-23 01:23:11 +00:00
|
|
|
name string
|
2021-03-10 18:30:06 +00:00
|
|
|
restConfig *rest.Config
|
2021-02-23 01:23:11 +00:00
|
|
|
wantCreationErr string
|
|
|
|
request *http.Request
|
|
|
|
wantHTTPBody string
|
|
|
|
wantHTTPStatus int
|
|
|
|
wantKubeAPIServerRequestHeaders http.Header
|
2021-03-10 18:30:06 +00:00
|
|
|
kubeAPIServerStatusCode int
|
2021-01-20 00:37:02 +00:00
|
|
|
}{
|
|
|
|
{
|
2021-03-10 18:30:06 +00:00
|
|
|
name: "invalid kubeconfig host",
|
|
|
|
restConfig: &rest.Config{Host: ":"},
|
2021-01-20 00:37:02 +00:00
|
|
|
wantCreationErr: "could not parse host URL from in-cluster config: parse \":\": missing protocol scheme",
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "invalid transport config",
|
2021-03-10 18:30:06 +00:00
|
|
|
restConfig: &rest.Config{
|
|
|
|
Host: "pinniped.dev/blah",
|
|
|
|
ExecProvider: &api.ExecConfig{},
|
|
|
|
AuthProvider: &api.AuthProviderConfig{},
|
2021-01-20 00:37:02 +00:00
|
|
|
},
|
|
|
|
wantCreationErr: "could not get in-cluster transport config: execProvider and authProvider cannot be used in combination",
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "fail to get transport from config",
|
2021-03-10 18:30:06 +00:00
|
|
|
restConfig: &rest.Config{
|
|
|
|
Host: "pinniped.dev/blah",
|
|
|
|
BearerToken: "test-bearer-token",
|
|
|
|
Transport: http.DefaultTransport,
|
|
|
|
TLSClientConfig: rest.TLSClientConfig{Insecure: true},
|
2021-01-20 00:37:02 +00:00
|
|
|
},
|
|
|
|
wantCreationErr: "could not get in-cluster transport: using a custom transport with TLS certificate options or the insecure flag is not allowed",
|
|
|
|
},
|
2021-02-16 13:15:50 +00:00
|
|
|
{
|
|
|
|
name: "Impersonate-User header already in request",
|
2021-03-10 18:30:06 +00:00
|
|
|
request: newRequest(map[string][]string{"Impersonate-User": {"some-user"}}, nil),
|
|
|
|
wantHTTPBody: "invalid impersonation\n",
|
|
|
|
wantHTTPStatus: http.StatusInternalServerError,
|
2021-02-16 13:15:50 +00:00
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "Impersonate-Group header already in request",
|
2021-03-10 18:30:06 +00:00
|
|
|
request: newRequest(map[string][]string{"Impersonate-Group": {"some-group"}}, nil),
|
|
|
|
wantHTTPBody: "invalid impersonation\n",
|
|
|
|
wantHTTPStatus: http.StatusInternalServerError,
|
2021-02-16 13:15:50 +00:00
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "Impersonate-Extra header already in request",
|
2021-03-10 18:30:06 +00:00
|
|
|
request: newRequest(map[string][]string{"Impersonate-Extra-something": {"something"}}, nil),
|
|
|
|
wantHTTPBody: "invalid impersonation\n",
|
|
|
|
wantHTTPStatus: http.StatusInternalServerError,
|
2021-01-20 00:37:02 +00:00
|
|
|
},
|
|
|
|
{
|
2021-03-10 18:30:06 +00:00
|
|
|
name: "Impersonate-* header already in request",
|
|
|
|
request: newRequest(map[string][]string{"Impersonate-Something": {"some-newfangled-impersonate-header"}}, nil),
|
|
|
|
wantHTTPBody: "invalid impersonation\n",
|
|
|
|
wantHTTPStatus: http.StatusInternalServerError,
|
2021-01-20 00:37:02 +00:00
|
|
|
},
|
|
|
|
{
|
2021-03-10 18:30:06 +00:00
|
|
|
name: "unexpected authorization header",
|
|
|
|
request: newRequest(map[string][]string{"Authorization": {"panda"}}, nil),
|
|
|
|
wantHTTPBody: "invalid authorization header\n",
|
|
|
|
wantHTTPStatus: http.StatusInternalServerError,
|
2021-01-20 00:37:02 +00:00
|
|
|
},
|
|
|
|
{
|
2021-03-10 18:30:06 +00:00
|
|
|
name: "missing user",
|
|
|
|
request: newRequest(map[string][]string{}, nil),
|
|
|
|
wantHTTPBody: "invalid user\n",
|
|
|
|
wantHTTPStatus: http.StatusInternalServerError,
|
2021-02-15 23:00:10 +00:00
|
|
|
},
|
|
|
|
{
|
2021-03-10 18:30:06 +00:00
|
|
|
name: "unexpected UID",
|
|
|
|
request: newRequest(map[string][]string{}, &user.DefaultInfo{UID: "007"}),
|
2021-03-12 15:33:30 +00:00
|
|
|
wantHTTPBody: "unable to act as user\n",
|
2021-03-10 18:30:06 +00:00
|
|
|
wantHTTPStatus: http.StatusUnprocessableEntity,
|
2021-01-20 00:37:02 +00:00
|
|
|
},
|
|
|
|
// happy path
|
|
|
|
{
|
2021-03-10 18:30:06 +00:00
|
|
|
name: "authenticated user",
|
2021-02-09 18:25:24 +00:00
|
|
|
request: newRequest(map[string][]string{
|
2021-03-02 22:56:54 +00:00
|
|
|
"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"},
|
|
|
|
"Other-Header": {"test-header-value-1"}, // this header will be passed through
|
2021-03-10 18:30:06 +00:00
|
|
|
}, &user.DefaultInfo{
|
|
|
|
Name: testUser,
|
|
|
|
Groups: testGroups,
|
|
|
|
Extra: testExtra,
|
2021-02-15 23:00:10 +00:00
|
|
|
}),
|
2021-02-23 01:23:11 +00:00
|
|
|
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"},
|
2021-03-02 22:56:54 +00:00
|
|
|
"Other-Header": {"test-header-value-1"},
|
2021-02-23 01:23:11 +00:00
|
|
|
},
|
2021-02-15 23:00:10 +00:00
|
|
|
wantHTTPBody: "successful proxied response",
|
|
|
|
wantHTTPStatus: http.StatusOK,
|
|
|
|
},
|
2021-02-23 01:23:11 +00:00
|
|
|
{
|
2021-03-10 18:30:06 +00:00
|
|
|
name: "user is authenticated but the kube API request returns an error",
|
2021-02-23 01:23:11 +00:00
|
|
|
request: newRequest(map[string][]string{
|
2021-03-10 18:30:06 +00:00
|
|
|
"User-Agent": {"test-user-agent"},
|
|
|
|
}, &user.DefaultInfo{
|
|
|
|
Name: testUser,
|
|
|
|
Groups: testGroups,
|
|
|
|
Extra: testExtra,
|
2021-02-23 01:23:11 +00:00
|
|
|
}),
|
2021-03-10 18:30:06 +00:00
|
|
|
kubeAPIServerStatusCode: http.StatusNotFound,
|
2021-02-23 01:23:11 +00:00
|
|
|
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,
|
2021-01-20 00:37:02 +00:00
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, tt := range tests {
|
|
|
|
tt := tt
|
|
|
|
t.Run(tt.name, func(t *testing.T) {
|
2021-03-12 00:44:08 +00:00
|
|
|
t.Parallel()
|
|
|
|
|
2021-03-10 18:30:06 +00:00
|
|
|
if tt.kubeAPIServerStatusCode == 0 {
|
|
|
|
tt.kubeAPIServerStatusCode = http.StatusOK
|
2021-02-23 01:23:11 +00:00
|
|
|
}
|
|
|
|
|
2021-03-12 00:44:08 +00:00
|
|
|
testKubeAPIServerWasCalled := false
|
|
|
|
testKubeAPIServerSawHeaders := http.Header{}
|
|
|
|
testKubeAPIServerCA, testKubeAPIServerURL := testutil.TLSTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
testKubeAPIServerWasCalled = true
|
|
|
|
testKubeAPIServerSawHeaders = r.Header
|
2021-03-10 18:30:06 +00:00
|
|
|
if tt.kubeAPIServerStatusCode != http.StatusOK {
|
|
|
|
w.WriteHeader(tt.kubeAPIServerStatusCode)
|
2021-02-23 01:23:11 +00:00
|
|
|
} else {
|
|
|
|
_, _ = w.Write([]byte("successful proxied response"))
|
|
|
|
}
|
|
|
|
})
|
2021-03-12 00:44:08 +00:00
|
|
|
testKubeAPIServerKubeconfig := rest.Config{
|
|
|
|
Host: testKubeAPIServerURL,
|
2021-02-23 01:23:11 +00:00
|
|
|
BearerToken: "some-service-account-token",
|
2021-03-12 00:44:08 +00:00
|
|
|
TLSClientConfig: rest.TLSClientConfig{CAData: []byte(testKubeAPIServerCA)},
|
2021-02-23 01:23:11 +00:00
|
|
|
}
|
2021-03-10 18:30:06 +00:00
|
|
|
if tt.restConfig == nil {
|
2021-03-12 00:44:08 +00:00
|
|
|
tt.restConfig = &testKubeAPIServerKubeconfig
|
2021-02-15 23:00:10 +00:00
|
|
|
}
|
|
|
|
|
2021-03-12 14:56:34 +00:00
|
|
|
impersonatorHTTPHandlerFunc, err := newImpersonationReverseProxyFunc(tt.restConfig)
|
2021-01-20 00:37:02 +00:00
|
|
|
if tt.wantCreationErr != "" {
|
|
|
|
require.EqualError(t, err, tt.wantCreationErr)
|
2021-03-12 14:56:34 +00:00
|
|
|
require.Nil(t, impersonatorHTTPHandlerFunc)
|
2021-01-20 00:37:02 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
require.NoError(t, err)
|
2021-03-12 14:56:34 +00:00
|
|
|
require.NotNil(t, impersonatorHTTPHandlerFunc)
|
2021-03-12 00:44:08 +00:00
|
|
|
|
2021-01-20 00:37:02 +00:00
|
|
|
w := httptest.NewRecorder()
|
2021-02-09 18:25:24 +00:00
|
|
|
requestBeforeServe := tt.request.Clone(tt.request.Context())
|
2021-03-12 14:56:34 +00:00
|
|
|
impersonatorHTTPHandlerFunc(nil).ServeHTTP(w, tt.request)
|
2021-03-12 00:44:08 +00:00
|
|
|
|
2021-02-09 18:25:24 +00:00
|
|
|
require.Equal(t, requestBeforeServe, tt.request, "ServeHTTP() mutated the request, and it should not per http.Handler docs")
|
2021-01-20 00:37:02 +00:00
|
|
|
if tt.wantHTTPStatus != 0 {
|
2021-02-16 14:09:54 +00:00
|
|
|
require.Equalf(t, tt.wantHTTPStatus, w.Code, "fyi, response body was %q", w.Body.String())
|
2021-01-20 00:37:02 +00:00
|
|
|
}
|
|
|
|
if tt.wantHTTPBody != "" {
|
|
|
|
require.Equal(t, tt.wantHTTPBody, w.Body.String())
|
|
|
|
}
|
2021-02-23 01:23:11 +00:00
|
|
|
|
2021-03-10 18:30:06 +00:00
|
|
|
if tt.wantHTTPStatus == http.StatusOK || tt.kubeAPIServerStatusCode != http.StatusOK {
|
2021-03-12 00:44:08 +00:00
|
|
|
require.True(t, testKubeAPIServerWasCalled, "Should have proxied the request to the Kube API server, but didn't")
|
|
|
|
require.Equal(t, tt.wantKubeAPIServerRequestHeaders, testKubeAPIServerSawHeaders)
|
2021-02-23 01:23:11 +00:00
|
|
|
} else {
|
2021-03-12 00:44:08 +00:00
|
|
|
require.False(t, testKubeAPIServerWasCalled, "Should not have proxied the request to the Kube API server, but did")
|
2021-02-23 01:23:11 +00:00
|
|
|
}
|
2021-01-20 00:37:02 +00:00
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
2021-03-12 01:24:52 +00:00
|
|
|
|
|
|
|
type clientCert struct {
|
|
|
|
certPEM, keyPEM []byte
|
|
|
|
}
|
|
|
|
|
|
|
|
func newClientCert(t *testing.T, ca *certauthority.CA, username string, groups []string) *clientCert {
|
2021-03-13 00:09:16 +00:00
|
|
|
certPEM, keyPEM, err := ca.IssueClientCertPEM(username, groups, time.Hour)
|
2021-03-12 01:24:52 +00:00
|
|
|
require.NoError(t, err)
|
|
|
|
return &clientCert{
|
|
|
|
certPEM: certPEM,
|
|
|
|
keyPEM: keyPEM,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func requireCanBindToPort(t *testing.T, port int) {
|
|
|
|
ln, _, listenErr := genericoptions.CreateListener("", "0.0.0.0:"+strconv.Itoa(port), net.ListenConfig{})
|
|
|
|
require.NoError(t, listenErr)
|
|
|
|
require.NoError(t, ln.Close())
|
|
|
|
}
|