From 6506a82b197d7b7dba88766af4842dd293d1f1c2 Mon Sep 17 00:00:00 2001 From: Matt Moyer Date: Mon, 14 Sep 2020 09:36:06 -0500 Subject: [PATCH] Add a cache of active IDPs, which implements authenticator.Token. Signed-off-by: Matt Moyer --- .../identityprovider/idpcache/cache.go | 77 +++++++++++++ .../identityprovider/idpcache/cache_test.go | 101 ++++++++++++++++++ 2 files changed, 178 insertions(+) create mode 100644 internal/controller/identityprovider/idpcache/cache.go create mode 100644 internal/controller/identityprovider/idpcache/cache_test.go diff --git a/internal/controller/identityprovider/idpcache/cache.go b/internal/controller/identityprovider/idpcache/cache.go new file mode 100644 index 000000000..0fddc2114 --- /dev/null +++ b/internal/controller/identityprovider/idpcache/cache.go @@ -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) +} diff --git a/internal/controller/identityprovider/idpcache/cache_test.go b/internal/controller/identityprovider/idpcache/cache_test.go new file mode 100644 index 000000000..48b971b34 --- /dev/null +++ b/internal/controller/identityprovider/idpcache/cache_test.go @@ -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())) + }) + } +}