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-04-09 21:52:53 +00:00
authenticationv1 "k8s.io/api/authentication/v1"
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-04-09 21:52:53 +00:00
auditinternal "k8s.io/apiserver/pkg/apis/audit"
2021-04-20 15:19:58 +00:00
"k8s.io/apiserver/pkg/authentication/authenticator"
"k8s.io/apiserver/pkg/authentication/request/bearertoken"
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-04-20 15:19:58 +00:00
"go.pinniped.dev/internal/constable"
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 ) {
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-04-09 21:52:53 +00:00
// turn off this code path for all tests because it does not handle the config we remove correctly
2021-03-10 18:30:06 +00:00
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 ) {
2021-04-09 21:52:53 +00:00
header [ "x-FORWARDED-for" ] = append ( header [ "x-FORWARDED-for" ] , "example.com" )
2021-03-19 19:35:06 +00:00
} ,
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" } ,
} ,
} ,
2021-04-20 15:19:58 +00:00
{
name : "when there is no client cert on request but it has basic auth, it is still an anonymous request" ,
clientCert : & clientCert { } ,
clientMutateHeaders : func ( header http . Header ) {
header . Set ( "Test" , "val" )
req := & http . Request { Header : header }
req . SetBasicAuth ( "foo" , "bar" )
} ,
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" } ,
"Test" : { "val" } ,
} ,
} ,
2021-03-12 01:11:38 +00:00
{
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
} ,
{
2021-04-09 21:52:53 +00:00
name : "nested impersonation by regular users calls delegating authorizer" ,
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" ,
2021-04-09 21:52:53 +00:00
// this fails because the delegating authorizer in this test only allows system:masters and fails everything else
2021-03-12 00:27:16 +00:00
wantError : ` users "some-other-username" is forbidden: User "test-username" ` +
2021-04-09 21:52:53 +00:00
` cannot impersonate resource "users" in API group "" at the cluster scope ` ,
2021-03-10 18:30:06 +00:00
} ,
{
2021-04-09 21:52:53 +00:00
name : "nested impersonation by admin users calls delegating authorizer" ,
clientCert : newClientCert ( t , ca , "test-admin" , [ ] string { "system:masters" , "test-group2" } ) ,
clientImpersonateUser : rest . ImpersonationConfig {
UserName : "fire" ,
Groups : [ ] string { "elements" } ,
Extra : map [ string ] [ ] string {
"colors" : { "red" , "orange" , "blue" } ,
// gke
"iam.gke.io/user-assertion" : { "good" , "stuff" } ,
"user-assertion.cloud.google.com" : { "smaller" , "things" } ,
// openshift
"scopes.authorization.openshift.io" : { "user:info" , "user:full" , "user:check-access" } ,
// openstack
"alpha.kubernetes.io/identity/roles" : { "a-role1" , "a-role2" } ,
"alpha.kubernetes.io/identity/project/id" : { "a-project-id" } ,
"alpha.kubernetes.io/identity/project/name" : { "a-project-name" } ,
"alpha.kubernetes.io/identity/user/domain/id" : { "a-domain-id" } ,
"alpha.kubernetes.io/identity/user/domain/name" : { "a-domain-name" } ,
} ,
} ,
kubeAPIServerClientBearerTokenFile : "required-to-be-set" ,
wantKubeAPIServerRequestHeaders : http . Header {
"Impersonate-User" : { "fire" } ,
"Impersonate-Group" : { "elements" , "system:authenticated" } ,
"Impersonate-Extra-Colors" : { "red" , "orange" , "blue" } ,
"Impersonate-Extra-Iam.gke.io%2fuser-Assertion" : { "good" , "stuff" } ,
"Impersonate-Extra-User-Assertion.cloud.google.com" : { "smaller" , "things" } ,
"Impersonate-Extra-Scopes.authorization.openshift.io" : { "user:info" , "user:full" , "user:check-access" } ,
"Impersonate-Extra-Alpha.kubernetes.io%2fidentity%2froles" : { "a-role1" , "a-role2" } ,
"Impersonate-Extra-Alpha.kubernetes.io%2fidentity%2fproject%2fid" : { "a-project-id" } ,
"Impersonate-Extra-Alpha.kubernetes.io%2fidentity%2fproject%2fname" : { "a-project-name" } ,
"Impersonate-Extra-Alpha.kubernetes.io%2fidentity%2fuser%2fdomain%2fid" : { "a-domain-id" } ,
"Impersonate-Extra-Alpha.kubernetes.io%2fidentity%2fuser%2fdomain%2fname" : { "a-domain-name" } ,
"Impersonate-Extra-Original-User-Info.impersonation-Proxy.concierge.pinniped.dev" : { ` { "username":"test-admin","groups":["test-group2","system:masters","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 : "nested impersonation by admin users cannot impersonate UID" ,
clientCert : newClientCert ( t , ca , "test-admin" , [ ] string { "system:masters" , "test-group2" } ) ,
clientImpersonateUser : rest . ImpersonationConfig { UserName : "some-other-username" } ,
clientMutateHeaders : func ( header http . Header ) {
header [ "Impersonate-Uid" ] = [ ] string { "root" }
} ,
2021-03-12 00:27:16 +00:00
kubeAPIServerClientBearerTokenFile : "required-to-be-set" ,
2021-04-09 21:52:53 +00:00
wantError : "Internal error occurred: invalid impersonation" ,
} ,
{
name : "nested impersonation by admin users cannot impersonate UID header canonicalization" ,
clientCert : newClientCert ( t , ca , "test-admin" , [ ] string { "system:masters" , "test-group2" } ) ,
clientImpersonateUser : rest . ImpersonationConfig { UserName : "some-other-username" } ,
clientMutateHeaders : func ( header http . Header ) {
header [ "imPerSoNaTE-uid" ] = [ ] string { "magic" }
} ,
kubeAPIServerClientBearerTokenFile : "required-to-be-set" ,
wantError : "Internal error occurred: invalid impersonation" ,
} ,
{
name : "nested impersonation by admin users cannot use reserved key" ,
clientCert : newClientCert ( t , ca , "test-admin" , [ ] string { "system:masters" , "test-group2" } ) ,
clientImpersonateUser : rest . ImpersonationConfig {
UserName : "other-user-to-impersonate" ,
Groups : [ ] string { "other-peeps" } ,
Extra : map [ string ] [ ] string {
"key" : { "good" } ,
"something.impersonation-proxy.concierge.pinniped.dev" : { "bad data" } ,
} ,
} ,
kubeAPIServerClientBearerTokenFile : "required-to-be-set" ,
wantError : "Internal error occurred: unimplemented functionality - unable to act as current user" ,
} ,
{
name : "nested impersonation by admin users cannot use invalid key" ,
clientCert : newClientCert ( t , ca , "test-admin" , [ ] string { "system:masters" , "test-group2" } ) ,
clientImpersonateUser : rest . ImpersonationConfig {
UserName : "panda" ,
Groups : [ ] string { "other-peeps" } ,
Extra : map [ string ] [ ] string {
"party~~time" : { "danger" } ,
} ,
} ,
kubeAPIServerClientBearerTokenFile : "required-to-be-set" ,
wantError : "Internal error occurred: unimplemented functionality - unable to act as current user" ,
} ,
{
name : "nested impersonation by admin users can use uppercase key because impersonation is lossy" ,
clientCert : newClientCert ( t , ca , "test-admin" , [ ] string { "system:masters" , "test-group2" } ) ,
clientImpersonateUser : rest . ImpersonationConfig {
UserName : "panda" ,
Groups : [ ] string { "other-peeps" } ,
Extra : map [ string ] [ ] string {
"ROAR" : { "tiger" } , // by the time our code sees this key, it is lowercased to "roar"
} ,
} ,
kubeAPIServerClientBearerTokenFile : "required-to-be-set" ,
wantKubeAPIServerRequestHeaders : http . Header {
"Impersonate-User" : { "panda" } ,
"Impersonate-Group" : { "other-peeps" , "system:authenticated" } ,
"Impersonate-Extra-Roar" : { "tiger" } ,
"Impersonate-Extra-Original-User-Info.impersonation-Proxy.concierge.pinniped.dev" : { ` { "username":"test-admin","groups":["test-group2","system:masters","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-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 ) {
2021-04-09 21:52:53 +00:00
header [ "imPerSonaTE-USer" ] = [ ] string { "PANDA" }
2021-03-16 16:59:07 +00:00
} ,
kubeAPIServerClientBearerTokenFile : "required-to-be-set" ,
wantError : ` users "PANDA" is forbidden: User "test-username" ` +
2021-04-09 21:52:53 +00:00
` cannot impersonate resource "users" in API group "" at the cluster scope ` ,
2021-03-16 16:59:07 +00:00
} ,
{
name : "header canonicalization future UID header" ,
clientCert : newClientCert ( t , ca , "test-username" , [ ] string { "test-group1" , "test-group2" } ) ,
clientMutateHeaders : func ( header http . Header ) {
2021-04-09 21:52:53 +00:00
header [ "imPerSonaTE-uid" ] = [ ] string { "007" }
2021-03-16 16:59:07 +00:00
} ,
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 ) {
2021-04-09 21:52:53 +00:00
header [ "Impersonate-Uid" ] = [ ] string { "008" }
2021-03-16 16:59:07 +00:00
} ,
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
t . Run ( tt . name , func ( t * testing . T ) {
2021-04-09 21:52:53 +00:00
t . Parallel ( )
// we need to create this listener ourselves because the API server
// code treats (port == 0 && listener == nil) to mean "do nothing"
listener , port , err := genericoptions . CreateListener ( "" , "127.0.0.1:0" , net . ListenConfig { } )
require . NoError ( t , err )
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-04-09 21:52:53 +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
options . SecureServing . Listener = listener // use our listener with the dynamic port
}
// Create an impersonator. Use an invalid port number to make sure our listener override works.
runner , constructionErr := newInternal ( - 1000 , certKeyContent , caContent , clientOpts , recOpts )
2021-03-12 00:27:16 +00:00
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
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
2021-04-20 15:19:58 +00:00
authenticator authenticator . Request
2021-02-23 01:23:11 +00:00
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-04-20 15:19:58 +00:00
request : newRequest ( t , map [ string ] [ ] string { "Impersonate-User" : { "some-user" } } , nil , 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-04-20 15:19:58 +00:00
request : newRequest ( t , map [ string ] [ ] string { "Impersonate-Group" : { "some-group" } } , nil , 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-04-20 15:19:58 +00:00
request : newRequest ( t , map [ string ] [ ] string { "Impersonate-Extra-something" : { "something" } } , nil , 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" ,
2021-04-20 15:19:58 +00:00
request : newRequest ( t , map [ string ] [ ] string { "Impersonate-Something" : { "some-newfangled-impersonate-header" } } , nil , 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" ,
2021-04-20 15:19:58 +00:00
request : newRequest ( t , map [ string ] [ ] string { "Authorization" : { "panda" } } , nil , 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" ,
2021-04-20 15:19:58 +00:00
request : newRequest ( t , map [ string ] [ ] string { } , nil , 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" ,
2021-04-20 15:19:58 +00:00
request : newRequest ( t , map [ string ] [ ] string { } , & user . DefaultInfo { UID : "007" } , nil , "" ) ,
2021-04-09 21:52:53 +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 ,
} ,
{
name : "authenticated user but missing audit event" ,
request : func ( ) * http . Request {
2021-04-20 15:19:58 +00:00
req := newRequest ( t , map [ string ] [ ] string {
2021-04-09 21:52:53 +00:00
"User-Agent" : { "test-user-agent" } ,
"Connection" : { "Upgrade" } ,
"Upgrade" : { "some-upgrade" } ,
"Other-Header" : { "test-header-value-1" } ,
} , & user . DefaultInfo {
Name : testUser ,
Groups : testGroups ,
Extra : testExtra ,
2021-04-20 15:19:58 +00:00
} , nil , "" )
2021-04-09 21:52:53 +00:00
ctx := request . WithAuditEvent ( req . Context ( ) , nil )
req = req . WithContext ( ctx )
return req
} ( ) ,
wantHTTPBody : ` { "kind":"Status","apiVersion":"v1","metadata": { },"status":"Failure","message":"Internal error occurred: invalid audit event","reason":"InternalError","details": { "causes":[ { "message":"invalid audit event"}]},"code":500} ` + "\n" ,
wantHTTPStatus : http . StatusInternalServerError ,
} ,
{
name : "authenticated user with upper case extra" ,
2021-04-20 15:19:58 +00:00
request : newRequest ( t , map [ string ] [ ] string {
2021-04-09 21:52:53 +00:00
"User-Agent" : { "test-user-agent" } ,
"Connection" : { "Upgrade" } ,
"Upgrade" : { "some-upgrade" } ,
"Content-Type" : { "some-type" } ,
"Content-Length" : { "some-length" } ,
"Other-Header" : { "test-header-value-1" } ,
} , & user . DefaultInfo {
Name : testUser ,
Groups : testGroups ,
Extra : map [ string ] [ ] string {
"valid-key" : { "valid-value" } ,
"Invalid-key" : { "still-valid-value" } ,
} ,
2021-04-20 15:19:58 +00:00
} , nil , "" ) ,
2021-04-09 21:52:53 +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 ,
} ,
{
name : "authenticated user with upper case extra across multiple lines" ,
2021-04-20 15:19:58 +00:00
request : newRequest ( t , map [ string ] [ ] string {
2021-04-09 21:52:53 +00:00
"User-Agent" : { "test-user-agent" } ,
"Connection" : { "Upgrade" } ,
"Upgrade" : { "some-upgrade" } ,
"Content-Type" : { "some-type" } ,
"Content-Length" : { "some-length" } ,
"Other-Header" : { "test-header-value-1" } ,
} , & user . DefaultInfo {
Name : testUser ,
Groups : testGroups ,
Extra : map [ string ] [ ] string {
"valid-key" : { "valid-value" } ,
"valid-data\nInvalid-key" : { "still-valid-value" } ,
} ,
2021-04-20 15:19:58 +00:00
} , nil , "" ) ,
2021-04-09 21:52:53 +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 ,
} ,
{
name : "authenticated user with reserved extra key" ,
2021-04-20 15:19:58 +00:00
request : newRequest ( t , map [ string ] [ ] string {
2021-04-09 21:52:53 +00:00
"User-Agent" : { "test-user-agent" } ,
"Connection" : { "Upgrade" } ,
"Upgrade" : { "some-upgrade" } ,
"Content-Type" : { "some-type" } ,
"Content-Length" : { "some-length" } ,
"Other-Header" : { "test-header-value-1" } ,
} , & user . DefaultInfo {
Name : testUser ,
Groups : testGroups ,
Extra : map [ string ] [ ] string {
"valid-key" : { "valid-value" } ,
"foo.impersonation-proxy.concierge.pinniped.dev" : { "still-valid-value" } ,
} ,
2021-04-20 15:19:58 +00:00
} , nil , "" ) ,
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 ,
} ,
{
name : "authenticated user with UID but no bearer token" ,
request : newRequest ( t , map [ string ] [ ] string {
"User-Agent" : { "test-user-agent" } ,
"Connection" : { "Upgrade" } ,
"Upgrade" : { "some-upgrade" } ,
"Content-Type" : { "some-type" } ,
"Content-Length" : { "some-length" } ,
"Other-Header" : { "test-header-value-1" } ,
} , & user . DefaultInfo {
UID : "-" , // anything non-empty, rest of the fields get ignored in this code path
} ,
& auditinternal . Event {
User : authenticationv1 . UserInfo {
Username : testUser ,
UID : "fancy-uid" ,
Groups : testGroups ,
Extra : map [ string ] authenticationv1 . ExtraValue {
"extra-1" : { "some" , "extra" , "stuff" } ,
"extra-2" : { "some" , "more" , "extra" , "stuff" } ,
} ,
} ,
ImpersonatedUser : nil ,
} ,
"" ,
) ,
authenticator : nil ,
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 ,
} ,
{
name : "authenticated user with UID and bearer token and nested impersonation" ,
request : newRequest ( t , map [ string ] [ ] string {
"User-Agent" : { "test-user-agent" } ,
"Connection" : { "Upgrade" } ,
"Upgrade" : { "some-upgrade" } ,
"Content-Type" : { "some-type" } ,
"Content-Length" : { "some-length" } ,
"Other-Header" : { "test-header-value-1" } ,
} , & user . DefaultInfo {
UID : "-" , // anything non-empty, rest of the fields get ignored in this code path
} ,
& auditinternal . Event {
User : authenticationv1 . UserInfo {
Username : "dude" ,
UID : "--1--" ,
Groups : [ ] string { "--a--" , "--b--" } ,
Extra : map [ string ] authenticationv1 . ExtraValue {
"--c--" : { "--d--" } ,
"--e--" : { "--f--" } ,
} ,
} ,
ImpersonatedUser : & authenticationv1 . UserInfo { } ,
} ,
"token-from-user-nested" ,
) ,
authenticator : nil ,
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 ,
} ,
{
name : "authenticated user with UID and bearer token results in error" ,
request : newRequest ( t , map [ string ] [ ] string {
"User-Agent" : { "test-user-agent" } ,
"Connection" : { "Upgrade" } ,
"Upgrade" : { "some-upgrade" } ,
"Content-Type" : { "some-type" } ,
"Content-Length" : { "some-length" } ,
"Other-Header" : { "test-header-value-1" } ,
} , & user . DefaultInfo {
UID : "-" , // anything non-empty, rest of the fields get ignored in this code path
} ,
& auditinternal . Event {
User : authenticationv1 . UserInfo {
Username : "dude" ,
UID : "--1--" ,
Groups : [ ] string { "--a--" , "--b--" } ,
Extra : map [ string ] authenticationv1 . ExtraValue {
"--c--" : { "--d--" } ,
"--e--" : { "--f--" } ,
} ,
} ,
ImpersonatedUser : nil ,
} ,
"some-non-empty-token" ,
) ,
authenticator : testTokenAuthenticator ( t , "" , nil , constable . Error ( "some err" ) ) ,
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 ,
} ,
{
name : "authenticated user with UID and bearer token does not authenticate" ,
request : newRequest ( t , map [ string ] [ ] string {
"User-Agent" : { "test-user-agent" } ,
"Connection" : { "Upgrade" } ,
"Upgrade" : { "some-upgrade" } ,
"Content-Type" : { "some-type" } ,
"Content-Length" : { "some-length" } ,
"Other-Header" : { "test-header-value-1" } ,
} , & user . DefaultInfo {
UID : "-" , // anything non-empty, rest of the fields get ignored in this code path
} ,
& auditinternal . Event {
User : authenticationv1 . UserInfo {
Username : "dude" ,
UID : "--1--" ,
Groups : [ ] string { "--a--" , "--b--" } ,
Extra : map [ string ] authenticationv1 . ExtraValue {
"--c--" : { "--d--" } ,
"--e--" : { "--f--" } ,
} ,
} ,
ImpersonatedUser : nil ,
} ,
"this-token-does-not-work" ,
) ,
authenticator : testTokenAuthenticator ( t , "some-other-token-works" , nil , nil ) ,
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 ,
} ,
{
name : "authenticated user with UID and bearer token authenticates as different user" ,
request : newRequest ( t , map [ string ] [ ] string {
"User-Agent" : { "test-user-agent" } ,
"Connection" : { "Upgrade" } ,
"Upgrade" : { "some-upgrade" } ,
"Content-Type" : { "some-type" } ,
"Content-Length" : { "some-length" } ,
"Other-Header" : { "test-header-value-1" } ,
} , & user . DefaultInfo {
UID : "-" , // anything non-empty, rest of the fields get ignored in this code path
} ,
& auditinternal . Event {
User : authenticationv1 . UserInfo {
Username : "dude" ,
UID : "--1--" ,
Groups : [ ] string { "--a--" , "--b--" } ,
Extra : map [ string ] authenticationv1 . ExtraValue {
"--c--" : { "--d--" } ,
"--e--" : { "--f--" } ,
} ,
} ,
ImpersonatedUser : nil ,
} ,
"this-token-does-work" ,
) ,
authenticator : testTokenAuthenticator ( t , "this-token-does-work" , & user . DefaultInfo { Name : "someone-else" } , nil ) ,
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-04-20 15:19:58 +00:00
request : newRequest ( t , 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-04-20 15:19:58 +00:00
} , nil , "" ) ,
2021-04-09 21:52:53 +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" } ,
"Other-Header" : { "test-header-value-1" } ,
} ,
wantHTTPBody : "successful proxied response" ,
wantHTTPStatus : http . StatusOK ,
} ,
2021-04-20 15:19:58 +00:00
{
name : "authenticated user with UID and bearer token" ,
request : newRequest ( t , map [ string ] [ ] string {
"User-Agent" : { "test-user-agent" } ,
"Accept" : { "some-accepted-format" } ,
"Accept-Encoding" : { "some-accepted-encoding" } ,
"Connection" : { "Upgrade" } ,
"Upgrade" : { "some-upgrade" } ,
"Content-Type" : { "some-type" } ,
"Content-Length" : { "some-length" } ,
"Other-Header" : { "test-header-value-1" } ,
} , & user . DefaultInfo {
UID : "-" , // anything non-empty, rest of the fields get ignored in this code path
} ,
& auditinternal . Event {
User : authenticationv1 . UserInfo {
Username : testUser ,
UID : "fancy-uid" ,
Groups : testGroups ,
Extra : map [ string ] authenticationv1 . ExtraValue {
"extra-1" : { "some" , "extra" , "stuff" } ,
"extra-2" : { "some" , "more" , "extra" , "stuff" } ,
} ,
} ,
ImpersonatedUser : nil ,
} ,
"token-from-user" ,
) ,
authenticator : testTokenAuthenticator (
t ,
"token-from-user" ,
& user . DefaultInfo {
Name : testUser ,
UID : "fancy-uid" ,
Groups : testGroups ,
Extra : testExtra ,
} ,
nil ,
) ,
wantKubeAPIServerRequestHeaders : map [ string ] [ ] string {
"Authorization" : { "Bearer token-from-user" } ,
"User-Agent" : { "test-user-agent" } ,
"Accept" : { "some-accepted-format" } ,
"Accept-Encoding" : { "some-accepted-encoding" } ,
"Connection" : { "Upgrade" } ,
"Upgrade" : { "some-upgrade" } ,
"Content-Type" : { "some-type" } ,
"Other-Header" : { "test-header-value-1" } ,
} ,
wantHTTPBody : "successful proxied response" ,
wantHTTPStatus : http . StatusOK ,
} ,
2021-04-09 21:52:53 +00:00
{
name : "authenticated gke user" ,
2021-04-20 15:19:58 +00:00
request : newRequest ( t , map [ string ] [ ] string {
2021-04-09 21:52:53 +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
} , & user . DefaultInfo {
Name : "username@company.com" ,
Groups : [ ] string { "system:authenticated" } ,
Extra : map [ string ] [ ] string {
// make sure we can handle these keys
"iam.gke.io/user-assertion" : { "ABC" } ,
"user-assertion.cloud.google.com" : { "XYZ" } ,
} ,
2021-04-20 15:19:58 +00:00
} , nil , "" ) ,
2021-04-09 21:52:53 +00:00
wantKubeAPIServerRequestHeaders : map [ string ] [ ] string {
"Authorization" : { "Bearer some-service-account-token" } ,
"Impersonate-Extra-Iam.gke.io%2fuser-Assertion" : { "ABC" } ,
"Impersonate-Extra-User-Assertion.cloud.google.com" : { "XYZ" } ,
"Impersonate-Group" : { "system:authenticated" } ,
"Impersonate-User" : { "username@company.com" } ,
"User-Agent" : { "test-user-agent" } ,
"Accept" : { "some-accepted-format" } ,
"Accept-Encoding" : { "some-accepted-encoding" } ,
"Connection" : { "Upgrade" } ,
"Upgrade" : { "some-upgrade" } ,
"Content-Type" : { "some-type" } ,
"Other-Header" : { "test-header-value-1" } ,
} ,
wantHTTPBody : "successful proxied response" ,
wantHTTPStatus : http . StatusOK ,
} ,
{
name : "authenticated openshift/openstack user" ,
2021-04-20 15:19:58 +00:00
request : newRequest ( t , map [ string ] [ ] string {
2021-04-09 21:52:53 +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
} , & user . DefaultInfo {
Name : "kube:admin" ,
// both of these auth stacks set UID but we cannot handle it today
// UID: "user-id",
Groups : [ ] string { "system:cluster-admins" , "system:authenticated" } ,
Extra : map [ string ] [ ] string {
// openshift
"scopes.authorization.openshift.io" : { "user:info" , "user:full" } ,
// openstack
"alpha.kubernetes.io/identity/roles" : { "role1" , "role2" } ,
"alpha.kubernetes.io/identity/project/id" : { "project-id" } ,
"alpha.kubernetes.io/identity/project/name" : { "project-name" } ,
"alpha.kubernetes.io/identity/user/domain/id" : { "domain-id" } ,
"alpha.kubernetes.io/identity/user/domain/name" : { "domain-name" } ,
} ,
2021-04-20 15:19:58 +00:00
} , nil , "" ) ,
2021-04-09 21:52:53 +00:00
wantKubeAPIServerRequestHeaders : map [ string ] [ ] string {
"Authorization" : { "Bearer some-service-account-token" } ,
"Impersonate-Extra-Scopes.authorization.openshift.io" : { "user:info" , "user:full" } ,
"Impersonate-Extra-Alpha.kubernetes.io%2fidentity%2froles" : { "role1" , "role2" } ,
"Impersonate-Extra-Alpha.kubernetes.io%2fidentity%2fproject%2fid" : { "project-id" } ,
"Impersonate-Extra-Alpha.kubernetes.io%2fidentity%2fproject%2fname" : { "project-name" } ,
"Impersonate-Extra-Alpha.kubernetes.io%2fidentity%2fuser%2fdomain%2fid" : { "domain-id" } ,
"Impersonate-Extra-Alpha.kubernetes.io%2fidentity%2fuser%2fdomain%2fname" : { "domain-name" } ,
"Impersonate-Group" : { "system:cluster-admins" , "system:authenticated" } ,
"Impersonate-User" : { "kube:admin" } ,
"User-Agent" : { "test-user-agent" } ,
"Accept" : { "some-accepted-format" } ,
"Accept-Encoding" : { "some-accepted-encoding" } ,
"Connection" : { "Upgrade" } ,
"Upgrade" : { "some-upgrade" } ,
"Content-Type" : { "some-type" } ,
"Other-Header" : { "test-header-value-1" } ,
} ,
wantHTTPBody : "successful proxied response" ,
wantHTTPStatus : http . StatusOK ,
} ,
{
name : "authenticated user with almost reserved key" ,
2021-04-20 15:19:58 +00:00
request : newRequest ( t , map [ string ] [ ] string {
2021-04-09 21:52:53 +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
} , & user . DefaultInfo {
Name : "username@company.com" ,
Groups : [ ] string { "system:authenticated" } ,
Extra : map [ string ] [ ] string {
"foo.iimpersonation-proxy.concierge.pinniped.dev" : { "still-valid-value" } ,
} ,
2021-04-20 15:19:58 +00:00
} , nil , "" ) ,
2021-04-09 21:52:53 +00:00
wantKubeAPIServerRequestHeaders : map [ string ] [ ] string {
"Authorization" : { "Bearer some-service-account-token" } ,
"Impersonate-Extra-Foo.iimpersonation-Proxy.concierge.pinniped.dev" : { "still-valid-value" } ,
"Impersonate-Group" : { "system:authenticated" } ,
"Impersonate-User" : { "username@company.com" } ,
"User-Agent" : { "test-user-agent" } ,
"Accept" : { "some-accepted-format" } ,
"Accept-Encoding" : { "some-accepted-encoding" } ,
"Connection" : { "Upgrade" } ,
"Upgrade" : { "some-upgrade" } ,
"Content-Type" : { "some-type" } ,
"Other-Header" : { "test-header-value-1" } ,
} ,
wantHTTPBody : "successful proxied response" ,
wantHTTPStatus : http . StatusOK ,
} ,
{
name : "authenticated user with almost reserved key and nested impersonation" ,
2021-04-20 15:19:58 +00:00
request : newRequest ( t , map [ string ] [ ] string {
2021-04-09 21:52:53 +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
} , & user . DefaultInfo {
Name : "username@company.com" ,
Groups : [ ] string { "system:authenticated" } ,
Extra : map [ string ] [ ] string {
"original-user-info.impersonation-proxyy.concierge.pinniped.dev" : { "log confusion stuff here" } ,
} ,
} ,
& auditinternal . Event {
User : authenticationv1 . UserInfo {
Username : "panda" ,
UID : "0x001" ,
Groups : [ ] string { "bears" , "friends" } ,
Extra : map [ string ] authenticationv1 . ExtraValue {
"original-user-info.impersonation-proxy.concierge.pinniped.dev" : { "this is allowed" } ,
} ,
} ,
ImpersonatedUser : & authenticationv1 . UserInfo { } ,
} ,
2021-04-20 15:19:58 +00:00
"" ,
2021-04-09 21:52:53 +00:00
) ,
wantKubeAPIServerRequestHeaders : map [ string ] [ ] string {
"Authorization" : { "Bearer some-service-account-token" } ,
"Impersonate-Extra-Original-User-Info.impersonation-Proxyy.concierge.pinniped.dev" : { "log confusion stuff here" } ,
"Impersonate-Extra-Original-User-Info.impersonation-Proxy.concierge.pinniped.dev" : { ` { "username":"panda","uid":"0x001","groups":["bears","friends"],"extra": { "original-user-info.impersonation-proxy.concierge.pinniped.dev":["this is allowed"]}} ` } ,
"Impersonate-Group" : { "system:authenticated" } ,
"Impersonate-User" : { "username@company.com" } ,
"User-Agent" : { "test-user-agent" } ,
"Accept" : { "some-accepted-format" } ,
"Accept-Encoding" : { "some-accepted-encoding" } ,
"Connection" : { "Upgrade" } ,
"Upgrade" : { "some-upgrade" } ,
"Content-Type" : { "some-type" } ,
"Other-Header" : { "test-header-value-1" } ,
} ,
wantHTTPBody : "successful proxied response" ,
wantHTTPStatus : http . StatusOK ,
} ,
{
name : "authenticated user with nested impersonation" ,
2021-04-20 15:19:58 +00:00
request : newRequest ( t , map [ string ] [ ] string {
2021-04-09 21:52:53 +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
} , & user . DefaultInfo {
Name : testUser ,
Groups : testGroups ,
Extra : testExtra ,
} ,
& auditinternal . Event {
User : authenticationv1 . UserInfo {
Username : "panda" ,
UID : "0x001" ,
Groups : [ ] string { "bears" , "friends" } ,
Extra : map [ string ] authenticationv1 . ExtraValue {
"assertion" : { "sha" , "md5" } ,
"req-id" : { "0123" } ,
} ,
} ,
ImpersonatedUser : & authenticationv1 . UserInfo { } ,
} ,
2021-04-20 15:19:58 +00:00
"" ,
2021-04-09 21:52:53 +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-04-09 21:52:53 +00:00
"Impersonate-Extra-Original-User-Info.impersonation-Proxy.concierge.pinniped.dev" : { ` { "username":"panda","uid":"0x001","groups":["bears","friends"],"extra": { "assertion":["sha","md5"],"req-id":["0123"]}} ` } ,
} ,
wantHTTPBody : "successful proxied response" ,
wantHTTPStatus : http . StatusOK ,
} ,
{
name : "authenticated gke user with nested impersonation" ,
2021-04-20 15:19:58 +00:00
request : newRequest ( t , map [ string ] [ ] string {
2021-04-09 21:52:53 +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
} , & user . DefaultInfo {
Name : testUser ,
Groups : testGroups ,
Extra : testExtra ,
} ,
& auditinternal . Event {
User : authenticationv1 . UserInfo {
Username : "username@company.com" ,
Groups : [ ] string { "system:authenticated" } ,
Extra : map [ string ] authenticationv1 . ExtraValue {
// make sure we can handle these keys
"iam.gke.io/user-assertion" : { "ABC" } ,
"user-assertion.cloud.google.com" : { "999" } ,
} ,
} ,
ImpersonatedUser : & authenticationv1 . UserInfo { } ,
} ,
2021-04-20 15:19:58 +00:00
"" ,
2021-04-09 21:52:53 +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" } ,
"Other-Header" : { "test-header-value-1" } ,
"Impersonate-Extra-Original-User-Info.impersonation-Proxy.concierge.pinniped.dev" : { ` { "username":"username@company.com","groups":["system:authenticated"],"extra": { "iam.gke.io/user-assertion":["ABC"],"user-assertion.cloud.google.com":["999"]}} ` } ,
} ,
wantHTTPBody : "successful proxied response" ,
wantHTTPStatus : http . StatusOK ,
} ,
{
name : "authenticated user with nested impersonation of gke user" ,
2021-04-20 15:19:58 +00:00
request : newRequest ( t , map [ string ] [ ] string {
2021-04-09 21:52:53 +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
} , & user . DefaultInfo {
Name : "username@company.com" ,
Groups : [ ] string { "system:authenticated" } ,
Extra : map [ string ] [ ] string {
// make sure we can handle these keys
"iam.gke.io/user-assertion" : { "DEF" } ,
"user-assertion.cloud.google.com" : { "XYZ" } ,
} ,
} ,
& auditinternal . Event {
User : authenticationv1 . UserInfo {
Username : "panda" ,
UID : "0x001" ,
Groups : [ ] string { "bears" , "friends" } ,
Extra : map [ string ] authenticationv1 . ExtraValue {
"assertion" : { "sha" , "md5" } ,
"req-id" : { "0123" } ,
} ,
} ,
ImpersonatedUser : & authenticationv1 . UserInfo { } ,
} ,
2021-04-20 15:19:58 +00:00
"" ,
2021-04-09 21:52:53 +00:00
) ,
wantKubeAPIServerRequestHeaders : map [ string ] [ ] string {
"Authorization" : { "Bearer some-service-account-token" } ,
"Impersonate-Extra-Iam.gke.io%2fuser-Assertion" : { "DEF" } ,
"Impersonate-Extra-User-Assertion.cloud.google.com" : { "XYZ" } ,
"Impersonate-Group" : { "system:authenticated" } ,
"Impersonate-User" : { "username@company.com" } ,
"User-Agent" : { "test-user-agent" } ,
"Accept" : { "some-accepted-format" } ,
"Accept-Encoding" : { "some-accepted-encoding" } ,
"Connection" : { "Upgrade" } ,
"Upgrade" : { "some-upgrade" } ,
"Content-Type" : { "some-type" } ,
"Other-Header" : { "test-header-value-1" } ,
"Impersonate-Extra-Original-User-Info.impersonation-Proxy.concierge.pinniped.dev" : { ` { "username":"panda","uid":"0x001","groups":["bears","friends"],"extra": { "assertion":["sha","md5"],"req-id":["0123"]}} ` } ,
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-04-20 15:19:58 +00:00
request : newRequest ( t , 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-04-20 15:19:58 +00:00
} , nil , "" ) ,
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-04-20 15:19:58 +00:00
serverConfig . Authentication . Authenticator = tt . authenticator
2021-03-14 01:25:23 +00:00
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
2021-04-20 15:19:58 +00:00
func newRequest ( t * testing . T , h http . Header , userInfo user . Info , event * auditinternal . Event , token string ) * http . Request {
t . Helper ( )
validURL , err := url . Parse ( "http://pinniped.dev/blah" )
require . NoError ( t , err )
ctx := context . Background ( )
if userInfo != nil {
ctx = request . WithUser ( ctx , userInfo )
}
ae := & auditinternal . Event { Level : auditinternal . LevelMetadata }
if event != nil {
ae = event
}
ctx = request . WithAuditEvent ( ctx , ae )
reqInfo := & request . RequestInfo {
IsResourceRequest : false ,
Path : validURL . Path ,
Verb : "get" ,
}
ctx = request . WithRequestInfo ( ctx , reqInfo )
ctx = authenticator . WithAudiences ( ctx , authenticator . Audiences { "must-be-ignored" } )
if len ( token ) != 0 {
ctx = context . WithValue ( ctx , tokenKey , token )
}
var cancel context . CancelFunc
ctx , cancel = context . WithDeadline ( ctx , time . Now ( ) . Add ( time . Hour ) )
t . Cleanup ( cancel )
r , err := http . NewRequestWithContext ( ctx , http . MethodGet , validURL . String ( ) , nil )
require . NoError ( t , err )
r . Header = h
return r
}
func testTokenAuthenticator ( t * testing . T , token string , userInfo user . Info , err error ) authenticator . Request {
t . Helper ( )
return authenticator . RequestFunc ( func ( r * http . Request ) ( * authenticator . Response , bool , error ) {
if auds , ok := authenticator . AudiencesFrom ( r . Context ( ) ) ; ok || len ( auds ) != 0 {
t . Errorf ( "unexpected audiences on request: %v" , auds )
}
if ctxToken := tokenFrom ( r . Context ( ) ) ; len ( ctxToken ) != 0 {
t . Errorf ( "unexpected token on request: %v" , ctxToken )
}
if _ , ok := r . Context ( ) . Deadline ( ) ; ! ok {
t . Error ( "request should always have deadline" )
}
if err != nil {
return nil , false , err
}
var reqToken string
_ , _ , _ = bearertoken . New ( authenticator . TokenFunc ( func ( _ context . Context , token string ) ( * authenticator . Response , bool , error ) {
reqToken = token
return nil , false , nil
} ) ) . AuthenticateRequest ( r )
if reqToken != token {
return nil , false , nil
}
return & authenticator . Response { User : userInfo } , true , nil
} )
}
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-04-09 21:52:53 +00:00
t . Helper ( )
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 ) {
2021-04-09 21:52:53 +00:00
t . Helper ( )
2021-03-12 01:24:52 +00:00
ln , _ , listenErr := genericoptions . CreateListener ( "" , "0.0.0.0:" + strconv . Itoa ( port ) , net . ListenConfig { } )
require . NoError ( t , listenErr )
require . NoError ( t , ln . Close ( ) )
}
2021-04-09 21:52:53 +00:00
func Test_deleteKnownImpersonationHeaders ( t * testing . T ) {
tests := [ ] struct {
name string
headers , want http . Header
} {
{
name : "no impersonation" ,
headers : map [ string ] [ ] string {
"a" : { "b" } ,
"Accept-Encoding" : { "gzip" } ,
"User-Agent" : { "test-user-agent" } ,
} ,
want : map [ string ] [ ] string {
"a" : { "b" } ,
"Accept-Encoding" : { "gzip" } ,
"User-Agent" : { "test-user-agent" } ,
} ,
} ,
{
name : "impersonate user header is dropped" ,
headers : map [ string ] [ ] string {
"a" : { "b" } ,
"Impersonate-User" : { "panda" } ,
"Accept-Encoding" : { "gzip" } ,
"User-Agent" : { "test-user-agent" } ,
} ,
want : map [ string ] [ ] string {
"a" : { "b" } ,
"Accept-Encoding" : { "gzip" } ,
"User-Agent" : { "test-user-agent" } ,
} ,
} ,
{
name : "all known impersonate headers are dropped" ,
headers : map [ string ] [ ] string {
"Accept-Encoding" : { "gzip" } ,
"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" } ,
} ,
want : map [ string ] [ ] string {
"Accept-Encoding" : { "gzip" } ,
"Authorization" : { "Bearer some-service-account-token" } ,
"User-Agent" : { "test-user-agent" } ,
} ,
} ,
{
name : "future UID header is not dropped" ,
headers : map [ string ] [ ] string {
"Accept-Encoding" : { "gzip" } ,
"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" } ,
"Impersonate-Uid" : { "008" } ,
"User-Agent" : { "test-user-agent" } ,
} ,
want : map [ string ] [ ] string {
"Accept-Encoding" : { "gzip" } ,
"Authorization" : { "Bearer some-service-account-token" } ,
"User-Agent" : { "test-user-agent" } ,
"Impersonate-Uid" : { "008" } ,
} ,
} ,
{
name : "future UID header is not dropped, no other headers" ,
headers : map [ string ] [ ] string {
"Impersonate-Uid" : { "009" } ,
} ,
want : map [ string ] [ ] string {
"Impersonate-Uid" : { "009" } ,
} ,
} ,
}
for _ , tt := range tests {
tt := tt
t . Run ( tt . name , func ( t * testing . T ) {
inputReq := ( & http . Request { Header : tt . headers } ) . WithContext ( context . Background ( ) )
inputReqCopy := inputReq . Clone ( inputReq . Context ( ) )
2021-04-20 15:19:58 +00:00
var called bool
2021-04-09 21:52:53 +00:00
delegate := http . HandlerFunc ( func ( w http . ResponseWriter , outputReq * http . Request ) {
2021-04-20 15:19:58 +00:00
called = true
2021-04-09 21:52:53 +00:00
require . Nil ( t , w )
// assert only headers mutated
outputReqCopy := outputReq . Clone ( outputReq . Context ( ) )
outputReqCopy . Header = tt . headers
require . Equal ( t , inputReqCopy , outputReqCopy )
require . Equal ( t , tt . want , outputReq . Header )
if ensureNoImpersonationHeaders ( inputReq ) == nil {
require . True ( t , inputReq == outputReq , "expect req to passed through when no modification needed" )
}
} )
deleteKnownImpersonationHeaders ( delegate ) . ServeHTTP ( nil , inputReq )
require . Equal ( t , inputReqCopy , inputReq ) // assert no mutation occurred
2021-04-20 15:19:58 +00:00
require . True ( t , called )
} )
}
}
func Test_withBearerTokenPreservation ( t * testing . T ) {
tests := [ ] struct {
name string
headers http . Header
want string
} {
{
name : "has bearer token" ,
headers : map [ string ] [ ] string {
"Authorization" : { "Bearer thingy" } ,
} ,
want : "thingy" ,
} ,
{
name : "has bearer token but too many preceding spaces" ,
headers : map [ string ] [ ] string {
"Authorization" : { "Bearer 1" } ,
} ,
want : "" ,
} ,
{
name : "has bearer token with space, only keeps first part" ,
headers : map [ string ] [ ] string {
"Authorization" : { "Bearer panda man" } ,
} ,
want : "panda" ,
} ,
{
name : "has bearer token with surrounding whitespace" ,
headers : map [ string ] [ ] string {
"Authorization" : { " Bearer cool beans " } ,
} ,
want : "cool" ,
} ,
{
name : "has multiple bearer tokens" ,
headers : map [ string ] [ ] string {
"Authorization" : { "Bearer this thing" , "what does this mean?" } ,
} ,
want : "this" ,
} ,
{
name : "no bearer token" ,
headers : map [ string ] [ ] string {
"Not-Authorization" : { "Bearer not a token" } ,
} ,
want : "" ,
} ,
}
for _ , tt := range tests {
tt := tt
t . Run ( tt . name , func ( t * testing . T ) {
inputReq := ( & http . Request { Header : tt . headers } ) . WithContext ( context . Background ( ) )
inputReqCopy := inputReq . Clone ( inputReq . Context ( ) )
var called bool
delegate := http . HandlerFunc ( func ( w http . ResponseWriter , outputReq * http . Request ) {
called = true
require . Nil ( t , w )
// assert only context is mutated
outputReqCopy := outputReq . Clone ( inputReq . Context ( ) )
require . Equal ( t , inputReqCopy , outputReqCopy )
require . Equal ( t , tt . want , tokenFrom ( outputReq . Context ( ) ) )
if len ( tt . want ) == 0 {
require . True ( t , inputReq == outputReq , "expect req to passed through when no token expected" )
}
} )
withBearerTokenPreservation ( delegate ) . ServeHTTP ( nil , inputReq )
require . Equal ( t , inputReqCopy , inputReq ) // assert no mutation occurred
require . True ( t , called )
2021-04-09 21:52:53 +00:00
} )
}
}