2021-01-07 22:58:09 +00:00
// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved.
2020-09-16 14:19:51 +00:00
// SPDX-License-Identifier: Apache-2.0
2020-09-14 14:36:06 +00:00
2020-10-30 19:02:21 +00:00
package authncache
2020-09-14 14:36:06 +00:00
import (
"context"
2020-09-21 16:37:54 +00:00
"fmt"
2020-09-22 14:50:34 +00:00
"math/rand"
2020-09-14 14:36:06 +00:00
"testing"
2020-09-21 16:37:54 +00:00
"time"
2020-09-14 14:36:06 +00:00
"github.com/golang/mock/gomock"
"github.com/stretchr/testify/require"
2020-09-21 16:37:54 +00:00
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2020-09-14 14:36:06 +00:00
"k8s.io/apiserver/pkg/authentication/authenticator"
"k8s.io/apiserver/pkg/authentication/user"
2021-01-07 22:58:09 +00:00
authv1alpha "go.pinniped.dev/generated/1.20/apis/concierge/authentication/v1alpha1"
loginapi "go.pinniped.dev/generated/1.20/apis/concierge/login"
2020-09-18 19:56:24 +00:00
"go.pinniped.dev/internal/mocks/mocktokenauthenticator"
2020-09-14 14:36:06 +00:00
)
func TestCache ( t * testing . T ) {
t . Parallel ( )
2020-09-21 16:37:54 +00:00
ctrl := gomock . NewController ( t )
defer ctrl . Finish ( )
2021-02-03 23:49:15 +00:00
cache := New ( "pinniped.dev" )
2020-09-21 16:37:54 +00:00
require . NotNil ( t , cache )
2020-10-30 19:02:21 +00:00
key1 := Key { Namespace : "foo" , Name : "authenticator-one" }
2020-09-21 16:37:54 +00:00
mockToken1 := mocktokenauthenticator . NewMockToken ( ctrl )
cache . Store ( key1 , mockToken1 )
require . Equal ( t , mockToken1 , cache . Get ( key1 ) )
require . Equal ( t , 1 , len ( cache . Keys ( ) ) )
2020-10-30 19:02:21 +00:00
key2 := Key { Namespace : "foo" , Name : "authenticator-two" }
2020-09-21 16:37:54 +00:00
mockToken2 := mocktokenauthenticator . NewMockToken ( ctrl )
cache . Store ( key2 , mockToken2 )
require . Equal ( t , mockToken2 , cache . Get ( key2 ) )
require . Equal ( t , 2 , len ( cache . Keys ( ) ) )
for _ , key := range cache . Keys ( ) {
cache . Delete ( key )
}
require . Zero ( t , len ( cache . Keys ( ) ) )
2020-09-22 14:50:34 +00:00
// Fill the cache back up with a fixed set of keys, but inserted in shuffled order.
keysInExpectedOrder := [ ] Key {
{ APIGroup : "a" , Kind : "a" , Namespace : "a" , Name : "a" } ,
{ APIGroup : "b" , Kind : "a" , Namespace : "a" , Name : "a" } ,
{ APIGroup : "b" , Kind : "b" , Namespace : "a" , Name : "a" } ,
{ APIGroup : "b" , Kind : "b" , Namespace : "b" , Name : "a" } ,
{ APIGroup : "b" , Kind : "b" , Namespace : "b" , Name : "b" } ,
}
for tries := 0 ; tries < 10 ; tries ++ {
2021-02-03 23:49:15 +00:00
cache := New ( "pinniped.dev" )
2020-09-22 14:50:34 +00:00
for _ , i := range rand . Perm ( len ( keysInExpectedOrder ) ) {
cache . Store ( keysInExpectedOrder [ i ] , nil )
}
// Expect that they come back out in sorted order.
require . Equal ( t , keysInExpectedOrder , cache . Keys ( ) )
}
2020-09-21 16:37:54 +00:00
}
func TestAuthenticateTokenCredentialRequest ( t * testing . T ) {
t . Parallel ( )
validRequest := loginapi . TokenCredentialRequest {
ObjectMeta : metav1 . ObjectMeta {
Namespace : "test-namespace" ,
2020-09-14 14:36:06 +00:00
} ,
2020-09-21 16:37:54 +00:00
Spec : loginapi . TokenCredentialRequestSpec {
2020-10-30 17:41:21 +00:00
Authenticator : corev1 . TypedLocalObjectReference {
2020-10-30 16:03:25 +00:00
APIGroup : & authv1alpha . SchemeGroupVersion . Group ,
2020-10-30 16:39:26 +00:00
Kind : "WebhookAuthenticator" ,
2020-09-21 16:37:54 +00:00
Name : "test-name" ,
2020-09-14 14:36:06 +00:00
} ,
2020-09-21 16:37:54 +00:00
Token : "test-token" ,
2020-09-14 14:36:06 +00:00
} ,
2020-09-21 16:37:54 +00:00
Status : loginapi . TokenCredentialRequestStatus { } ,
}
validRequestKey := Key {
2020-10-30 17:41:21 +00:00
APIGroup : * validRequest . Spec . Authenticator . APIGroup ,
Kind : validRequest . Spec . Authenticator . Kind ,
2020-09-21 16:37:54 +00:00
Namespace : validRequest . Namespace ,
2020-10-30 17:41:21 +00:00
Name : validRequest . Spec . Authenticator . Name ,
2020-09-14 14:36:06 +00:00
}
2021-02-03 23:49:15 +00:00
mockCache := func ( t * testing . T , apiGroupSuffix string , expectAuthenticatorToBeCalled bool , res * authenticator . Response , authenticated bool , err error ) * Cache {
2020-09-21 16:37:54 +00:00
ctrl := gomock . NewController ( t )
t . Cleanup ( ctrl . Finish )
m := mocktokenauthenticator . NewMockToken ( ctrl )
2021-02-03 23:49:15 +00:00
if expectAuthenticatorToBeCalled {
m . EXPECT ( ) . AuthenticateToken ( audienceFreeContext { } , validRequest . Spec . Token ) . Return ( res , authenticated , err )
}
c := New ( apiGroupSuffix )
2020-09-21 16:37:54 +00:00
c . Store ( validRequestKey , m )
return c
}
2020-10-30 19:02:21 +00:00
t . Run ( "no such authenticator" , func ( t * testing . T ) {
2021-02-03 23:49:15 +00:00
c := New ( "pinniped.dev" )
2020-09-21 16:37:54 +00:00
res , err := c . AuthenticateTokenCredentialRequest ( context . Background ( ) , validRequest . DeepCopy ( ) )
2020-10-30 19:02:21 +00:00
require . EqualError ( t , err , "no such authenticator" )
2020-09-21 16:37:54 +00:00
require . Nil ( t , res )
} )
t . Run ( "authenticator returns error" , func ( t * testing . T ) {
2021-02-03 23:49:15 +00:00
c := mockCache ( t , "pinniped.dev" , true , nil , false , fmt . Errorf ( "some authenticator error" ) )
2020-09-21 16:37:54 +00:00
res , err := c . AuthenticateTokenCredentialRequest ( context . Background ( ) , validRequest . DeepCopy ( ) )
require . EqualError ( t , err , "some authenticator error" )
require . Nil ( t , res )
} )
t . Run ( "authenticator returns unauthenticated without error" , func ( t * testing . T ) {
2021-02-03 23:49:15 +00:00
c := mockCache ( t , "pinniped.dev" , true , & authenticator . Response { } , false , nil )
2020-09-21 16:37:54 +00:00
res , err := c . AuthenticateTokenCredentialRequest ( context . Background ( ) , validRequest . DeepCopy ( ) )
require . NoError ( t , err )
require . Nil ( t , res )
} )
t . Run ( "authenticator returns nil response without error" , func ( t * testing . T ) {
2021-02-03 23:49:15 +00:00
c := mockCache ( t , "pinniped.dev" , true , nil , true , nil )
2020-09-21 16:37:54 +00:00
res , err := c . AuthenticateTokenCredentialRequest ( context . Background ( ) , validRequest . DeepCopy ( ) )
require . NoError ( t , err )
require . Nil ( t , res )
} )
2020-09-14 14:36:06 +00:00
2020-09-21 16:37:54 +00:00
t . Run ( "authenticator returns response with nil user" , func ( t * testing . T ) {
2021-02-03 23:49:15 +00:00
c := mockCache ( t , "pinniped.dev" , true , & authenticator . Response { } , true , nil )
2020-09-21 16:37:54 +00:00
res , err := c . AuthenticateTokenCredentialRequest ( context . Background ( ) , validRequest . DeepCopy ( ) )
require . NoError ( t , err )
require . Nil ( t , res )
} )
2020-09-14 14:36:06 +00:00
2020-09-21 16:37:54 +00:00
t . Run ( "context is cancelled" , func ( t * testing . T ) {
ctrl := gomock . NewController ( t )
t . Cleanup ( ctrl . Finish )
m := mocktokenauthenticator . NewMockToken ( ctrl )
m . EXPECT ( ) . AuthenticateToken ( gomock . Any ( ) , validRequest . Spec . Token ) . DoAndReturn (
func ( ctx context . Context , token string ) ( * authenticator . Response , bool , error ) {
select {
case <- time . After ( 2 * time . Second ) :
require . Fail ( t , "expected to be cancelled" )
return nil , true , nil
case <- ctx . Done ( ) :
return nil , false , ctx . Err ( )
2020-09-14 14:36:06 +00:00
}
2020-09-21 16:37:54 +00:00
} ,
)
2021-02-03 23:49:15 +00:00
c := New ( "pinniped.dev" )
2020-09-21 16:37:54 +00:00
c . Store ( validRequestKey , m )
2020-09-14 14:36:06 +00:00
2020-09-21 16:37:54 +00:00
ctx , cancel := context . WithCancel ( context . Background ( ) )
errchan := make ( chan error )
go func ( ) {
_ , err := c . AuthenticateTokenCredentialRequest ( ctx , validRequest . DeepCopy ( ) )
errchan <- err
} ( )
cancel ( )
require . EqualError ( t , <- errchan , "context canceled" )
} )
t . Run ( "authenticator returns success" , func ( t * testing . T ) {
userInfo := user . DefaultInfo {
Name : "test-user" ,
UID : "test-uid" ,
Groups : [ ] string { "test-group-1" , "test-group-2" } ,
Extra : map [ string ] [ ] string { "extra-key-1" : { "extra-value-1" , "extra-value-2" } } ,
}
2021-02-03 23:49:15 +00:00
c := mockCache ( t , "pinniped.dev" , true , & authenticator . Response { User : & userInfo } , true , nil )
2020-09-21 16:37:54 +00:00
audienceCtx := authenticator . WithAudiences ( context . Background ( ) , authenticator . Audiences { "test-audience-1" } )
res , err := c . AuthenticateTokenCredentialRequest ( audienceCtx , validRequest . DeepCopy ( ) )
require . NoError ( t , err )
require . NotNil ( t , res )
require . Equal ( t , "test-user" , res . GetName ( ) )
require . Equal ( t , "test-uid" , res . GetUID ( ) )
require . Equal ( t , [ ] string { "test-group-1" , "test-group-2" } , res . GetGroups ( ) )
require . Equal ( t , map [ string ] [ ] string { "extra-key-1" : { "extra-value-1" , "extra-value-2" } } , res . GetExtra ( ) )
} )
2021-02-03 23:49:15 +00:00
t . Run ( "using a non-default API group suffix still performs the cache lookup using the pinniped.dev suffix" , func ( t * testing . T ) {
authenticationGroupWithCustomSuffix := "authentication.concierge.custom-suffix.com"
validRequestForAlternateAPIGroup := loginapi . TokenCredentialRequest {
ObjectMeta : metav1 . ObjectMeta {
Namespace : "test-namespace" ,
} ,
Spec : loginapi . TokenCredentialRequestSpec {
Authenticator : corev1 . TypedLocalObjectReference {
APIGroup : & authenticationGroupWithCustomSuffix ,
Kind : "WebhookAuthenticator" ,
Name : "test-name" ,
} ,
Token : "test-token" ,
} ,
Status : loginapi . TokenCredentialRequestStatus { } ,
}
userInfo := user . DefaultInfo {
Name : "test-user" ,
UID : "test-uid" ,
Groups : [ ] string { "test-group-1" , "test-group-2" } ,
Extra : map [ string ] [ ] string { "extra-key-1" : { "extra-value-1" , "extra-value-2" } } ,
}
c := mockCache ( t , "custom-suffix.com" , true , & authenticator . Response { User : & userInfo } , true , nil )
audienceCtx := authenticator . WithAudiences ( context . Background ( ) , authenticator . Audiences { "test-audience-1" } )
res , err := c . AuthenticateTokenCredentialRequest ( audienceCtx , validRequestForAlternateAPIGroup . DeepCopy ( ) )
require . NoError ( t , err )
require . NotNil ( t , res )
require . Equal ( t , "test-user" , res . GetName ( ) )
require . Equal ( t , "test-uid" , res . GetUID ( ) )
require . Equal ( t , [ ] string { "test-group-1" , "test-group-2" } , res . GetGroups ( ) )
require . Equal ( t , map [ string ] [ ] string { "extra-key-1" : { "extra-value-1" , "extra-value-2" } } , res . GetExtra ( ) )
} )
t . Run ( "using a non-default API group suffix and the incoming request mentions a different API group, results in no such authenticator" , func ( t * testing . T ) {
c := mockCache ( t , "custom-suffix.com" , false , & authenticator . Response { User : & user . DefaultInfo { Name : "someone" } } , true , nil )
// Note that the validRequest.Spec.Authenticator.APIGroup value uses "pinniped.dev", not "custom-suffix.com"
res , err := c . AuthenticateTokenCredentialRequest ( context . Background ( ) , validRequest . DeepCopy ( ) )
require . EqualError ( t , err , "no such authenticator" )
require . Nil ( t , res )
} )
2020-09-21 16:37:54 +00:00
}
type audienceFreeContext struct { }
func ( audienceFreeContext ) Matches ( in interface { } ) bool {
ctx , isCtx := in . ( context . Context )
if ! isCtx {
return false
2020-09-14 14:36:06 +00:00
}
2020-09-21 16:37:54 +00:00
_ , hasAudiences := authenticator . AudiencesFrom ( ctx )
return ! hasAudiences
}
func ( audienceFreeContext ) String ( ) string {
return "is a context without authenticator audiences"
2020-09-14 14:36:06 +00:00
}