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-18 19:32:33 +00:00
"math/rand"
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-16 16:59:07 +00:00
corev1 "k8s.io/api/core/v1"
2021-03-12 00:27:16 +00:00
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2021-03-14 01:25:23 +00:00
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/serializer"
2021-03-19 17:39:55 +00:00
"k8s.io/apimachinery/pkg/util/httpstream"
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"
2021-03-14 01:25:23 +00:00
genericapiserver "k8s.io/apiserver/pkg/server"
2021-03-10 18:30:06 +00:00
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-16 16:59:07 +00:00
"go.pinniped.dev/internal/httputil/roundtripper"
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 )
2021-03-15 16:24:07 +00:00
caContent := dynamiccert . NewCA ( "ca" )
2021-03-11 21:20:25 +00:00
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-15 16:24:07 +00:00
certKeyContent := dynamiccert . NewServingCert ( "cert-key" )
2021-03-11 21:20:25 +00:00
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
2021-03-16 16:59:07 +00:00
clientMutateHeaders func ( http . Header )
2021-03-19 17:39:55 +00:00
clientNextProtos [ ] string
2021-03-12 00:27:16 +00:00
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
} ,
2021-03-19 17:39:55 +00:00
{
name : "happy path with upgrade" ,
clientCert : newClientCert ( t , ca , "test-username2" , [ ] string { "test-group3" , "test-group4" } ) ,
kubeAPIServerClientBearerTokenFile : "required-to-be-set" ,
clientMutateHeaders : func ( header http . Header ) {
header . Add ( "Connection" , "Upgrade" )
header . Add ( "Upgrade" , "spdy/3.1" )
if ok := httpstream . IsUpgradeRequest ( & http . Request { Header : header } ) ; ! ok {
panic ( "request must be upgrade in this test" )
}
} ,
clientNextProtos : [ ] string { "http/1.1" } , // we need to use http1 as http2 does not support upgrades, see http2checkConnHeaders
wantKubeAPIServerRequestHeaders : http . Header {
"Impersonate-User" : { "test-username2" } ,
"Impersonate-Group" : { "test-group3" , "test-group4" , "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" } ,
"Connection" : { "Upgrade" } ,
"Upgrade" : { "spdy/3.1" } ,
} ,
} ,
2021-03-19 19:35:06 +00:00
{
name : "happy path ignores forwarded header" ,
clientCert : newClientCert ( t , ca , "test-username2" , [ ] string { "test-group3" , "test-group4" } ) ,
kubeAPIServerClientBearerTokenFile : "required-to-be-set" ,
clientMutateHeaders : func ( header http . Header ) {
header . Add ( "X-Forwarded-For" , "example.com" )
} ,
wantKubeAPIServerRequestHeaders : http . Header {
"Impersonate-User" : { "test-username2" } ,
"Impersonate-Group" : { "test-group3" , "test-group4" , "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 : "happy path ignores forwarded header canonicalization" ,
clientCert : newClientCert ( t , ca , "test-username2" , [ ] string { "test-group3" , "test-group4" } ) ,
kubeAPIServerClientBearerTokenFile : "required-to-be-set" ,
clientMutateHeaders : func ( header http . Header ) {
header . Add ( "x-FORWARDED-for" , "example.com" )
} ,
wantKubeAPIServerRequestHeaders : http . Header {
"Impersonate-User" : { "test-username2" } ,
"Impersonate-Group" : { "test-group3" , "test-group4" , "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-16 16:59:07 +00:00
{
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" ,
} ,
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-16 16:59:07 +00:00
// After failing to start and after shutdown, the impersonator port should be available again.
defer requireCanBindToPort ( t , port )
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
2021-03-16 16:59:07 +00:00
var testKubeAPIServerSawHeaders http . Header
2021-03-12 00:27:16 +00:00
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 )
// 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 {
2021-03-19 17:39:55 +00:00
CAData : ca . Bundle ( ) ,
CertData : tt . clientCert . certPEM ,
KeyData : tt . clientCert . keyPEM ,
NextProtos : tt . clientNextProtos ,
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 ,
2021-03-16 16:59:07 +00:00
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 )
} )
} ,
2021-03-12 00:27:16 +00:00
}
// 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 )
2021-03-16 16:59:07 +00:00
require . Equal ( t , & corev1 . NamespaceList { } , listResponse )
2021-03-12 00:27:16 +00:00
} else {
require . NoError ( t , err )
2021-03-16 16:59:07 +00:00
require . Equal ( t , & corev1 . NamespaceList {
Items : [ ] corev1 . Namespace {
2021-03-12 00:27:16 +00:00
{ ObjectMeta : metav1 . ObjectMeta { Name : "namespace1" } } ,
{ ObjectMeta : metav1 . ObjectMeta { Name : "namespace2" } } ,
} ,
} , listResponse )
}
2021-03-16 16:59:07 +00:00
// 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 )
2021-03-12 00:27:16 +00:00
// Stop the impersonator server.
close ( stopCh )
exitErr := <- errCh
require . NoError ( t , exitErr )
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
2021-03-14 01:25:23 +00:00
reqInfo := & request . RequestInfo {
IsResourceRequest : false ,
Path : validURL . Path ,
Verb : "get" ,
}
r = r . WithContext ( request . WithRequestInfo ( ctx , reqInfo ) )
2021-02-09 18:25:24 +00:00
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
} ,
2021-03-18 19:32:33 +00:00
wantCreationErr : "could not get http/1.1 round tripper: could not get in-cluster transport config: execProvider and authProvider cannot be used in combination" ,
2021-01-20 00:37:02 +00:00
} ,
{
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
} ,
2021-03-18 19:32:33 +00:00
wantCreationErr : "could not get http/1.1 round tripper: using a custom transport with TLS certificate options or the insecure flag is not allowed" ,
2021-01-20 00:37:02 +00:00
} ,
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 ) ,
2021-03-14 01:25:23 +00:00
wantHTTPBody : ` { "kind":"Status","apiVersion":"v1","metadata": { },"status":"Failure","message":"Internal error occurred: invalid impersonation","reason":"InternalError","details": { "causes":[ { "message":"invalid impersonation"}]},"code":500} ` + "\n" ,
2021-03-10 18:30:06 +00:00
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 ) ,
2021-03-14 01:25:23 +00:00
wantHTTPBody : ` { "kind":"Status","apiVersion":"v1","metadata": { },"status":"Failure","message":"Internal error occurred: invalid impersonation","reason":"InternalError","details": { "causes":[ { "message":"invalid impersonation"}]},"code":500} ` + "\n" ,
2021-03-10 18:30:06 +00:00
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 ) ,
2021-03-14 01:25:23 +00:00
wantHTTPBody : ` { "kind":"Status","apiVersion":"v1","metadata": { },"status":"Failure","message":"Internal error occurred: invalid impersonation","reason":"InternalError","details": { "causes":[ { "message":"invalid impersonation"}]},"code":500} ` + "\n" ,
2021-03-10 18:30:06 +00:00
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 ) ,
2021-03-14 01:25:23 +00:00
wantHTTPBody : ` { "kind":"Status","apiVersion":"v1","metadata": { },"status":"Failure","message":"Internal error occurred: invalid impersonation","reason":"InternalError","details": { "causes":[ { "message":"invalid impersonation"}]},"code":500} ` + "\n" ,
2021-03-10 18:30:06 +00:00
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 ) ,
2021-03-14 01:25:23 +00:00
wantHTTPBody : ` { "kind":"Status","apiVersion":"v1","metadata": { },"status":"Failure","message":"Internal error occurred: invalid authorization header","reason":"InternalError","details": { "causes":[ { "message":"invalid authorization header"}]},"code":500} ` + "\n" ,
2021-03-10 18:30:06 +00:00
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 ) ,
2021-03-14 01:25:23 +00:00
wantHTTPBody : ` { "kind":"Status","apiVersion":"v1","metadata": { },"status":"Failure","message":"Internal error occurred: invalid user","reason":"InternalError","details": { "causes":[ { "message":"invalid user"}]},"code":500} ` + "\n" ,
2021-03-10 18:30:06 +00:00
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-14 01:25:23 +00:00
wantHTTPBody : ` { "kind":"Status","apiVersion":"v1","metadata": { },"status":"Failure","message":"Internal error occurred: unimplemented functionality - unable to act as current user","reason":"InternalError","details": { "causes":[ { "message":"unimplemented functionality - unable to act as current user"}]},"code":500} ` + "\n" ,
wantHTTPStatus : http . StatusInternalServerError ,
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-03-14 01:25:23 +00:00
// this is not a valid way to get a server config, but it is good enough for a unit test
scheme := runtime . NewScheme ( )
metav1 . AddToGroupVersion ( scheme , metav1 . Unversioned )
codecs := serializer . NewCodecFactory ( scheme )
serverConfig := genericapiserver . NewRecommendedConfig ( codecs )
2021-01-20 00:37:02 +00:00
w := httptest . NewRecorder ( )
2021-03-12 00:44:08 +00:00
2021-03-19 17:39:55 +00:00
r := tt . request
wantKubeAPIServerRequestHeaders := tt . wantKubeAPIServerRequestHeaders
// take the isUpgradeRequest branch randomly to make sure we exercise both branches
forceUpgradeRequest := rand . Int ( ) % 2 == 0 //nolint:gosec // we do not care if this is cryptographically secure
if forceUpgradeRequest && len ( r . Header . Get ( "Upgrade" ) ) == 0 {
r = r . Clone ( r . Context ( ) )
r . Header . Add ( "Connection" , "Upgrade" )
r . Header . Add ( "Upgrade" , "spdy/3.1" )
wantKubeAPIServerRequestHeaders = wantKubeAPIServerRequestHeaders . Clone ( )
if wantKubeAPIServerRequestHeaders == nil {
wantKubeAPIServerRequestHeaders = http . Header { }
}
wantKubeAPIServerRequestHeaders . Add ( "Connection" , "Upgrade" )
wantKubeAPIServerRequestHeaders . Add ( "Upgrade" , "spdy/3.1" )
}
requestBeforeServe := r . Clone ( r . Context ( ) )
impersonatorHTTPHandlerFunc ( & serverConfig . Config ) . ServeHTTP ( w , r )
require . Equal ( t , requestBeforeServe , r , "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" )
2021-03-19 17:39:55 +00:00
require . Equal ( t , 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 ( ) )
}