2023-05-08 21:07:38 +00:00
// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved.
2020-10-08 02:18:34 +00:00
// SPDX-License-Identifier: Apache-2.0
package supervisorconfig
import (
2020-10-08 17:27:45 +00:00
"context"
"fmt"
2020-10-23 23:25:44 +00:00
"net/url"
"strings"
2023-05-08 21:07:38 +00:00
"time"
2020-10-08 18:28:21 +00:00
2023-06-30 20:43:40 +00:00
"k8s.io/apimachinery/pkg/api/equality"
2020-10-08 17:27:45 +00:00
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2020-10-08 02:18:34 +00:00
"k8s.io/apimachinery/pkg/labels"
2023-05-08 21:07:38 +00:00
"k8s.io/apimachinery/pkg/types"
2021-02-05 17:56:05 +00:00
"k8s.io/apimachinery/pkg/util/errors"
2021-12-10 22:22:36 +00:00
"k8s.io/utils/clock"
2020-10-08 02:18:34 +00:00
2021-02-16 19:00:08 +00:00
configv1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/config/v1alpha1"
pinnipedclientset "go.pinniped.dev/generated/latest/client/supervisor/clientset/versioned"
configinformers "go.pinniped.dev/generated/latest/client/supervisor/informers/externalversions/config/v1alpha1"
2023-05-08 21:07:38 +00:00
idpinformers "go.pinniped.dev/generated/latest/client/supervisor/informers/externalversions/idp/v1alpha1"
"go.pinniped.dev/internal/celtransformer"
2020-10-08 02:18:34 +00:00
pinnipedcontroller "go.pinniped.dev/internal/controller"
2023-06-30 20:43:40 +00:00
"go.pinniped.dev/internal/controller/conditionsutil"
2020-10-08 02:18:34 +00:00
"go.pinniped.dev/internal/controllerlib"
2023-06-22 22:12:33 +00:00
"go.pinniped.dev/internal/federationdomain/federationdomainproviders"
2023-05-08 21:07:38 +00:00
"go.pinniped.dev/internal/idtransform"
2020-11-10 15:22:16 +00:00
"go.pinniped.dev/internal/plog"
2020-10-08 02:18:34 +00:00
)
2023-06-30 20:43:40 +00:00
const (
typeReady = "Ready"
typeIssuerURLValid = "IssuerURLValid"
typeOneTLSSecretPerIssuerHostname = "OneTLSSecretPerIssuerHostname"
typeIssuerIsUnique = "IssuerIsUnique"
2023-07-07 21:10:07 +00:00
typeIdentityProvidersFound = "IdentityProvidersFound"
2023-06-30 20:43:40 +00:00
2023-07-07 21:10:07 +00:00
reasonSuccess = "Success"
reasonNotReady = "NotReady"
reasonUnableToValidate = "UnableToValidate"
reasonInvalidIssuerURL = "InvalidIssuerURL"
reasonDuplicateIssuer = "DuplicateIssuer"
reasonDifferentSecretRefsFound = "DifferentSecretRefsFound"
reasonLegacyConfigurationSuccess = "LegacyConfigurationSuccess"
2023-06-30 20:43:40 +00:00
celTransformerMaxExpressionRuntime = 5 * time . Second
)
2023-06-13 21:20:39 +00:00
// FederationDomainsSetter can be notified of all known valid providers with its SetIssuer function.
2020-10-08 02:18:34 +00:00
// If there are no longer any valid issuers, then it can be called with no arguments.
// Implementations of this type should be thread-safe to support calls from multiple goroutines.
2023-06-13 21:20:39 +00:00
type FederationDomainsSetter interface {
2023-06-22 20:12:50 +00:00
SetFederationDomains ( federationDomains ... * federationdomainproviders . FederationDomainIssuer )
2020-10-08 02:18:34 +00:00
}
2020-12-16 22:27:09 +00:00
type federationDomainWatcherController struct {
2023-06-13 21:20:39 +00:00
federationDomainsSetter FederationDomainsSetter
clock clock . Clock
client pinnipedclientset . Interface
2023-05-08 21:07:38 +00:00
federationDomainInformer configinformers . FederationDomainInformer
oidcIdentityProviderInformer idpinformers . OIDCIdentityProviderInformer
ldapIdentityProviderInformer idpinformers . LDAPIdentityProviderInformer
activeDirectoryIdentityProviderInformer idpinformers . ActiveDirectoryIdentityProviderInformer
2020-10-08 02:18:34 +00:00
}
2020-12-16 22:27:09 +00:00
// NewFederationDomainWatcherController creates a controllerlib.Controller that watches
// FederationDomain objects and notifies a callback object of the collection of provider configs.
func NewFederationDomainWatcherController (
2023-06-13 21:20:39 +00:00
federationDomainsSetter FederationDomainsSetter ,
2020-10-08 17:27:45 +00:00
clock clock . Clock ,
client pinnipedclientset . Interface ,
2020-12-17 19:34:49 +00:00
federationDomainInformer configinformers . FederationDomainInformer ,
2023-05-08 21:07:38 +00:00
oidcIdentityProviderInformer idpinformers . OIDCIdentityProviderInformer ,
ldapIdentityProviderInformer idpinformers . LDAPIdentityProviderInformer ,
activeDirectoryIdentityProviderInformer idpinformers . ActiveDirectoryIdentityProviderInformer ,
2020-10-08 02:18:34 +00:00
withInformer pinnipedcontroller . WithInformerOptionFunc ,
) controllerlib . Controller {
return controllerlib . New (
controllerlib . Config {
2020-12-16 22:27:09 +00:00
Name : "FederationDomainWatcherController" ,
Syncer : & federationDomainWatcherController {
2023-06-13 21:20:39 +00:00
federationDomainsSetter : federationDomainsSetter ,
2023-05-08 21:07:38 +00:00
clock : clock ,
client : client ,
federationDomainInformer : federationDomainInformer ,
oidcIdentityProviderInformer : oidcIdentityProviderInformer ,
ldapIdentityProviderInformer : ldapIdentityProviderInformer ,
activeDirectoryIdentityProviderInformer : activeDirectoryIdentityProviderInformer ,
2020-10-08 02:18:34 +00:00
} ,
} ,
withInformer (
2020-12-17 19:34:49 +00:00
federationDomainInformer ,
2020-10-02 17:22:18 +00:00
pinnipedcontroller . MatchAnythingFilter ( pinnipedcontroller . SingletonQueue ( ) ) ,
2020-10-08 02:18:34 +00:00
controllerlib . InformerOption { } ,
) ,
2023-05-08 21:07:38 +00:00
withInformer (
oidcIdentityProviderInformer ,
// Since this controller only cares about IDP metadata names and UIDs (immutable fields),
// we only need to trigger Sync on creates and deletes.
pinnipedcontroller . MatchAnythingIgnoringUpdatesFilter ( pinnipedcontroller . SingletonQueue ( ) ) ,
controllerlib . InformerOption { } ,
) ,
withInformer (
ldapIdentityProviderInformer ,
// Since this controller only cares about IDP metadata names and UIDs (immutable fields),
// we only need to trigger Sync on creates and deletes.
pinnipedcontroller . MatchAnythingIgnoringUpdatesFilter ( pinnipedcontroller . SingletonQueue ( ) ) ,
controllerlib . InformerOption { } ,
) ,
withInformer (
activeDirectoryIdentityProviderInformer ,
// Since this controller only cares about IDP metadata names and UIDs (immutable fields),
// we only need to trigger Sync on creates and deletes.
pinnipedcontroller . MatchAnythingIgnoringUpdatesFilter ( pinnipedcontroller . SingletonQueue ( ) ) ,
controllerlib . InformerOption { } ,
) ,
2020-10-08 02:18:34 +00:00
)
}
// Sync implements controllerlib.Syncer.
2023-06-13 19:26:59 +00:00
func ( c * federationDomainWatcherController ) Sync ( ctx controllerlib . Context ) error { //nolint:funlen,gocyclo
2020-12-17 19:34:49 +00:00
federationDomains , err := c . federationDomainInformer . Lister ( ) . List ( labels . Everything ( ) )
2020-10-08 02:18:34 +00:00
if err != nil {
return err
}
2021-02-05 17:56:05 +00:00
var errs [ ] error
2023-06-22 20:12:50 +00:00
federationDomainIssuers := make ( [ ] * federationdomainproviders . FederationDomainIssuer , 0 )
2023-06-30 20:43:40 +00:00
crossDomainConfigValidator := newCrossFederationDomainConfigValidator ( federationDomains )
2020-12-17 19:34:49 +00:00
for _ , federationDomain := range federationDomains {
2023-06-30 20:43:40 +00:00
conditions := make ( [ ] * configv1alpha1 . Condition , 0 , 4 )
2020-10-23 23:25:44 +00:00
2023-06-30 20:43:40 +00:00
conditions = crossDomainConfigValidator . Validate ( federationDomain , conditions )
2020-10-08 17:27:45 +00:00
2023-05-08 21:07:38 +00:00
// TODO: Move all this identity provider stuff into helper functions. This is just a sketch of how the code would
// work in the sense that this is not doing error handling, is not validating everything that it should, and
// is not updating the status of the FederationDomain with anything related to these identity providers.
// This code may crash on invalid inputs since it is not handling any errors. However, when given valid inputs,
// this correctly implements the multiple IDPs features.
// Create the list of IDPs for this FederationDomain.
// Don't worry if the IDP CRs themselves is phase=Ready because those which are not ready will not be loaded
// into the provider cache, so they cannot actually be used to authenticate.
2023-06-22 20:12:50 +00:00
federationDomainIdentityProviders := [ ] * federationdomainproviders . FederationDomainIdentityProvider { }
var defaultFederationDomainIdentityProvider * federationdomainproviders . FederationDomainIdentityProvider
2023-05-08 21:07:38 +00:00
if len ( federationDomain . Spec . IdentityProviders ) == 0 {
// When the FederationDomain does not list any IDPs, then we might be in backwards compatibility mode.
oidcIdentityProviders , _ := c . oidcIdentityProviderInformer . Lister ( ) . List ( labels . Everything ( ) )
ldapIdentityProviders , _ := c . ldapIdentityProviderInformer . Lister ( ) . List ( labels . Everything ( ) )
activeDirectoryIdentityProviders , _ := c . activeDirectoryIdentityProviderInformer . Lister ( ) . List ( labels . Everything ( ) )
// TODO handle err return value for each of the above three lines
// Check if that there is exactly one IDP defined in the Supervisor namespace of any IDP CRD type.
idpCRsCount := len ( oidcIdentityProviders ) + len ( ldapIdentityProviders ) + len ( activeDirectoryIdentityProviders )
if idpCRsCount == 1 {
2023-07-07 21:10:07 +00:00
foundIDPName := ""
2023-05-08 21:07:38 +00:00
// If so, default that IDP's DisplayName to be the same as its resource Name.
2023-06-22 20:12:50 +00:00
defaultFederationDomainIdentityProvider = & federationdomainproviders . FederationDomainIdentityProvider { }
2023-05-08 21:07:38 +00:00
switch {
case len ( oidcIdentityProviders ) == 1 :
defaultFederationDomainIdentityProvider . DisplayName = oidcIdentityProviders [ 0 ] . Name
defaultFederationDomainIdentityProvider . UID = oidcIdentityProviders [ 0 ] . UID
2023-07-07 21:10:07 +00:00
foundIDPName = oidcIdentityProviders [ 0 ] . Name
2023-05-08 21:07:38 +00:00
case len ( ldapIdentityProviders ) == 1 :
defaultFederationDomainIdentityProvider . DisplayName = ldapIdentityProviders [ 0 ] . Name
defaultFederationDomainIdentityProvider . UID = ldapIdentityProviders [ 0 ] . UID
2023-07-07 21:10:07 +00:00
foundIDPName = ldapIdentityProviders [ 0 ] . Name
2023-05-08 21:07:38 +00:00
case len ( activeDirectoryIdentityProviders ) == 1 :
defaultFederationDomainIdentityProvider . DisplayName = activeDirectoryIdentityProviders [ 0 ] . Name
defaultFederationDomainIdentityProvider . UID = activeDirectoryIdentityProviders [ 0 ] . UID
2023-07-07 21:10:07 +00:00
foundIDPName = activeDirectoryIdentityProviders [ 0 ] . Name
2023-05-08 21:07:38 +00:00
}
// Backwards compatibility mode always uses an empty identity transformation pipline since no
// transformations are defined on the FederationDomain.
defaultFederationDomainIdentityProvider . Transforms = idtransform . NewTransformationPipeline ( )
2023-07-07 21:10:07 +00:00
conditions = append ( conditions , & configv1alpha1 . Condition {
Type : typeIdentityProvidersFound ,
Status : configv1alpha1 . ConditionTrue ,
Reason : reasonLegacyConfigurationSuccess ,
Message : fmt . Sprintf ( "no resources were specified by .spec.identityProviders[].objectRef but exactly one " +
"identity provider resource has been found: using %q as " +
"identity provider: please explicitly list identity providers in .spec.identityProviders " +
"(this legacy configuration mode may be removed in a future version of Pinniped)" , foundIDPName ) ,
} )
2023-05-08 21:07:38 +00:00
plog . Warning ( "detected FederationDomain identity provider backwards compatibility mode: using the one existing identity provider for authentication" ,
"federationDomain" , federationDomain . Name )
} else {
2023-07-07 21:10:07 +00:00
// TODO(BEN): add the "falsy" status versions of this condition and add tests to cover.
// this matches the above "truthy" version of the condition above.
//
2023-05-08 21:07:38 +00:00
// There are no IDP CRs or there is more than one IDP CR. Either way, we are not in the backwards
// compatibility mode because there is not exactly one IDP CR in the namespace, despite the fact that no
// IDPs are listed on the FederationDomain. Create a FederationDomain which has no IDPs and therefore
// cannot actually be used to log in, but still serves endpoints.
// TODO: Write something into the FederationDomain's status to explain what's happening and how to fix it.
2023-07-07 21:10:07 +00:00
// write code for these two condition~
// Type: IdentityProvidersFound
// Reason: LegacyConfigurationIdentityProviderNotFound
// Message: "no resources were specified by .spec.identityProviders[].objectRef and {number.of.idp.resources} identity provider resources have been found: please update .spec.identityProviders to specify which identity providers this federation domain should use"
// Status: false
//
// Type: IdentityProvidersFound
// Reason: LegacyConfigurationIdentityProviderNotFound
// Message: "no resources were specified by .spec.identityProviders[].objectRef and no identity provider resources have been found: please create an identity provider resource"
// status: false
//
2023-05-08 21:07:38 +00:00
plog . Warning ( "FederationDomain has no identity providers listed and there is not exactly one identity provider defined in the namespace: authentication disabled" ,
"federationDomain" , federationDomain . Name ,
"namespace" , federationDomain . Namespace ,
"identityProvidersCustomResourcesCount" , idpCRsCount ,
)
}
}
// If there is an explicit list of IDPs on the FederationDomain, then process the list.
2023-06-30 20:43:40 +00:00
celTransformer , _ := celtransformer . NewCELTransformer ( celTransformerMaxExpressionRuntime ) // TODO: what is a good duration limit here?
2023-05-08 21:07:38 +00:00
// TODO: handle err
for _ , idp := range federationDomain . Spec . IdentityProviders {
var idpResourceUID types . UID
var idpResourceName string
// TODO: Validate that all displayNames are unique within this FederationDomain's spec's list of identity providers.
// TODO: Validate that idp.ObjectRef.APIGroup is the expected APIGroup for IDP CRs "idp.supervisor.pinniped.dev"
// Validate that each objectRef resolves to an existing IDP. It does not matter if the IDP itself
// is phase=Ready, because it will not be loaded into the cache if not ready. For each objectRef
// that does not resolve, put an error on the FederationDomain status.
switch idp . ObjectRef . Kind {
case "LDAPIdentityProvider" :
ldapIDP , _ := c . ldapIdentityProviderInformer . Lister ( ) . LDAPIdentityProviders ( federationDomain . Namespace ) . Get ( idp . ObjectRef . Name )
// TODO: handle notfound err and also unexpected errors
idpResourceName = ldapIDP . Name
idpResourceUID = ldapIDP . UID
case "ActiveDirectoryIdentityProvider" :
adIDP , _ := c . activeDirectoryIdentityProviderInformer . Lister ( ) . ActiveDirectoryIdentityProviders ( federationDomain . Namespace ) . Get ( idp . ObjectRef . Name )
// TODO: handle notfound err and also unexpected errors
idpResourceName = adIDP . Name
idpResourceUID = adIDP . UID
case "OIDCIdentityProvider" :
oidcIDP , _ := c . oidcIdentityProviderInformer . Lister ( ) . OIDCIdentityProviders ( federationDomain . Namespace ) . Get ( idp . ObjectRef . Name )
// TODO: handle notfound err and also unexpected errors
idpResourceName = oidcIDP . Name
idpResourceUID = oidcIDP . UID
default :
// TODO: handle bad user input
}
plog . Debug ( "resolved identity provider object reference" ,
"kind" , idp . ObjectRef . Kind ,
"name" , idp . ObjectRef . Name ,
"foundResourceName" , idpResourceName ,
"foundResourceUID" , idpResourceUID ,
)
// Prepare the transformations.
pipeline := idtransform . NewTransformationPipeline ( )
consts := & celtransformer . TransformationConstants {
StringConstants : map [ string ] string { } ,
StringListConstants : map [ string ] [ ] string { } ,
}
// Read all the declared constants.
for _ , c := range idp . Transforms . Constants {
switch c . Type {
case "string" :
consts . StringConstants [ c . Name ] = c . StringValue
case "stringList" :
consts . StringListConstants [ c . Name ] = c . StringListValue
default :
// TODO: this shouldn't really happen since the CRD validates it, but handle it as an error
}
}
// Compile all the expressions and add them to the pipeline.
for idx , e := range idp . Transforms . Expressions {
var rawTransform celtransformer . CELTransformation
switch e . Type {
case "username/v1" :
rawTransform = & celtransformer . UsernameTransformation { Expression : e . Expression }
case "groups/v1" :
rawTransform = & celtransformer . GroupsTransformation { Expression : e . Expression }
case "policy/v1" :
rawTransform = & celtransformer . AllowAuthenticationPolicy {
Expression : e . Expression ,
RejectedAuthenticationMessage : e . Message ,
}
default :
// TODO: this shouldn't really happen since the CRD validates it, but handle it as an error
}
compiledTransform , err := celTransformer . CompileTransformation ( rawTransform , consts )
if err != nil {
// TODO: handle compile err
plog . Error ( "error compiling identity transformation" , err ,
"federationDomain" , federationDomain . Name ,
"idpDisplayName" , idp . DisplayName ,
"transformationIndex" , idx ,
"transformationType" , e . Type ,
"transformationExpression" , e . Expression ,
)
}
pipeline . AppendTransformation ( compiledTransform )
plog . Debug ( "successfully compiled identity transformation expression" ,
"type" , e . Type ,
"expr" , e . Expression ,
"policyMessage" , e . Message ,
)
}
// Run all the provided transform examples. If any fail, put errors on the FederationDomain status.
for idx , e := range idp . Transforms . Examples {
// TODO: use a real context param below
result , _ := pipeline . Evaluate ( context . TODO ( ) , e . Username , e . Groups )
// TODO: handle err
resultWasAuthRejected := ! result . AuthenticationAllowed
2023-06-13 19:26:59 +00:00
if e . Expects . Rejected && ! resultWasAuthRejected { //nolint:gocritic,nestif
2023-05-08 21:07:38 +00:00
// TODO: handle this failed example
plog . Warning ( "FederationDomain identity provider transformations example failed: expected authentication to be rejected but it was not" ,
"federationDomain" , federationDomain . Name ,
"idpDisplayName" , idp . DisplayName ,
"exampleIndex" , idx ,
"expectedRejected" , e . Expects . Rejected ,
"actualRejectedResult" , resultWasAuthRejected ,
"expectedMessage" , e . Expects . Message ,
"actualMessageResult" , result . RejectedAuthenticationMessage ,
)
} else if ! e . Expects . Rejected && resultWasAuthRejected {
// TODO: handle this failed example
plog . Warning ( "FederationDomain identity provider transformations example failed: expected authentication not to be rejected but it was rejected" ,
"federationDomain" , federationDomain . Name ,
"idpDisplayName" , idp . DisplayName ,
"exampleIndex" , idx ,
"expectedRejected" , e . Expects . Rejected ,
"actualRejectedResult" , resultWasAuthRejected ,
"expectedMessage" , e . Expects . Message ,
"actualMessageResult" , result . RejectedAuthenticationMessage ,
)
} else if e . Expects . Rejected && resultWasAuthRejected && e . Expects . Message != result . RejectedAuthenticationMessage {
// TODO: when expected message is blank, then treat it like it expects the default message
// TODO: handle this failed example
plog . Warning ( "FederationDomain identity provider transformations example failed: expected a different authentication rejection message" ,
"federationDomain" , federationDomain . Name ,
"idpDisplayName" , idp . DisplayName ,
"exampleIndex" , idx ,
"expectedRejected" , e . Expects . Rejected ,
"actualRejectedResult" , resultWasAuthRejected ,
"expectedMessage" , e . Expects . Message ,
"actualMessageResult" , result . RejectedAuthenticationMessage ,
)
} else if result . AuthenticationAllowed {
// In the case where the user expected the auth to be allowed and it was allowed, then compare
// the expected username and group names to the actual username and group names.
// TODO: when both of these fail, put both errors onto the status (not just the first one)
if e . Expects . Username != result . Username {
// TODO: handle this failed example
plog . Warning ( "FederationDomain identity provider transformations example failed: expected a different transformed username" ,
"federationDomain" , federationDomain . Name ,
"idpDisplayName" , idp . DisplayName ,
"exampleIndex" , idx ,
"expectedUsername" , e . Expects . Username ,
"actualUsernameResult" , result . Username ,
)
}
if ! stringSlicesEqual ( e . Expects . Groups , result . Groups ) {
// TODO: Do we need to make this insensitive to ordering, or should the transformations evaluator be changed to always return sorted group names at the end of the pipeline?
2023-06-30 20:43:40 +00:00
// TODO: What happens if the user did not write any group expectation? Treat it like expecting an empty list of groups?
2023-05-08 21:07:38 +00:00
// TODO: handle this failed example
plog . Warning ( "FederationDomain identity provider transformations example failed: expected a different transformed groups list" ,
"federationDomain" , federationDomain . Name ,
"idpDisplayName" , idp . DisplayName ,
"exampleIndex" , idx ,
"expectedGroups" , e . Expects . Groups ,
"actualGroupsResult" , result . Groups ,
)
}
}
}
// For each valid IDP (unique displayName, valid objectRef + valid transforms), add it to the list.
2023-06-22 20:12:50 +00:00
federationDomainIdentityProviders = append ( federationDomainIdentityProviders , & federationdomainproviders . FederationDomainIdentityProvider {
2023-05-08 21:07:38 +00:00
DisplayName : idp . DisplayName ,
UID : idpResourceUID ,
Transforms : pipeline ,
} )
plog . Debug ( "loaded FederationDomain identity provider" ,
"federationDomain" , federationDomain . Name ,
"identityProviderDisplayName" , idp . DisplayName ,
"identityProviderResourceUID" , idpResourceUID ,
)
}
2023-07-07 21:10:07 +00:00
// TODO:
// - for the new "non-legacy" version of this to pass, we need to do this work. however, we don't need this for all of the tests for legacy functionality to work.
// Type: IdentityProvidersFound
// Reason: IdentityProvidersObjectRefsNotFound
// Message: ".spec.identityProviders[].objectRef identifies resource(s) that cannot be found: {list.of.specific.resources.that.cant.be.found.in.case.some.can.be.found.but.not.all}"
// Status: false
//
// TODO: maaaaybe we need this happy case as well for the legacy functionality tests to fully pass?
// Type: IdentityProvidersFound
// Reason: Success
// Message: Non-legacy happy state "the resources specified by .spec.identityProviders[].objectRef were found"
// Status: true
2023-05-08 21:07:38 +00:00
// Now that we have the list of IDPs for this FederationDomain, create the issuer.
2023-06-22 20:12:50 +00:00
var federationDomainIssuer * federationdomainproviders . FederationDomainIssuer
2023-05-08 21:07:38 +00:00
if defaultFederationDomainIdentityProvider != nil {
// This is the constructor for the backwards compatibility mode.
2023-06-22 20:12:50 +00:00
federationDomainIssuer , err = federationdomainproviders . NewFederationDomainIssuerWithDefaultIDP ( federationDomain . Spec . Issuer , defaultFederationDomainIdentityProvider )
2023-05-08 21:07:38 +00:00
} else {
// This is the constructor for any other case, including when there is an empty list of IDPs.
2023-06-22 20:12:50 +00:00
federationDomainIssuer , err = federationdomainproviders . NewFederationDomainIssuer ( federationDomain . Spec . Issuer , federationDomainIdentityProviders )
2023-05-08 21:07:38 +00:00
}
2020-10-08 02:18:34 +00:00
if err != nil {
2023-06-30 20:43:40 +00:00
// Note that the FederationDomainIssuer constructors only validate the Issuer URL,
// so these are always issuer URL validation errors.
conditions = append ( conditions , & configv1alpha1 . Condition {
Type : typeIssuerURLValid ,
Status : configv1alpha1 . ConditionFalse ,
Reason : reasonInvalidIssuerURL ,
Message : err . Error ( ) ,
} )
} else {
conditions = append ( conditions , & configv1alpha1 . Condition {
Type : typeIssuerURLValid ,
Status : configv1alpha1 . ConditionTrue ,
Reason : reasonSuccess ,
Message : "spec.issuer is a valid URL" ,
} )
2020-10-08 02:18:34 +00:00
}
2020-10-08 17:27:45 +00:00
2023-06-30 20:43:40 +00:00
if err = c . updateStatus ( ctx . Context , federationDomain , conditions ) ; err != nil {
2021-02-05 17:56:05 +00:00
errs = append ( errs , fmt . Errorf ( "could not update status: %w" , err ) )
2020-10-09 14:39:17 +00:00
continue
2020-10-08 17:27:45 +00:00
}
2020-12-17 19:34:49 +00:00
2023-06-30 20:43:40 +00:00
if ! hadErrorCondition ( conditions ) {
// Successfully validated the FederationDomain, so allow it to be loaded.
federationDomainIssuers = append ( federationDomainIssuers , federationDomainIssuer )
}
2020-10-08 17:27:45 +00:00
}
2023-07-07 21:10:07 +00:00
// BEN: notes:
// implementer of this function is the endpoint manager.
// it should re-setup all of the endpoints for the specified federation domains.
// (in test, this is wantFederationDomainIssues)
2023-06-13 21:20:39 +00:00
c . federationDomainsSetter . SetFederationDomains ( federationDomainIssuers ... )
2020-10-08 17:27:45 +00:00
2021-02-05 17:56:05 +00:00
return errors . NewAggregate ( errs )
2020-10-08 17:27:45 +00:00
}
2023-06-30 20:43:40 +00:00
func ( c * federationDomainWatcherController ) updateStatus (
ctx context . Context ,
federationDomain * configv1alpha1 . FederationDomain ,
conditions [ ] * configv1alpha1 . Condition ,
) error {
updated := federationDomain . DeepCopy ( )
if hadErrorCondition ( conditions ) {
updated . Status . Phase = configv1alpha1 . FederationDomainPhaseError
conditions = append ( conditions , & configv1alpha1 . Condition {
Type : typeReady ,
Status : configv1alpha1 . ConditionFalse ,
Reason : reasonNotReady ,
Message : "the FederationDomain is not ready: see other conditions for details" ,
} )
} else {
updated . Status . Phase = configv1alpha1 . FederationDomainPhaseReady
conditions = append ( conditions , & configv1alpha1 . Condition {
Type : typeReady ,
Status : configv1alpha1 . ConditionTrue ,
Reason : reasonSuccess ,
Message : fmt . Sprintf ( "the FederationDomain is ready and its endpoints are available: " +
"the discovery endpoint is %s/.well-known/openid-configuration" , federationDomain . Spec . Issuer ) ,
} )
2023-05-08 21:07:38 +00:00
}
2023-06-30 20:43:40 +00:00
_ = conditionsutil . MergeConfigConditions ( conditions ,
federationDomain . Generation , & updated . Status . Conditions , plog . New ( ) , metav1 . NewTime ( c . clock . Now ( ) ) )
if equality . Semantic . DeepEqual ( federationDomain , updated ) {
return nil
2023-05-08 21:07:38 +00:00
}
2023-06-30 20:43:40 +00:00
_ , err := c . client .
ConfigV1alpha1 ( ) .
FederationDomains ( federationDomain . Namespace ) .
UpdateStatus ( ctx , updated , metav1 . UpdateOptions { } )
return err
2023-05-08 21:07:38 +00:00
}
2023-06-30 20:43:40 +00:00
type crossFederationDomainConfigValidator struct {
issuerCounts map [ string ] int
uniqueSecretNamesPerIssuerAddress map [ string ] map [ string ] bool
}
func issuerURLToHostnameKey ( issuerURL * url . URL ) string {
return lowercaseHostWithoutPort ( issuerURL )
}
func issuerURLToIssuerKey ( issuerURL * url . URL ) string {
return fmt . Sprintf ( "%s://%s%s" , issuerURL . Scheme , strings . ToLower ( issuerURL . Host ) , issuerURL . Path )
}
func ( v * crossFederationDomainConfigValidator ) Validate ( federationDomain * configv1alpha1 . FederationDomain , conditions [ ] * configv1alpha1 . Condition ) [ ] * configv1alpha1 . Condition {
issuerURL , urlParseErr := url . Parse ( federationDomain . Spec . Issuer )
if urlParseErr != nil {
// Don't write a condition about the issuer URL being invalid because that is added elsewhere in the controller.
conditions = append ( conditions , & configv1alpha1 . Condition {
Type : typeIssuerIsUnique ,
Status : configv1alpha1 . ConditionUnknown ,
Reason : reasonUnableToValidate ,
Message : "unable to check if spec.issuer is unique among all FederationDomains because URL cannot be parsed" ,
} )
conditions = append ( conditions , & configv1alpha1 . Condition {
Type : typeOneTLSSecretPerIssuerHostname ,
Status : configv1alpha1 . ConditionUnknown ,
Reason : reasonUnableToValidate ,
Message : "unable to check if all FederationDomains are using the same TLS secret when using the same hostname in the spec.issuer URL because URL cannot be parsed" ,
} )
return conditions
}
if issuerCount := v . issuerCounts [ issuerURLToIssuerKey ( issuerURL ) ] ; issuerCount > 1 {
conditions = append ( conditions , & configv1alpha1 . Condition {
Type : typeIssuerIsUnique ,
Status : configv1alpha1 . ConditionFalse ,
Reason : reasonDuplicateIssuer ,
Message : "multiple FederationDomains have the same spec.issuer URL: these URLs must be unique (can use different hosts or paths)" ,
} )
} else {
conditions = append ( conditions , & configv1alpha1 . Condition {
Type : typeIssuerIsUnique ,
Status : configv1alpha1 . ConditionTrue ,
Reason : reasonSuccess ,
Message : "spec.issuer is unique among all FederationDomains" ,
} )
}
if len ( v . uniqueSecretNamesPerIssuerAddress [ issuerURLToHostnameKey ( issuerURL ) ] ) > 1 {
conditions = append ( conditions , & configv1alpha1 . Condition {
Type : typeOneTLSSecretPerIssuerHostname ,
Status : configv1alpha1 . ConditionFalse ,
Reason : reasonDifferentSecretRefsFound ,
Message : "when different FederationDomains are using the same hostname in the spec.issuer URL then they must also use the same TLS secretRef: different secretRefs found" ,
} )
} else {
conditions = append ( conditions , & configv1alpha1 . Condition {
Type : typeOneTLSSecretPerIssuerHostname ,
Status : configv1alpha1 . ConditionTrue ,
Reason : reasonSuccess ,
Message : "all FederationDomains are using the same TLS secret when using the same hostname in the spec.issuer URL" ,
} )
}
return conditions
}
func newCrossFederationDomainConfigValidator ( federationDomains [ ] * configv1alpha1 . FederationDomain ) * crossFederationDomainConfigValidator {
// Make a map of issuer strings -> count of how many times we saw that issuer string.
// This will help us complain when there are duplicate issuer strings.
// Also make a helper function for forming keys into this map.
issuerCounts := make ( map [ string ] int )
// Make a map of issuer hostnames -> set of unique secret names. This will help us complain when
// multiple FederationDomains have the same issuer hostname (excluding port) but specify
// different TLS serving Secrets. Doesn't make sense to have the one address use more than one
// TLS cert. Ignore ports because SNI information on the incoming requests is not going to include
// port numbers. Also make a helper function for forming keys into this map.
uniqueSecretNamesPerIssuerAddress := make ( map [ string ] map [ string ] bool )
for _ , federationDomain := range federationDomains {
issuerURL , err := url . Parse ( federationDomain . Spec . Issuer )
2020-10-08 17:27:45 +00:00
if err != nil {
2023-06-30 20:43:40 +00:00
continue // Skip url parse errors because they will be handled in the Validate function.
2020-10-08 17:27:45 +00:00
}
2023-06-30 20:43:40 +00:00
issuerCounts [ issuerURLToIssuerKey ( issuerURL ) ] ++
setOfSecretNames := uniqueSecretNamesPerIssuerAddress [ issuerURLToHostnameKey ( issuerURL ) ]
if setOfSecretNames == nil {
setOfSecretNames = make ( map [ string ] bool )
uniqueSecretNamesPerIssuerAddress [ issuerURLToHostnameKey ( issuerURL ) ] = setOfSecretNames
}
if federationDomain . Spec . TLS != nil {
setOfSecretNames [ federationDomain . Spec . TLS . SecretName ] = true
2020-10-08 17:27:45 +00:00
}
2023-06-30 20:43:40 +00:00
}
2020-10-08 17:27:45 +00:00
2023-06-30 20:43:40 +00:00
return & crossFederationDomainConfigValidator {
issuerCounts : issuerCounts ,
uniqueSecretNamesPerIssuerAddress : uniqueSecretNamesPerIssuerAddress ,
}
2020-10-08 17:27:45 +00:00
}
2020-10-09 15:54:50 +00:00
2023-06-30 20:43:40 +00:00
func hadErrorCondition ( conditions [ ] * configv1alpha1 . Condition ) bool {
for _ , c := range conditions {
if c . Status != configv1alpha1 . ConditionTrue {
return true
}
}
return false
}
func stringSlicesEqual ( a [ ] string , b [ ] string ) bool {
if len ( a ) != len ( b ) {
return false
}
for i , itemFromA := range a {
if b [ i ] != itemFromA {
return false
}
}
return true
}