// Copyright 2021 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 // Package ldapupstreamwatcher implements a controller which watches LDAPIdentityProviders. package ldapupstreamwatcher import ( "context" "fmt" "time" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/equality" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/labels" corev1informers "k8s.io/client-go/informers/core/v1" "k8s.io/klog/v2/klogr" "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/controller/conditionsutil" "go.pinniped.dev/internal/controller/supervisorconfig/upstreamwatchers" "go.pinniped.dev/internal/controllerlib" "go.pinniped.dev/internal/oidc/provider" "go.pinniped.dev/internal/upstreamldap" ) const ( ldapControllerName = "ldap-upstream-observer" ldapBindAccountSecretType = corev1.SecretTypeBasicAuth testLDAPConnectionTimeout = 90 * time.Second ) type ldapUpstreamGenericLDAPImpl struct { ldapIdentityProvider v1alpha1.LDAPIdentityProvider } func (g *ldapUpstreamGenericLDAPImpl) Spec() upstreamwatchers.UpstreamGenericLDAPSpec { return &ldapUpstreamGenericLDAPSpec{g.ldapIdentityProvider} } func (g *ldapUpstreamGenericLDAPImpl) Namespace() string { return g.ldapIdentityProvider.Namespace } func (g *ldapUpstreamGenericLDAPImpl) Name() string { return g.ldapIdentityProvider.Name } func (g *ldapUpstreamGenericLDAPImpl) Generation() int64 { return g.ldapIdentityProvider.Generation } func (g *ldapUpstreamGenericLDAPImpl) Status() upstreamwatchers.UpstreamGenericLDAPStatus { return &ldapUpstreamGenericLDAPStatus{g.ldapIdentityProvider} } type ldapUpstreamGenericLDAPSpec struct { ldapIdentityProvider v1alpha1.LDAPIdentityProvider } func (s *ldapUpstreamGenericLDAPSpec) Host() string { return s.ldapIdentityProvider.Spec.Host } func (s *ldapUpstreamGenericLDAPSpec) TLSSpec() *v1alpha1.TLSSpec { return s.ldapIdentityProvider.Spec.TLS } func (s *ldapUpstreamGenericLDAPSpec) BindSecretName() string { return s.ldapIdentityProvider.Spec.Bind.SecretName } func (s *ldapUpstreamGenericLDAPSpec) UserSearch() upstreamwatchers.UpstreamGenericLDAPUserSearch { return &ldapUpstreamGenericLDAPUserSearch{s.ldapIdentityProvider.Spec.UserSearch} } func (s *ldapUpstreamGenericLDAPSpec) GroupSearch() upstreamwatchers.UpstreamGenericLDAPGroupSearch { return &ldapUpstreamGenericLDAPGroupSearch{s.ldapIdentityProvider.Spec.GroupSearch} } type ldapUpstreamGenericLDAPUserSearch struct { userSearch v1alpha1.LDAPIdentityProviderUserSearch } func (u *ldapUpstreamGenericLDAPUserSearch) Base() string { return u.userSearch.Base } func (u *ldapUpstreamGenericLDAPUserSearch) Filter() string { return u.userSearch.Filter } func (u *ldapUpstreamGenericLDAPUserSearch) UsernameAttribute() string { return u.userSearch.Attributes.Username } func (u *ldapUpstreamGenericLDAPUserSearch) UIDAttribute() string { return u.userSearch.Attributes.UID } type ldapUpstreamGenericLDAPGroupSearch struct { groupSearch v1alpha1.LDAPIdentityProviderGroupSearch } func (g *ldapUpstreamGenericLDAPGroupSearch) Base() string { return g.groupSearch.Base } func (g *ldapUpstreamGenericLDAPGroupSearch) Filter() string { return g.groupSearch.Filter } func (g *ldapUpstreamGenericLDAPGroupSearch) GroupNameAttribute() string { return g.groupSearch.Attributes.GroupName } type ldapUpstreamGenericLDAPStatus struct { ldapIdentityProvider v1alpha1.LDAPIdentityProvider } func (s *ldapUpstreamGenericLDAPStatus) Conditions() []v1alpha1.Condition { return s.ldapIdentityProvider.Status.Conditions } // 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 validatedSecretVersionsCache *upstreamwatchers.SecretVersionCache ldapDialer upstreamldap.LDAPDialer client pinnipedclientset.Interface ldapIdentityProviderInformer idpinformers.LDAPIdentityProviderInformer secretInformer corev1informers.SecretInformer } // New instantiates a new controllerlib.Controller which will populate the provided UpstreamLDAPIdentityProviderICache. func New( idpCache UpstreamLDAPIdentityProviderICache, client pinnipedclientset.Interface, ldapIdentityProviderInformer idpinformers.LDAPIdentityProviderInformer, secretInformer corev1informers.SecretInformer, withInformer pinnipedcontroller.WithInformerOptionFunc, ) controllerlib.Controller { return newInternal( idpCache, // start with an empty secretVersionCache upstreamwatchers.NewSecretVersionCache(), // nil means to use a real production dialer when creating objects to add to the cache nil, client, ldapIdentityProviderInformer, secretInformer, withInformer, ) } // For test dependency injection purposes. func newInternal( idpCache UpstreamLDAPIdentityProviderICache, validatedSecretVersionsCache *upstreamwatchers.SecretVersionCache, ldapDialer upstreamldap.LDAPDialer, client pinnipedclientset.Interface, ldapIdentityProviderInformer idpinformers.LDAPIdentityProviderInformer, secretInformer corev1informers.SecretInformer, withInformer pinnipedcontroller.WithInformerOptionFunc, ) controllerlib.Controller { c := ldapWatcherController{ cache: idpCache, validatedSecretVersionsCache: validatedSecretVersionsCache, ldapDialer: ldapDialer, 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 { valid, requestedRequeue := c.validateUpstream(ctx.Context, upstream) if valid != nil { validatedUpstreams = append(validatedUpstreams, valid) } if requestedRequeue { requeue = true } } c.cache.SetLDAPIdentityProviders(validatedUpstreams) if requeue { return controllerlib.ErrSyntheticRequeue } return nil } func (c *ldapWatcherController) validateUpstream(ctx context.Context, upstream *v1alpha1.LDAPIdentityProvider) (p provider.UpstreamLDAPIdentityProviderI, requeue bool) { spec := upstream.Spec config := &upstreamldap.ProviderConfig{ Name: upstream.Name, Host: spec.Host, UserSearch: upstreamldap.UserSearchConfig{ Base: spec.UserSearch.Base, Filter: spec.UserSearch.Filter, UsernameAttribute: spec.UserSearch.Attributes.Username, UIDAttribute: spec.UserSearch.Attributes.UID, }, GroupSearch: upstreamldap.GroupSearchConfig{ Base: spec.GroupSearch.Base, Filter: spec.GroupSearch.Filter, GroupNameAttribute: spec.GroupSearch.Attributes.GroupName, }, Dialer: c.ldapDialer, } conditions := upstreamwatchers.ValidateGenericLDAP(ctx, &ldapUpstreamGenericLDAPImpl{*upstream}, c.secretInformer, c.validatedSecretVersionsCache, config) c.updateStatus(ctx, upstream, conditions.Conditions()) return upstreamwatchers.EvaluateConditions(conditions, config) } func (c *ldapWatcherController) updateStatus(ctx context.Context, upstream *v1alpha1.LDAPIdentityProvider, conditions []*v1alpha1.Condition) { log := klogr.New().WithValues("namespace", upstream.Namespace, "name", upstream.Name) updated := upstream.DeepCopy() hadErrorCondition := conditionsutil.Merge(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 // nothing to update } _, err := c.client. IDPV1alpha1(). LDAPIdentityProviders(upstream.Namespace). UpdateStatus(ctx, updated, metav1.UpdateOptions{}) if err != nil { log.Error(err, "failed to update status") } }