ContainerImage.Pinniped/internal/controller/identityprovider/webhookcachefiller/webhookcachefiller.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

131 lines
4.5 KiB
Go

// Copyright 2020 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
// Package webhookcachefiller implements a controller for filling an idpcache.Cache with each added/updated WebhookIdentityProvider.
package webhookcachefiller
import (
"encoding/base64"
"fmt"
"io/ioutil"
"os"
"github.com/go-logr/logr"
k8sauthv1beta1 "k8s.io/api/authentication/v1beta1"
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/util/net"
"k8s.io/apiserver/pkg/authentication/authenticator"
"k8s.io/apiserver/plugin/pkg/authenticator/token/webhook"
"k8s.io/client-go/tools/clientcmd"
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
"k8s.io/klog/v2"
idpv1alpha1 "go.pinniped.dev/generated/1.19/apis/idp/v1alpha1"
idpinformers "go.pinniped.dev/generated/1.19/client/informers/externalversions/idp/v1alpha1"
pinnipedcontroller "go.pinniped.dev/internal/controller"
"go.pinniped.dev/internal/controller/identityprovider/idpcache"
"go.pinniped.dev/internal/controllerlib"
)
// New instantiates a new controllerlib.Controller which will populate the provided idpcache.Cache.
func New(cache *idpcache.Cache, webhookIDPs idpinformers.WebhookIdentityProviderInformer, log logr.Logger) controllerlib.Controller {
return controllerlib.New(
controllerlib.Config{
Name: "webhookcachefiller-controller",
Syncer: &controller{
cache: cache,
webhookIDPs: webhookIDPs,
log: log.WithName("webhookcachefiller-controller"),
},
},
controllerlib.WithInformer(
webhookIDPs,
pinnipedcontroller.NoOpFilter(),
controllerlib.InformerOption{},
),
)
}
type controller struct {
cache *idpcache.Cache
webhookIDPs idpinformers.WebhookIdentityProviderInformer
log logr.Logger
}
// Sync implements controllerlib.Syncer.
func (c *controller) Sync(ctx controllerlib.Context) error {
obj, err := c.webhookIDPs.Lister().WebhookIdentityProviders(ctx.Key.Namespace).Get(ctx.Key.Name)
if err != nil && errors.IsNotFound(err) {
c.log.Info("Sync() found that the WebhookIdentityProvider does not exist yet or was deleted")
return nil
}
if err != nil {
return fmt.Errorf("failed to get WebhookIdentityProvider %s/%s: %w", ctx.Key.Namespace, ctx.Key.Name, err)
}
webhookAuthenticator, err := newWebhookAuthenticator(&obj.Spec, ioutil.TempFile, clientcmd.WriteToFile)
if err != nil {
return fmt.Errorf("failed to build webhook config: %w", err)
}
c.cache.Store(idpcache.Key{
APIGroup: idpv1alpha1.GroupName,
Kind: "WebhookIdentityProvider",
Namespace: ctx.Key.Namespace,
Name: ctx.Key.Name,
}, webhookAuthenticator)
c.log.WithValues("idp", klog.KObj(obj), "endpoint", obj.Spec.Endpoint).Info("added new webhook IDP")
return nil
}
// newWebhookAuthenticator creates a webhook from the provided API server url and caBundle
// used to validate TLS connections.
func newWebhookAuthenticator(
spec *idpv1alpha1.WebhookIdentityProviderSpec,
tempfileFunc func(string, string) (*os.File, error),
marshalFunc func(clientcmdapi.Config, string) error,
) (*webhook.WebhookTokenAuthenticator, error) {
temp, err := tempfileFunc("", "pinniped-webhook-kubeconfig-*")
if err != nil {
return nil, fmt.Errorf("unable to create temporary file: %w", err)
}
defer func() { _ = os.Remove(temp.Name()) }()
cluster := &clientcmdapi.Cluster{Server: spec.Endpoint}
cluster.CertificateAuthorityData, err = getCABundle(spec.TLS)
if err != nil {
return nil, fmt.Errorf("invalid TLS configuration: %w", err)
}
kubeconfig := clientcmdapi.NewConfig()
kubeconfig.Clusters["anonymous-cluster"] = cluster
kubeconfig.Contexts["anonymous"] = &clientcmdapi.Context{Cluster: "anonymous-cluster"}
kubeconfig.CurrentContext = "anonymous"
if err := marshalFunc(*kubeconfig, temp.Name()); err != nil {
return nil, fmt.Errorf("unable to marshal kubeconfig: %w", err)
}
// We use v1beta1 instead of v1 since v1beta1 is more prevalent in our desired
// integration points.
version := k8sauthv1beta1.SchemeGroupVersion.Version
// At the current time, we don't provide any audiences because we simply don't
// have any requirements to do so. This can be changed in the future as
// requirements change.
var implicitAuds authenticator.Audiences
// We set this to nil because we would only need this to support some of the
// custom proxy stuff used by the API server.
var customDial net.DialFunc
return webhook.New(temp.Name(), version, implicitAuds, customDial)
}
func getCABundle(spec *idpv1alpha1.TLSSpec) ([]byte, error) {
if spec == nil {
return nil, nil
}
return base64.StdEncoding.DecodeString(spec.CertificateAuthorityData)
}