2021-04-10 01:49:43 +00:00
|
|
|
// Copyright 2021 the Pinniped contributors. All Rights Reserved.
|
|
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
|
|
|
|
package upstreamwatcher
|
|
|
|
|
|
|
|
import (
|
2021-04-12 20:53:21 +00:00
|
|
|
"context"
|
2021-04-14 00:16:57 +00:00
|
|
|
"crypto/x509"
|
|
|
|
"encoding/base64"
|
2021-04-10 01:49:43 +00:00
|
|
|
"fmt"
|
2021-04-15 21:44:43 +00:00
|
|
|
"time"
|
2021-04-10 01:49:43 +00:00
|
|
|
|
|
|
|
corev1 "k8s.io/api/core/v1"
|
2021-04-12 20:53:21 +00:00
|
|
|
"k8s.io/apimachinery/pkg/api/equality"
|
|
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
2021-04-10 01:49:43 +00:00
|
|
|
"k8s.io/apimachinery/pkg/labels"
|
|
|
|
corev1informers "k8s.io/client-go/informers/core/v1"
|
2021-04-12 20:53:21 +00:00
|
|
|
"k8s.io/klog/v2/klogr"
|
2021-04-10 01:49:43 +00:00
|
|
|
|
|
|
|
"go.pinniped.dev/generated/latest/apis/supervisor/idp/v1alpha1"
|
|
|
|
pinnipedclientset "go.pinniped.dev/generated/latest/client/supervisor/clientset/versioned"
|
|
|
|
idpinformers "go.pinniped.dev/generated/latest/client/supervisor/informers/externalversions/idp/v1alpha1"
|
|
|
|
pinnipedcontroller "go.pinniped.dev/internal/controller"
|
|
|
|
"go.pinniped.dev/internal/controllerlib"
|
|
|
|
"go.pinniped.dev/internal/oidc/provider"
|
|
|
|
"go.pinniped.dev/internal/upstreamldap"
|
|
|
|
)
|
|
|
|
|
|
|
|
const (
|
|
|
|
ldapControllerName = "ldap-upstream-observer"
|
|
|
|
ldapBindAccountSecretType = corev1.SecretTypeBasicAuth
|
2021-04-12 20:53:21 +00:00
|
|
|
|
|
|
|
// Constants related to conditions.
|
2021-04-14 00:16:57 +00:00
|
|
|
typeBindSecretValid = "BindSecretValid"
|
2021-04-15 21:44:43 +00:00
|
|
|
typeTLSConfigurationValid = "TLSConfigurationValid"
|
|
|
|
typeLDAPConnectionValid = "LDAPConnectionValid"
|
|
|
|
reasonLDAPConnectionError = "LDAPConnectionError"
|
2021-04-14 00:16:57 +00:00
|
|
|
noTLSConfigurationMessage = "no TLS configuration provided"
|
|
|
|
loadedTLSConfigurationMessage = "loaded TLS configuration"
|
2021-04-10 01:49:43 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
// UpstreamLDAPIdentityProviderICache is a thread safe cache that holds a list of validated upstream LDAP IDP configurations.
|
|
|
|
type UpstreamLDAPIdentityProviderICache interface {
|
|
|
|
SetLDAPIdentityProviders([]provider.UpstreamLDAPIdentityProviderI)
|
|
|
|
}
|
|
|
|
|
|
|
|
type ldapWatcherController struct {
|
|
|
|
cache UpstreamLDAPIdentityProviderICache
|
2021-04-12 18:23:08 +00:00
|
|
|
ldapDialer upstreamldap.LDAPDialer
|
2021-04-10 01:49:43 +00:00
|
|
|
client pinnipedclientset.Interface
|
|
|
|
ldapIdentityProviderInformer idpinformers.LDAPIdentityProviderInformer
|
|
|
|
secretInformer corev1informers.SecretInformer
|
|
|
|
}
|
|
|
|
|
|
|
|
// NewLDAPUpstreamWatcherController instantiates a new controllerlib.Controller which will populate the provided UpstreamLDAPIdentityProviderICache.
|
|
|
|
func NewLDAPUpstreamWatcherController(
|
|
|
|
idpCache UpstreamLDAPIdentityProviderICache,
|
2021-04-12 18:23:08 +00:00
|
|
|
ldapDialer upstreamldap.LDAPDialer,
|
2021-04-10 01:49:43 +00:00
|
|
|
client pinnipedclientset.Interface,
|
|
|
|
ldapIdentityProviderInformer idpinformers.LDAPIdentityProviderInformer,
|
|
|
|
secretInformer corev1informers.SecretInformer,
|
|
|
|
withInformer pinnipedcontroller.WithInformerOptionFunc,
|
|
|
|
) controllerlib.Controller {
|
|
|
|
c := ldapWatcherController{
|
|
|
|
cache: idpCache,
|
2021-04-12 18:23:08 +00:00
|
|
|
ldapDialer: ldapDialer,
|
2021-04-10 01:49:43 +00:00
|
|
|
client: client,
|
|
|
|
ldapIdentityProviderInformer: ldapIdentityProviderInformer,
|
|
|
|
secretInformer: secretInformer,
|
|
|
|
}
|
|
|
|
return controllerlib.New(
|
|
|
|
controllerlib.Config{Name: ldapControllerName, Syncer: &c},
|
|
|
|
withInformer(
|
|
|
|
ldapIdentityProviderInformer,
|
|
|
|
pinnipedcontroller.MatchAnythingFilter(pinnipedcontroller.SingletonQueue()),
|
|
|
|
controllerlib.InformerOption{},
|
|
|
|
),
|
|
|
|
withInformer(
|
|
|
|
secretInformer,
|
|
|
|
pinnipedcontroller.MatchAnySecretOfTypeFilter(ldapBindAccountSecretType, pinnipedcontroller.SingletonQueue()),
|
|
|
|
controllerlib.InformerOption{},
|
|
|
|
),
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Sync implements controllerlib.Syncer.
|
|
|
|
func (c *ldapWatcherController) Sync(ctx controllerlib.Context) error {
|
|
|
|
actualUpstreams, err := c.ldapIdentityProviderInformer.Lister().List(labels.Everything())
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("failed to list LDAPIdentityProviders: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
requeue := false
|
|
|
|
validatedUpstreams := make([]provider.UpstreamLDAPIdentityProviderI, 0, len(actualUpstreams))
|
|
|
|
for _, upstream := range actualUpstreams {
|
2021-04-12 20:53:21 +00:00
|
|
|
valid := c.validateUpstream(ctx.Context, upstream)
|
2021-04-10 01:49:43 +00:00
|
|
|
if valid == nil {
|
|
|
|
requeue = true
|
|
|
|
} else {
|
|
|
|
validatedUpstreams = append(validatedUpstreams, valid)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
c.cache.SetLDAPIdentityProviders(validatedUpstreams)
|
|
|
|
if requeue {
|
|
|
|
return controllerlib.ErrSyntheticRequeue
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2021-04-12 20:53:21 +00:00
|
|
|
func (c *ldapWatcherController) validateUpstream(ctx context.Context, upstream *v1alpha1.LDAPIdentityProvider) provider.UpstreamLDAPIdentityProviderI {
|
2021-04-12 18:23:08 +00:00
|
|
|
spec := upstream.Spec
|
2021-04-14 00:16:57 +00:00
|
|
|
|
2021-04-15 17:25:35 +00:00
|
|
|
config := &upstreamldap.ProviderConfig{
|
2021-04-14 00:16:57 +00:00
|
|
|
Name: upstream.Name,
|
|
|
|
Host: spec.Host,
|
2021-04-15 17:25:35 +00:00
|
|
|
UserSearch: upstreamldap.UserSearchConfig{
|
2021-04-12 18:23:08 +00:00
|
|
|
Base: spec.UserSearch.Base,
|
|
|
|
Filter: spec.UserSearch.Filter,
|
|
|
|
UsernameAttribute: spec.UserSearch.Attributes.Username,
|
|
|
|
UIDAttribute: spec.UserSearch.Attributes.UniqueID,
|
|
|
|
},
|
|
|
|
Dialer: c.ldapDialer,
|
|
|
|
}
|
2021-04-15 21:44:43 +00:00
|
|
|
|
|
|
|
conditions := []*v1alpha1.Condition{}
|
|
|
|
secretValidCondition := c.validateSecret(upstream, config)
|
|
|
|
tlsValidCondition := c.validateTLSConfig(upstream, config)
|
|
|
|
conditions = append(conditions, secretValidCondition, tlsValidCondition)
|
|
|
|
|
|
|
|
// No point in trying to connect to the server if the config was already determined to be invalid.
|
|
|
|
if secretValidCondition.Status == v1alpha1.ConditionTrue && tlsValidCondition.Status == v1alpha1.ConditionTrue {
|
2021-04-15 23:46:27 +00:00
|
|
|
finishedConfigCondition := c.validateFinishedConfig(ctx, upstream, config)
|
|
|
|
// nil when there is no need to update this condition.
|
|
|
|
if finishedConfigCondition != nil {
|
|
|
|
conditions = append(conditions, finishedConfigCondition)
|
|
|
|
}
|
2021-04-12 20:53:21 +00:00
|
|
|
}
|
2021-04-15 21:44:43 +00:00
|
|
|
|
2021-04-12 20:53:21 +00:00
|
|
|
hadErrorCondition := c.updateStatus(ctx, upstream, conditions)
|
|
|
|
if hadErrorCondition {
|
|
|
|
return nil
|
|
|
|
}
|
2021-04-15 21:44:43 +00:00
|
|
|
|
2021-04-15 17:25:35 +00:00
|
|
|
return upstreamldap.New(*config)
|
2021-04-12 18:23:08 +00:00
|
|
|
}
|
|
|
|
|
2021-04-15 21:44:43 +00:00
|
|
|
func (c *ldapWatcherController) validateTLSConfig(upstream *v1alpha1.LDAPIdentityProvider, config *upstreamldap.ProviderConfig) *v1alpha1.Condition {
|
2021-04-14 00:16:57 +00:00
|
|
|
tlsSpec := upstream.Spec.TLS
|
|
|
|
if tlsSpec == nil {
|
|
|
|
return c.validTLSCondition(noTLSConfigurationMessage)
|
|
|
|
}
|
|
|
|
if len(tlsSpec.CertificateAuthorityData) == 0 {
|
|
|
|
return c.validTLSCondition(loadedTLSConfigurationMessage)
|
|
|
|
}
|
|
|
|
|
|
|
|
bundle, err := base64.StdEncoding.DecodeString(tlsSpec.CertificateAuthorityData)
|
|
|
|
if err != nil {
|
|
|
|
return c.invalidTLSCondition(fmt.Sprintf("certificateAuthorityData is invalid: %s", err.Error()))
|
|
|
|
}
|
|
|
|
|
|
|
|
ca := x509.NewCertPool()
|
|
|
|
ok := ca.AppendCertsFromPEM(bundle)
|
|
|
|
if !ok {
|
|
|
|
return c.invalidTLSCondition(fmt.Sprintf("certificateAuthorityData is invalid: %s", errNoCertificates))
|
|
|
|
}
|
|
|
|
|
2021-04-15 21:44:43 +00:00
|
|
|
config.CABundle = bundle
|
2021-04-14 00:16:57 +00:00
|
|
|
return c.validTLSCondition(loadedTLSConfigurationMessage)
|
|
|
|
}
|
|
|
|
|
2021-04-15 23:46:27 +00:00
|
|
|
func (c *ldapWatcherController) validateFinishedConfig(ctx context.Context, upstream *v1alpha1.LDAPIdentityProvider, config *upstreamldap.ProviderConfig) *v1alpha1.Condition {
|
2021-04-15 21:44:43 +00:00
|
|
|
ldapProvider := upstreamldap.New(*config)
|
|
|
|
|
2021-04-15 23:46:27 +00:00
|
|
|
if alreadyValidatedFinishedConfigForThisSpecGeneration(upstream) {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
testConnectionTimeout, cancelFunc := context.WithTimeout(ctx, 90*time.Second)
|
2021-04-15 21:44:43 +00:00
|
|
|
defer cancelFunc()
|
|
|
|
|
|
|
|
err := ldapProvider.TestConnection(testConnectionTimeout)
|
|
|
|
if err != nil {
|
|
|
|
return &v1alpha1.Condition{
|
|
|
|
Type: typeLDAPConnectionValid,
|
|
|
|
Status: v1alpha1.ConditionFalse,
|
|
|
|
Reason: reasonLDAPConnectionError,
|
|
|
|
Message: fmt.Sprintf(`could not successfully connect to "%s" and bind as user "%s: %s`, config.Host, config.BindUsername, err.Error()),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return &v1alpha1.Condition{
|
|
|
|
Type: typeLDAPConnectionValid,
|
|
|
|
Status: v1alpha1.ConditionTrue,
|
|
|
|
Reason: reasonSuccess,
|
|
|
|
Message: fmt.Sprintf(`successfully able to connect to "%s" and bind as user "%s"`, config.Host, config.BindUsername),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-04-15 23:46:27 +00:00
|
|
|
func alreadyValidatedFinishedConfigForThisSpecGeneration(upstream *v1alpha1.LDAPIdentityProvider) bool {
|
|
|
|
currentGeneration := upstream.Generation
|
|
|
|
for _, c := range upstream.Status.Conditions {
|
|
|
|
if c.Type == typeLDAPConnectionValid && c.Status == v1alpha1.ConditionTrue && c.ObservedGeneration == currentGeneration {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
2021-04-14 00:16:57 +00:00
|
|
|
func (c *ldapWatcherController) validTLSCondition(message string) *v1alpha1.Condition {
|
|
|
|
return &v1alpha1.Condition{
|
2021-04-15 21:44:43 +00:00
|
|
|
Type: typeTLSConfigurationValid,
|
2021-04-14 00:16:57 +00:00
|
|
|
Status: v1alpha1.ConditionTrue,
|
|
|
|
Reason: reasonSuccess,
|
|
|
|
Message: message,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *ldapWatcherController) invalidTLSCondition(message string) *v1alpha1.Condition {
|
|
|
|
return &v1alpha1.Condition{
|
2021-04-15 21:44:43 +00:00
|
|
|
Type: typeTLSConfigurationValid,
|
2021-04-14 00:16:57 +00:00
|
|
|
Status: v1alpha1.ConditionFalse,
|
|
|
|
Reason: reasonInvalidTLSConfig,
|
|
|
|
Message: message,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-04-15 21:44:43 +00:00
|
|
|
func (c *ldapWatcherController) validateSecret(upstream *v1alpha1.LDAPIdentityProvider, config *upstreamldap.ProviderConfig) *v1alpha1.Condition {
|
2021-04-12 18:23:08 +00:00
|
|
|
secretName := upstream.Spec.Bind.SecretName
|
|
|
|
|
|
|
|
secret, err := c.secretInformer.Lister().Secrets(upstream.Namespace).Get(secretName)
|
|
|
|
if err != nil {
|
2021-04-12 20:53:21 +00:00
|
|
|
return &v1alpha1.Condition{
|
|
|
|
Type: typeBindSecretValid,
|
|
|
|
Status: v1alpha1.ConditionFalse,
|
|
|
|
Reason: reasonNotFound,
|
|
|
|
Message: err.Error(),
|
|
|
|
}
|
2021-04-12 18:23:08 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if secret.Type != corev1.SecretTypeBasicAuth {
|
2021-04-12 20:53:21 +00:00
|
|
|
return &v1alpha1.Condition{
|
|
|
|
Type: typeBindSecretValid,
|
|
|
|
Status: v1alpha1.ConditionFalse,
|
|
|
|
Reason: reasonWrongType,
|
|
|
|
Message: fmt.Sprintf("referenced Secret %q has wrong type %q (should be %q)", secretName, secret.Type, corev1.SecretTypeBasicAuth),
|
|
|
|
}
|
2021-04-12 18:23:08 +00:00
|
|
|
}
|
|
|
|
|
2021-04-15 21:44:43 +00:00
|
|
|
config.BindUsername = string(secret.Data[corev1.BasicAuthUsernameKey])
|
|
|
|
config.BindPassword = string(secret.Data[corev1.BasicAuthPasswordKey])
|
|
|
|
if len(config.BindUsername) == 0 || len(config.BindPassword) == 0 {
|
2021-04-12 20:53:21 +00:00
|
|
|
return &v1alpha1.Condition{
|
|
|
|
Type: typeBindSecretValid,
|
|
|
|
Status: v1alpha1.ConditionFalse,
|
|
|
|
Reason: reasonMissingKeys,
|
|
|
|
Message: fmt.Sprintf("referenced Secret %q is missing required keys %q", secretName, []string{corev1.BasicAuthUsernameKey, corev1.BasicAuthPasswordKey}),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return &v1alpha1.Condition{
|
|
|
|
Type: typeBindSecretValid,
|
|
|
|
Status: v1alpha1.ConditionTrue,
|
|
|
|
Reason: reasonSuccess,
|
|
|
|
Message: "loaded bind secret",
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *ldapWatcherController) updateStatus(ctx context.Context, upstream *v1alpha1.LDAPIdentityProvider, conditions []*v1alpha1.Condition) bool {
|
|
|
|
log := klogr.New().WithValues("namespace", upstream.Namespace, "name", upstream.Name)
|
|
|
|
updated := upstream.DeepCopy()
|
|
|
|
|
|
|
|
hadErrorCondition := mergeConditions(conditions, upstream.Generation, &updated.Status.Conditions, log)
|
|
|
|
|
|
|
|
updated.Status.Phase = v1alpha1.LDAPPhaseReady
|
|
|
|
if hadErrorCondition {
|
|
|
|
updated.Status.Phase = v1alpha1.LDAPPhaseError
|
|
|
|
}
|
|
|
|
|
|
|
|
if equality.Semantic.DeepEqual(upstream, updated) {
|
|
|
|
return hadErrorCondition
|
|
|
|
}
|
|
|
|
|
|
|
|
_, err := c.client.
|
|
|
|
IDPV1alpha1().
|
|
|
|
LDAPIdentityProviders(upstream.Namespace).
|
|
|
|
UpdateStatus(ctx, updated, metav1.UpdateOptions{})
|
|
|
|
if err != nil {
|
|
|
|
log.Error(err, "failed to update status")
|
2021-04-12 18:23:08 +00:00
|
|
|
}
|
|
|
|
|
2021-04-12 20:53:21 +00:00
|
|
|
return hadErrorCondition
|
2021-04-10 01:49:43 +00:00
|
|
|
}
|