ContainerImage.Pinniped/internal/controller/authenticator/jwtcachefiller/jwtcachefiller.go
2021-01-07 15:25:47 -08:00

198 lines
6.6 KiB
Go

// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
// Package jwtcachefiller implements a controller for filling an authncache.Cache with each
// added/updated JWTAuthenticator.
package jwtcachefiller
import (
"fmt"
"io/ioutil"
"os"
"reflect"
"github.com/go-logr/logr"
"gopkg.in/square/go-jose.v2"
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apiserver/pkg/authentication/authenticator"
"k8s.io/apiserver/plugin/pkg/authenticator/token/oidc"
"k8s.io/klog/v2"
auth1alpha1 "go.pinniped.dev/generated/1.20/apis/concierge/authentication/v1alpha1"
authinformers "go.pinniped.dev/generated/1.20/client/concierge/informers/externalversions/authentication/v1alpha1"
pinnipedcontroller "go.pinniped.dev/internal/controller"
pinnipedauthenticator "go.pinniped.dev/internal/controller/authenticator"
"go.pinniped.dev/internal/controller/authenticator/authncache"
"go.pinniped.dev/internal/controllerlib"
)
// These default values come from the way that the Supervisor issues and signs tokens. We make these
// the defaults for a JWTAuthenticator so that they can easily integrate with the Supervisor.
const (
defaultUsernameClaim = "username"
defaultGroupsClaim = "groups"
)
// defaultSupportedSigningAlgos returns the default signing algos that this JWTAuthenticator
// supports (i.e., if none are supplied by the user).
func defaultSupportedSigningAlgos() []string {
return []string{
// RS256 is recommended by the OIDC spec and required, in some capacity. Since we want the
// JWTAuthenticator to be able to support many OIDC ID tokens out of the box, we include this
// algorithm by default.
string(jose.RS256),
// ES256 is what the Supervisor does, by default. We want integration with the JWTAuthenticator
// to be as seamless as possible, so we include this algorithm by default.
string(jose.ES256),
}
}
type tokenAuthenticatorCloser interface {
authenticator.Token
pinnipedauthenticator.Closer
}
type jwtAuthenticator struct {
tokenAuthenticatorCloser
spec *auth1alpha1.JWTAuthenticatorSpec
}
// New instantiates a new controllerlib.Controller which will populate the provided authncache.Cache.
func New(
cache *authncache.Cache,
jwtAuthenticators authinformers.JWTAuthenticatorInformer,
log logr.Logger,
) controllerlib.Controller {
return controllerlib.New(
controllerlib.Config{
Name: "jwtcachefiller-controller",
Syncer: &controller{
cache: cache,
jwtAuthenticators: jwtAuthenticators,
log: log.WithName("jwtcachefiller-controller"),
},
},
controllerlib.WithInformer(
jwtAuthenticators,
pinnipedcontroller.MatchAnythingFilter(nil), // nil parent func is fine because each event is distinct
controllerlib.InformerOption{},
),
)
}
type controller struct {
cache *authncache.Cache
jwtAuthenticators authinformers.JWTAuthenticatorInformer
log logr.Logger
}
// Sync implements controllerlib.Syncer.
func (c *controller) Sync(ctx controllerlib.Context) error {
obj, err := c.jwtAuthenticators.Lister().JWTAuthenticators(ctx.Key.Namespace).Get(ctx.Key.Name)
if err != nil && errors.IsNotFound(err) {
c.log.Info("Sync() found that the JWTAuthenticator does not exist yet or was deleted")
return nil
}
if err != nil {
return fmt.Errorf("failed to get JWTAuthenticator %s/%s: %w", ctx.Key.Namespace, ctx.Key.Name, err)
}
cacheKey := authncache.Key{
APIGroup: auth1alpha1.GroupName,
Kind: "JWTAuthenticator",
Namespace: ctx.Key.Namespace,
Name: ctx.Key.Name,
}
// If this authenticator already exists, then only recreate it if is different from the desired
// authenticator. We don't want to be creating a new authenticator for every resync period.
//
// If we do need to recreate the authenticator, then make sure we close the old one to avoid
// goroutine leaks.
if value := c.cache.Get(cacheKey); value != nil {
jwtAuthenticator := c.extractValueAsJWTAuthenticator(value)
if jwtAuthenticator != nil {
if reflect.DeepEqual(jwtAuthenticator.spec, &obj.Spec) {
c.log.WithValues("jwtAuthenticator", klog.KObj(obj), "issuer", obj.Spec.Issuer).Info("actual jwt authenticator and desired jwt authenticator are the same")
return nil
}
jwtAuthenticator.Close()
}
}
// Make a deep copy of the spec so we aren't storing pointers to something that the informer cache
// may mutate!
jwtAuthenticator, err := newJWTAuthenticator(obj.Spec.DeepCopy())
if err != nil {
return fmt.Errorf("failed to build jwt authenticator: %w", err)
}
c.cache.Store(cacheKey, jwtAuthenticator)
c.log.WithValues("jwtAuthenticator", klog.KObj(obj), "issuer", obj.Spec.Issuer).Info("added new jwt authenticator")
return nil
}
func (c *controller) extractValueAsJWTAuthenticator(value authncache.Value) *jwtAuthenticator {
jwtAuthenticator, ok := value.(*jwtAuthenticator)
if !ok {
actualType := "<nil>"
if t := reflect.TypeOf(value); t != nil {
actualType = t.String()
}
c.log.WithValues("actualType", actualType).Info("wrong JWT authenticator type in cache")
return nil
}
return jwtAuthenticator
}
// newJWTAuthenticator creates a jwt authenticator from the provided spec.
func newJWTAuthenticator(spec *auth1alpha1.JWTAuthenticatorSpec) (*jwtAuthenticator, error) {
caBundle, err := pinnipedauthenticator.CABundle(spec.TLS)
if err != nil {
return nil, fmt.Errorf("invalid TLS configuration: %w", err)
}
var caFile string
if caBundle != nil {
temp, err := ioutil.TempFile("", "pinniped-jwkauthenticator-cafile-*")
if err != nil {
return nil, fmt.Errorf("unable to create temporary file: %w", err)
}
// We can safely remove the temp file at the end of this function since oidc.New() reads the
// provided CA file and then forgets about it.
defer func() { _ = os.Remove(temp.Name()) }()
if _, err := temp.Write(caBundle); err != nil {
return nil, fmt.Errorf("cannot write CA file: %w", err)
}
caFile = temp.Name()
}
usernameClaim := spec.Claims.Username
if usernameClaim == "" {
usernameClaim = defaultUsernameClaim
}
groupsClaim := spec.Claims.Groups
if groupsClaim == "" {
groupsClaim = defaultGroupsClaim
}
authenticator, err := oidc.New(oidc.Options{
IssuerURL: spec.Issuer,
ClientID: spec.Audience,
UsernameClaim: usernameClaim,
GroupsClaim: groupsClaim,
SupportedSigningAlgs: defaultSupportedSigningAlgos(),
CAFile: caFile,
})
if err != nil {
return nil, fmt.Errorf("could not initialize authenticator: %w", err)
}
return &jwtAuthenticator{
tokenAuthenticatorCloser: authenticator,
spec: spec,
}, nil
}