2020-09-16 14:19:51 +00:00
|
|
|
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
|
|
|
// SPDX-License-Identifier: Apache-2.0
|
2020-09-14 14:36:06 +00:00
|
|
|
|
|
|
|
package idpcache
|
|
|
|
|
|
|
|
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"
|
|
|
|
|
2020-10-30 14:34:43 +00:00
|
|
|
loginapi "go.pinniped.dev/generated/1.19/apis/concierge/login"
|
2020-09-21 16:37:54 +00:00
|
|
|
idpv1alpha "go.pinniped.dev/generated/1.19/apis/idp/v1alpha1"
|
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()
|
|
|
|
|
|
|
|
cache := New()
|
|
|
|
require.NotNil(t, cache)
|
|
|
|
|
|
|
|
key1 := Key{Namespace: "foo", Name: "idp-one"}
|
|
|
|
mockToken1 := mocktokenauthenticator.NewMockToken(ctrl)
|
|
|
|
cache.Store(key1, mockToken1)
|
|
|
|
require.Equal(t, mockToken1, cache.Get(key1))
|
|
|
|
require.Equal(t, 1, len(cache.Keys()))
|
|
|
|
|
|
|
|
key2 := Key{Namespace: "foo", Name: "idp-two"}
|
|
|
|
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++ {
|
|
|
|
cache := New()
|
|
|
|
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{
|
|
|
|
IdentityProvider: corev1.TypedLocalObjectReference{
|
|
|
|
APIGroup: &idpv1alpha.SchemeGroupVersion.Group,
|
|
|
|
Kind: "WebhookIdentityProvider",
|
|
|
|
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{
|
|
|
|
APIGroup: *validRequest.Spec.IdentityProvider.APIGroup,
|
|
|
|
Kind: validRequest.Spec.IdentityProvider.Kind,
|
|
|
|
Namespace: validRequest.Namespace,
|
|
|
|
Name: validRequest.Spec.IdentityProvider.Name,
|
2020-09-14 14:36:06 +00:00
|
|
|
}
|
|
|
|
|
2020-09-21 16:37:54 +00:00
|
|
|
mockCache := func(t *testing.T, res *authenticator.Response, authenticated bool, err error) *Cache {
|
|
|
|
ctrl := gomock.NewController(t)
|
|
|
|
t.Cleanup(ctrl.Finish)
|
|
|
|
m := mocktokenauthenticator.NewMockToken(ctrl)
|
|
|
|
m.EXPECT().AuthenticateToken(audienceFreeContext{}, validRequest.Spec.Token).Return(res, authenticated, err)
|
|
|
|
c := New()
|
|
|
|
c.Store(validRequestKey, m)
|
|
|
|
return c
|
|
|
|
}
|
|
|
|
|
|
|
|
t.Run("no such IDP", func(t *testing.T) {
|
|
|
|
c := New()
|
|
|
|
res, err := c.AuthenticateTokenCredentialRequest(context.Background(), validRequest.DeepCopy())
|
|
|
|
require.EqualError(t, err, "no such identity provider")
|
|
|
|
require.Nil(t, res)
|
|
|
|
})
|
|
|
|
|
|
|
|
t.Run("authenticator returns error", func(t *testing.T) {
|
|
|
|
c := mockCache(t, nil, false, fmt.Errorf("some authenticator error"))
|
|
|
|
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) {
|
|
|
|
c := mockCache(t, &authenticator.Response{}, false, nil)
|
|
|
|
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) {
|
|
|
|
c := mockCache(t, nil, true, nil)
|
|
|
|
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) {
|
|
|
|
c := mockCache(t, &authenticator.Response{}, true, nil)
|
|
|
|
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
|
|
|
},
|
|
|
|
)
|
|
|
|
c := New()
|
|
|
|
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"}},
|
|
|
|
}
|
|
|
|
c := mockCache(t, &authenticator.Response{User: &userInfo}, true, nil)
|
|
|
|
|
|
|
|
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())
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
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
|
|
|
}
|