Add a cache of active IDPs, which implements authenticator.Token.

Signed-off-by: Matt Moyer <moyerm@vmware.com>
This commit is contained in:
Matt Moyer 2020-09-14 09:36:06 -05:00
parent 66f4e62c6c
commit 6506a82b19
No known key found for this signature in database
GPG Key ID: EAE88AD172C5AE2D
2 changed files with 178 additions and 0 deletions

View File

@ -0,0 +1,77 @@
/*
Copyright 2020 VMware, Inc.
SPDX-License-Identifier: Apache-2.0
*/
// Package idpcache implements a cache of active identity providers.
package idpcache
import (
"context"
"fmt"
"sync"
"k8s.io/apiserver/pkg/authentication/authenticator"
"github.com/suzerain-io/pinniped/internal/controllerlib"
)
var (
// ErrNoIDPs is returned by Cache.AuthenticateToken() when there are no IDPs configured.
ErrNoIDPs = fmt.Errorf("no identity providers are loaded")
// ErrIndeterminateIDP is returned by Cache.AuthenticateToken() when the correct IDP cannot be determined.
ErrIndeterminateIDP = fmt.Errorf("could not uniquely match against an identity provider")
)
// Cache implements the authenticator.Token interface by multiplexing across a dynamic set of identity providers
// loaded from IDP resources.
type Cache struct {
cache sync.Map
}
// New returns an empty cache.
func New() *Cache {
return &Cache{}
}
// Store an identity provider into the cache.
func (c *Cache) Store(key controllerlib.Key, value authenticator.Token) {
c.cache.Store(key, value)
}
// Delete an identity provider from the cache.
func (c *Cache) Delete(key controllerlib.Key) {
c.cache.Delete(key)
}
// Keys currently stored in the cache.
func (c *Cache) Keys() []controllerlib.Key {
var result []controllerlib.Key
c.cache.Range(func(key, _ interface{}) bool {
result = append(result, key.(controllerlib.Key))
return true
})
return result
}
// AuthenticateToken validates the provided token against the currently loaded identity providers.
func (c *Cache) AuthenticateToken(ctx context.Context, token string) (*authenticator.Response, bool, error) {
var matchingIDPs []authenticator.Token
c.cache.Range(func(key, value interface{}) bool {
matchingIDPs = append(matchingIDPs, value.(authenticator.Token))
return true
})
// Return an error if there are no known IDPs.
if len(matchingIDPs) == 0 {
return nil, false, ErrNoIDPs
}
// For now, allow there to be only exactly one IDP (until we specify a good mechanism for selecting one).
if len(matchingIDPs) != 1 {
return nil, false, ErrIndeterminateIDP
}
return matchingIDPs[0].AuthenticateToken(ctx, token)
}

View File

@ -0,0 +1,101 @@
/*
Copyright 2020 VMware, Inc.
SPDX-License-Identifier: Apache-2.0
*/
package idpcache
import (
"context"
"testing"
"github.com/golang/mock/gomock"
"github.com/stretchr/testify/require"
"k8s.io/apiserver/pkg/authentication/authenticator"
"k8s.io/apiserver/pkg/authentication/user"
"github.com/suzerain-io/pinniped/internal/controllerlib"
"github.com/suzerain-io/pinniped/internal/mocks/mocktokenauthenticator"
)
func TestCache(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
tests := []struct {
name string
mockAuthenticators map[controllerlib.Key]func(*mocktokenauthenticator.MockToken)
wantResponse *authenticator.Response
wantAuthenticated bool
wantErr string
}{
{
name: "no IDPs",
wantErr: "no identity providers are loaded",
},
{
name: "multiple IDPs",
mockAuthenticators: map[controllerlib.Key]func(mockToken *mocktokenauthenticator.MockToken){
controllerlib.Key{Namespace: "foo", Name: "idp-one"}: nil,
controllerlib.Key{Namespace: "foo", Name: "idp-two"}: nil,
},
wantErr: "could not uniquely match against an identity provider",
},
{
name: "success",
mockAuthenticators: map[controllerlib.Key]func(mockToken *mocktokenauthenticator.MockToken){
controllerlib.Key{
Namespace: "foo",
Name: "idp-one",
}: func(mockToken *mocktokenauthenticator.MockToken) {
mockToken.EXPECT().AuthenticateToken(ctx, "test-token").Return(
&authenticator.Response{User: &user.DefaultInfo{Name: "test-user"}},
true,
nil,
)
},
},
wantResponse: &authenticator.Response{User: &user.DefaultInfo{Name: "test-user"}},
wantAuthenticated: true,
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
ctrl := gomock.NewController(t)
defer ctrl.Finish()
cache := New()
require.NotNil(t, cache)
require.Implements(t, (*authenticator.Token)(nil), cache)
for key, mockFunc := range tt.mockAuthenticators {
mockToken := mocktokenauthenticator.NewMockToken(ctrl)
if mockFunc != nil {
mockFunc(mockToken)
}
cache.Store(key, mockToken)
}
require.Equal(t, len(tt.mockAuthenticators), len(cache.Keys()))
resp, authenticated, err := cache.AuthenticateToken(ctx, "test-token")
require.Equal(t, tt.wantResponse, resp)
require.Equal(t, tt.wantAuthenticated, authenticated)
if tt.wantErr != "" {
require.EqualError(t, err, tt.wantErr)
return
}
require.NoError(t, err)
for _, key := range cache.Keys() {
cache.Delete(key)
}
require.Zero(t, len(cache.Keys()))
})
}
}