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 (
"encoding/base64"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"net/url"
"testing"
"github.com/golang/mock/gomock"
"github.com/stretchr/testify/require"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apiserver/pkg/authentication/authenticator"
"k8s.io/apiserver/pkg/authentication/user"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd/api"
loginv1alpha1 "go.pinniped.dev/generated/1.20/apis/concierge/login/v1alpha1"
"go.pinniped.dev/internal/controller/authenticator/authncache"
"go.pinniped.dev/internal/mocks/mocktokenauthenticator"
"go.pinniped.dev/internal/testutil"
"go.pinniped.dev/internal/testutil/testlogger"
)
func TestImpersonator ( t * testing . T ) {
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
}
_ , _ = w . Write ( [ ] byte ( "successful proxied response" ) )
} )
testServerKubeconfig := rest . Config {
Host : testServerURL ,
BearerToken : "some-service-account-token" ,
TLSClientConfig : rest . TLSClientConfig { CAData : [ ] byte ( testServerCA ) } ,
}
tests := [ ] struct {
name string
getKubeconfig func ( ) ( * rest . Config , error )
wantCreationErr string
request * http . Request
wantHTTPBody string
wantHTTPStatus int
wantLogs [ ] string
expectMockToken func ( * testing . T , * mocktokenauthenticator . MockTokenMockRecorder )
} {
{
name : "fail to get in-cluster config" ,
getKubeconfig : func ( ) ( * rest . Config , error ) {
return nil , fmt . Errorf ( "some kubernetes error" )
} ,
wantCreationErr : "could not get in-cluster config: some kubernetes error" ,
} ,
{
name : "invalid kubeconfig host" ,
getKubeconfig : func ( ) ( * rest . Config , error ) {
return & rest . Config { Host : ":" } , nil
} ,
wantCreationErr : "could not parse host URL from in-cluster config: parse \":\": missing protocol scheme" ,
} ,
{
name : "invalid transport config" ,
getKubeconfig : func ( ) ( * rest . Config , error ) {
return & rest . Config {
Host : "pinniped.dev/blah" ,
ExecProvider : & api . ExecConfig { } ,
AuthProvider : & api . AuthProviderConfig { } ,
} , nil
} ,
wantCreationErr : "could not get in-cluster transport config: execProvider and authProvider cannot be used in combination" ,
} ,
{
name : "fail to get transport from config" ,
getKubeconfig : func ( ) ( * rest . Config , error ) {
return & rest . Config {
Host : "pinniped.dev/blah" ,
BearerToken : "test-bearer-token" ,
Transport : http . DefaultTransport ,
TLSClientConfig : rest . TLSClientConfig { Insecure : true } ,
} , nil
} ,
wantCreationErr : "could not get in-cluster transport: using a custom transport with TLS certificate options or the insecure flag is not allowed" ,
} ,
{
name : "missing authorization header" ,
getKubeconfig : func ( ) ( * rest . Config , error ) { return & testServerKubeconfig , nil } ,
request : & http . Request {
Method : "GET" ,
Header : map [ string ] [ ] string { } ,
URL : validURL ,
} ,
wantHTTPBody : "invalid token encoding\n" ,
wantHTTPStatus : http . StatusBadRequest ,
wantLogs : [ ] string { "\"error\"=\"missing authorization header\" \"msg\"=\"invalid token encoding\" \"method\"=\"GET\" \"url\"=\"http://pinniped.dev/blah\"" } ,
} ,
{
name : "authorization header missing bearer prefix" ,
getKubeconfig : func ( ) ( * rest . Config , error ) { return & testServerKubeconfig , nil } ,
request : & http . Request {
Method : "GET" ,
Header : map [ string ] [ ] string { "Authorization" : { makeTestTokenRequest ( "foo" , "authenticator-one" , "test-token" ) } } ,
URL : validURL ,
} ,
wantHTTPBody : "invalid token encoding\n" ,
wantHTTPStatus : http . StatusBadRequest ,
wantLogs : [ ] string { "\"error\"=\"authorization header must be of type Bearer\" \"msg\"=\"invalid token encoding\" \"method\"=\"GET\" \"url\"=\"http://pinniped.dev/blah\"" } ,
} ,
{
name : "token is not base64 encoded" ,
getKubeconfig : func ( ) ( * rest . Config , error ) { return & testServerKubeconfig , nil } ,
request : & http . Request {
Method : "GET" ,
Header : map [ string ] [ ] string { "Authorization" : { "Bearer !!!" } } ,
URL : validURL ,
} ,
wantHTTPBody : "invalid token encoding\n" ,
wantHTTPStatus : http . StatusBadRequest ,
wantLogs : [ ] string { "\"error\"=\"invalid base64 in encoded bearer token: illegal base64 data at input byte 0\" \"msg\"=\"invalid token encoding\" \"method\"=\"GET\" \"url\"=\"http://pinniped.dev/blah\"" } ,
} ,
{
name : "base64 encoded token is not valid json" ,
getKubeconfig : func ( ) ( * rest . Config , error ) { return & testServerKubeconfig , nil } ,
request : & http . Request {
Method : "GET" ,
Header : map [ string ] [ ] string { "Authorization" : { "Bearer abc" } } ,
URL : validURL ,
} ,
wantHTTPBody : "invalid token encoding\n" ,
wantHTTPStatus : http . StatusBadRequest ,
wantLogs : [ ] string { "\"error\"=\"invalid TokenCredentialRequest encoded in bearer token: invalid character 'i' looking for beginning of value\" \"msg\"=\"invalid token encoding\" \"method\"=\"GET\" \"url\"=\"http://pinniped.dev/blah\"" } ,
} ,
{
name : "token could not be authenticated" ,
getKubeconfig : func ( ) ( * rest . Config , error ) { return & testServerKubeconfig , nil } ,
request : & http . Request {
Method : "GET" ,
Header : map [ string ] [ ] string { "Authorization" : { "Bearer " + makeTestTokenRequest ( "" , "" , "" ) } } ,
URL : validURL ,
} ,
wantHTTPBody : "invalid token\n" ,
wantHTTPStatus : http . StatusUnauthorized ,
wantLogs : [ ] string { "\"error\"=\"no such authenticator\" \"msg\"=\"received invalid token\" \"authenticator\"={\"apiGroup\":null,\"kind\":\"\",\"name\":\"\"} \"authenticatorNamespace\"=\"\" \"method\"=\"GET\" \"url\"=\"http://pinniped.dev/blah\"" } ,
} ,
{
name : "token authenticates as nil" ,
getKubeconfig : func ( ) ( * rest . Config , error ) { return & testServerKubeconfig , nil } ,
request : & http . Request {
Method : "GET" ,
Header : map [ string ] [ ] string { "Authorization" : { "Bearer " + makeTestTokenRequest ( "foo" , "authenticator-one" , "test-token" ) } } ,
URL : validURL ,
} ,
expectMockToken : func ( t * testing . T , recorder * mocktokenauthenticator . MockTokenMockRecorder ) {
recorder . AuthenticateToken ( gomock . Any ( ) , "test-token" ) . Return ( nil , false , nil )
} ,
wantHTTPBody : "not authenticated\n" ,
wantHTTPStatus : http . StatusUnauthorized ,
wantLogs : [ ] string { "\"level\"=0 \"msg\"=\"received token that did not authenticate\" \"authenticator\"={\"apiGroup\":null,\"kind\":\"\",\"name\":\"authenticator-one\"} \"authenticatorNamespace\"=\"foo\" \"method\"=\"GET\" \"url\"=\"http://pinniped.dev/blah\"" } ,
} ,
// happy path
{
name : "token validates" ,
getKubeconfig : func ( ) ( * rest . Config , error ) { return & testServerKubeconfig , nil } ,
request : & http . Request {
Method : "GET" ,
Header : map [ string ] [ ] string {
"Authorization" : { "Bearer " + makeTestTokenRequest ( "foo" , "authenticator-one" , "test-token" ) } ,
"Malicious-Header" : { "test-header-value-1" } ,
"User-Agent" : { "test-user-agent" } ,
} ,
URL : validURL ,
} ,
expectMockToken : func ( t * testing . T , recorder * mocktokenauthenticator . MockTokenMockRecorder ) {
2021-01-22 18:12:12 +00:00
userInfo := user . DefaultInfo {
Name : "test-user" ,
Groups : [ ] string { "test-group-1" , "test-group-2" } ,
UID : "test-uid" ,
}
2021-01-20 00:37:02 +00:00
response := & authenticator . Response { User : & userInfo }
recorder . AuthenticateToken ( gomock . Any ( ) , "test-token" ) . Return ( response , true , nil )
} ,
wantHTTPBody : "successful proxied response" ,
wantHTTPStatus : http . StatusOK ,
2021-01-22 18:12:12 +00:00
wantLogs : [ ] string { "\"level\"=0 \"msg\"=\"proxying authenticated request\" \"authenticator\"={\"apiGroup\":null,\"kind\":\"\",\"name\":\"authenticator-one\"} \"authenticatorNamespace\"=\"foo\" \"method\"=\"GET\" \"url\"=\"http://pinniped.dev/blah\" \"userID\"=\"test-uid\"" } ,
2021-01-20 00:37:02 +00:00
} ,
}
for _ , tt := range tests {
tt := tt
testLog := testlogger . New ( t )
t . Run ( tt . name , func ( t * testing . T ) {
// stole this from cache_test, hopefully it is sufficient
cacheWithMockAuthenticator := authncache . New ( )
ctrl := gomock . NewController ( t )
defer ctrl . Finish ( )
key := authncache . Key { Namespace : "foo" , Name : "authenticator-one" }
mockToken := mocktokenauthenticator . NewMockToken ( ctrl )
cacheWithMockAuthenticator . Store ( key , mockToken )
if tt . expectMockToken != nil {
tt . expectMockToken ( t , mockToken . EXPECT ( ) )
}
proxy , err := newInternal ( cacheWithMockAuthenticator , testLog , tt . getKubeconfig )
if tt . wantCreationErr != "" {
require . EqualError ( t , err , tt . wantCreationErr )
return
}
require . NoError ( t , err )
require . NotNil ( t , proxy )
w := httptest . NewRecorder ( )
proxy . ServeHTTP ( w , tt . request )
if tt . wantHTTPStatus != 0 {
require . Equal ( t , tt . wantHTTPStatus , w . Code )
}
if tt . wantHTTPBody != "" {
require . Equal ( t , tt . wantHTTPBody , w . Body . String ( ) )
}
if tt . wantLogs != nil {
require . Equal ( t , tt . wantLogs , testLog . Lines ( ) )
}
} )
}
}
func makeTestTokenRequest ( namespace string , name string , token string ) string {
reqJSON , err := json . Marshal ( & loginv1alpha1 . TokenCredentialRequest {
ObjectMeta : metav1 . ObjectMeta {
Namespace : namespace ,
} ,
TypeMeta : metav1 . TypeMeta {
Kind : "TokenCredentialRequest" ,
APIVersion : loginv1alpha1 . GroupName + "/v1alpha1" ,
} ,
Spec : loginv1alpha1 . TokenCredentialRequestSpec {
Token : token ,
Authenticator : corev1 . TypedLocalObjectReference { Name : name } ,
} ,
} )
if err != nil {
panic ( err )
}
return base64 . RawURLEncoding . EncodeToString ( reqJSON )
}