ContainerImage.Pinniped/internal/controller/identityprovider/idpcache/cache.go
Matt Moyer 6cdd4a9506
Add support for multiple IDPs selected using IdentityProvider field.
This also has fallback compatibility support if no IDP is specified and there is exactly one IDP in the cache.

Signed-off-by: Matt Moyer <moyerm@vmware.com>
2020-09-22 10:03:31 -05:00

133 lines
3.3 KiB
Go

// Copyright 2020 the Pinniped contributors. All Rights Reserved.
// 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"
"k8s.io/apiserver/pkg/authentication/user"
loginapi "go.pinniped.dev/generated/1.19/apis/login"
)
var (
// ErrNoSuchIDP is returned by Cache.AuthenticateTokenCredentialRequest() when the requested IDP is not configured.
ErrNoSuchIDP = fmt.Errorf("no such identity provider")
// ErrNoIDPs is returned by Cache.AuthenticateTokenCredentialRequest() when there are no IDPs configured.
ErrNoIDPs = fmt.Errorf("no identity providers are loaded")
// ErrIndeterminateIDP is returned by Cache.AuthenticateTokenCredentialRequest() 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
}
type Key struct {
APIGroup string
Kind string
Namespace string
Name string
}
type Value interface {
authenticator.Token
}
// New returns an empty cache.
func New() *Cache {
return &Cache{}
}
// Get an identity provider by key.
func (c *Cache) Get(key Key) Value {
res, _ := c.cache.Load(key)
if res == nil {
return nil
}
return res.(Value)
}
// Store an identity provider into the cache.
func (c *Cache) Store(key Key, value Value) {
c.cache.Store(key, value)
}
// Delete an identity provider from the cache.
func (c *Cache) Delete(key Key) {
c.cache.Delete(key)
}
// Keys currently stored in the cache.
func (c *Cache) Keys() []Key {
var result []Key
c.cache.Range(func(key, _ interface{}) bool {
result = append(result, key.(Key))
return true
})
return result
}
func (c *Cache) AuthenticateTokenCredentialRequest(ctx context.Context, req *loginapi.TokenCredentialRequest) (user.Info, error) {
// Map the incoming request to a cache key.
key := Key{
Namespace: req.Namespace,
Name: req.Spec.IdentityProvider.Name,
Kind: req.Spec.IdentityProvider.Kind,
}
if req.Spec.IdentityProvider.APIGroup != nil {
key.APIGroup = *req.Spec.IdentityProvider.APIGroup
}
// If the IDP is unspecified (legacy requests), choose the single loaded IDP or fail if there is not exactly
// one IDP configured.
if key.Name == "" || key.Kind == "" || key.APIGroup == "" {
keys := c.Keys()
if len(keys) == 0 {
return nil, ErrNoIDPs
}
if len(keys) > 1 {
return nil, ErrIndeterminateIDP
}
key = keys[0]
}
val := c.Get(key)
if val == nil {
return nil, ErrNoSuchIDP
}
// The incoming context could have an audience. Since we do not want to handle audiences right now, do not pass it
// through directly to the authentication webhook.
ctx = valuelessContext{ctx}
// Call the selected IDP.
resp, authenticated, err := val.AuthenticateToken(ctx, req.Spec.Token)
if err != nil {
return nil, err
}
if !authenticated {
return nil, nil
}
// Return the user.Info from the response (if it is non-nil).
var respUser user.Info
if resp != nil {
respUser = resp.User
}
return respUser, nil
}
type valuelessContext struct{ context.Context }
func (valuelessContext) Value(interface{}) interface{} { return nil }