// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package supervisorconfig import ( "context" "errors" "fmt" "net/url" "sort" "testing" "time" "github.com/stretchr/testify/require" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" coretesting "k8s.io/client-go/testing" clocktesting "k8s.io/utils/clock/testing" "k8s.io/utils/pointer" configv1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/config/v1alpha1" idpv1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/idp/v1alpha1" pinnipedfake "go.pinniped.dev/generated/latest/client/supervisor/clientset/versioned/fake" pinnipedinformers "go.pinniped.dev/generated/latest/client/supervisor/informers/externalversions" "go.pinniped.dev/internal/controllerlib" "go.pinniped.dev/internal/federationdomain/federationdomainproviders" "go.pinniped.dev/internal/here" "go.pinniped.dev/internal/idtransform" "go.pinniped.dev/internal/testutil" ) func TestFederationDomainWatcherControllerInformerFilters(t *testing.T) { t.Parallel() federationDomainInformer := pinnipedinformers.NewSharedInformerFactoryWithOptions(nil, 0).Config().V1alpha1().FederationDomains() oidcIdentityProviderInformer := pinnipedinformers.NewSharedInformerFactoryWithOptions(nil, 0).IDP().V1alpha1().OIDCIdentityProviders() ldapIdentityProviderInformer := pinnipedinformers.NewSharedInformerFactoryWithOptions(nil, 0).IDP().V1alpha1().LDAPIdentityProviders() adIdentityProviderInformer := pinnipedinformers.NewSharedInformerFactoryWithOptions(nil, 0).IDP().V1alpha1().ActiveDirectoryIdentityProviders() tests := []struct { name string obj metav1.Object informer controllerlib.InformerGetter wantAdd bool wantUpdate bool wantDelete bool }{ { name: "any FederationDomain changes", obj: &configv1alpha1.FederationDomain{}, informer: federationDomainInformer, wantAdd: true, wantUpdate: true, wantDelete: true, }, { name: "any OIDCIdentityProvider adds or deletes, but updates are ignored", obj: &idpv1alpha1.OIDCIdentityProvider{}, informer: oidcIdentityProviderInformer, wantAdd: true, wantUpdate: false, wantDelete: true, }, { name: "any LDAPIdentityProvider adds or deletes, but updates are ignored", obj: &idpv1alpha1.LDAPIdentityProvider{}, informer: ldapIdentityProviderInformer, wantAdd: true, wantUpdate: false, wantDelete: true, }, { name: "any ActiveDirectoryIdentityProvider adds or deletes, but updates are ignored", obj: &idpv1alpha1.ActiveDirectoryIdentityProvider{}, informer: adIdentityProviderInformer, wantAdd: true, wantUpdate: false, wantDelete: true, }, } for _, test := range tests { test := test t.Run(test.name, func(t *testing.T) { t.Parallel() withInformer := testutil.NewObservableWithInformerOption() NewFederationDomainWatcherController( nil, "", nil, nil, federationDomainInformer, oidcIdentityProviderInformer, ldapIdentityProviderInformer, adIdentityProviderInformer, withInformer.WithInformer, // make it possible to observe the behavior of the Filters ) unrelatedObj := corev1.Secret{} filter := withInformer.GetFilterForInformer(test.informer) require.Equal(t, test.wantAdd, filter.Add(test.obj)) require.Equal(t, test.wantUpdate, filter.Update(&unrelatedObj, test.obj)) require.Equal(t, test.wantUpdate, filter.Update(test.obj, &unrelatedObj)) require.Equal(t, test.wantDelete, filter.Delete(test.obj)) }) } } type fakeFederationDomainsSetter struct { SetFederationDomainsWasCalled bool FederationDomainsReceived []*federationdomainproviders.FederationDomainIssuer } func (f *fakeFederationDomainsSetter) SetFederationDomains(federationDomains ...*federationdomainproviders.FederationDomainIssuer) { f.SetFederationDomainsWasCalled = true f.FederationDomainsReceived = federationDomains } var federationDomainGVR = schema.GroupVersionResource{ Group: configv1alpha1.SchemeGroupVersion.Group, Version: configv1alpha1.SchemeGroupVersion.Version, Resource: "federationdomains", } func TestTestFederationDomainWatcherControllerSync(t *testing.T) { t.Parallel() const namespace = "some-namespace" const apiGroupSuffix = "custom.suffix.pinniped.dev" const apiGroupSupervisor = "idp.supervisor." + apiGroupSuffix frozenNow := time.Date(2020, time.September, 23, 7, 42, 0, 0, time.Local) frozenMetav1Now := metav1.NewTime(frozenNow) oidcIdentityProvider := &idpv1alpha1.OIDCIdentityProvider{ ObjectMeta: metav1.ObjectMeta{ Name: "some-oidc-idp", Namespace: namespace, UID: "some-oidc-uid", }, } ldapIdentityProvider := &idpv1alpha1.LDAPIdentityProvider{ ObjectMeta: metav1.ObjectMeta{ Name: "some-ldap-idp", Namespace: namespace, UID: "some-ldap-uid", }, } adIdentityProvider := &idpv1alpha1.ActiveDirectoryIdentityProvider{ ObjectMeta: metav1.ObjectMeta{ Name: "some-ad-idp", Namespace: namespace, UID: "some-ad-uid", }, } federationDomain1 := &configv1alpha1.FederationDomain{ ObjectMeta: metav1.ObjectMeta{Name: "config1", Namespace: namespace, Generation: 123}, Spec: configv1alpha1.FederationDomainSpec{Issuer: "https://issuer1.com"}, } federationDomain2 := &configv1alpha1.FederationDomain{ ObjectMeta: metav1.ObjectMeta{Name: "config2", Namespace: namespace, Generation: 123}, Spec: configv1alpha1.FederationDomainSpec{Issuer: "https://issuer2.com"}, } invalidIssuerURLFederationDomain := &configv1alpha1.FederationDomain{ ObjectMeta: metav1.ObjectMeta{Name: "invalid-config", Namespace: namespace, Generation: 123}, Spec: configv1alpha1.FederationDomainSpec{Issuer: "https://invalid-issuer.com?some=query"}, } federationDomainIssuerWithIDPs := func(t *testing.T, fedDomainIssuer string, fdIDPs []*federationdomainproviders.FederationDomainIdentityProvider) *federationdomainproviders.FederationDomainIssuer { fdIssuer, err := federationdomainproviders.NewFederationDomainIssuer(fedDomainIssuer, fdIDPs) require.NoError(t, err) return fdIssuer } federationDomainIssuerWithDefaultIDP := func(t *testing.T, fedDomainIssuer string, idpObjectMeta metav1.ObjectMeta) *federationdomainproviders.FederationDomainIssuer { fdIDP := &federationdomainproviders.FederationDomainIdentityProvider{ DisplayName: idpObjectMeta.Name, UID: idpObjectMeta.UID, Transforms: idtransform.NewTransformationPipeline(), } fdIssuer, err := federationdomainproviders.NewFederationDomainIssuerWithDefaultIDP(fedDomainIssuer, fdIDP) require.NoError(t, err) return fdIssuer } happyReadyCondition := func(issuer string, time metav1.Time, observedGeneration int64) configv1alpha1.Condition { return configv1alpha1.Condition{ Type: "Ready", Status: "True", ObservedGeneration: observedGeneration, LastTransitionTime: time, Reason: "Success", Message: fmt.Sprintf("the FederationDomain is ready and its endpoints are available: "+ "the discovery endpoint is %s/.well-known/openid-configuration", issuer), } } sadReadyCondition := func(time metav1.Time, observedGeneration int64) configv1alpha1.Condition { return configv1alpha1.Condition{ Type: "Ready", Status: "False", ObservedGeneration: observedGeneration, LastTransitionTime: time, Reason: "NotReady", Message: "the FederationDomain is not ready: see other conditions for details", } } happyIssuerIsUniqueCondition := func(time metav1.Time, observedGeneration int64) configv1alpha1.Condition { return configv1alpha1.Condition{ Type: "IssuerIsUnique", Status: "True", ObservedGeneration: observedGeneration, LastTransitionTime: time, Reason: "Success", Message: "spec.issuer is unique among all FederationDomains", } } unknownIssuerIsUniqueCondition := func(time metav1.Time, observedGeneration int64) configv1alpha1.Condition { return configv1alpha1.Condition{ Type: "IssuerIsUnique", Status: "Unknown", ObservedGeneration: observedGeneration, LastTransitionTime: time, Reason: "UnableToValidate", Message: "unable to check if spec.issuer is unique among all FederationDomains because URL cannot be parsed", } } sadIssuerIsUniqueCondition := func(time metav1.Time, observedGeneration int64) configv1alpha1.Condition { return configv1alpha1.Condition{ Type: "IssuerIsUnique", Status: "False", ObservedGeneration: observedGeneration, LastTransitionTime: time, Reason: "DuplicateIssuer", Message: "multiple FederationDomains have the same spec.issuer URL: these URLs must be unique (can use different hosts or paths)", } } happyOneTLSSecretPerIssuerHostnameCondition := func(time metav1.Time, observedGeneration int64) configv1alpha1.Condition { return configv1alpha1.Condition{ Type: "OneTLSSecretPerIssuerHostname", Status: "True", ObservedGeneration: observedGeneration, LastTransitionTime: time, Reason: "Success", Message: "all FederationDomains are using the same TLS secret when using the same hostname in the spec.issuer URL", } } unknownOneTLSSecretPerIssuerHostnameCondition := func(time metav1.Time, observedGeneration int64) configv1alpha1.Condition { return configv1alpha1.Condition{ Type: "OneTLSSecretPerIssuerHostname", Status: "Unknown", ObservedGeneration: observedGeneration, LastTransitionTime: time, Reason: "UnableToValidate", 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", } } sadOneTLSSecretPerIssuerHostnameCondition := func(time metav1.Time, observedGeneration int64) configv1alpha1.Condition { return configv1alpha1.Condition{ Type: "OneTLSSecretPerIssuerHostname", Status: "False", ObservedGeneration: observedGeneration, LastTransitionTime: time, Reason: "DifferentSecretRefsFound", 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", } } happyIssuerURLValidCondition := func(time metav1.Time, observedGeneration int64) configv1alpha1.Condition { return configv1alpha1.Condition{ Type: "IssuerURLValid", Status: "True", ObservedGeneration: observedGeneration, LastTransitionTime: time, Reason: "Success", Message: "spec.issuer is a valid URL", } } sadIssuerURLValidConditionCannotHaveQuery := func(time metav1.Time, observedGeneration int64) configv1alpha1.Condition { return configv1alpha1.Condition{ Type: "IssuerURLValid", Status: "False", ObservedGeneration: observedGeneration, LastTransitionTime: time, Reason: "InvalidIssuerURL", Message: "issuer must not have query", } } sadIssuerURLValidConditionCannotParse := func(time metav1.Time, observedGeneration int64) configv1alpha1.Condition { return configv1alpha1.Condition{ Type: "IssuerURLValid", Status: "False", ObservedGeneration: observedGeneration, LastTransitionTime: time, Reason: "InvalidIssuerURL", Message: `could not parse issuer as URL: parse ":/host//path": missing protocol scheme`, } } happyIdentityProvidersFoundConditionLegacyConfigurationSuccess := func(idpName string, time metav1.Time, observedGeneration int64) configv1alpha1.Condition { return configv1alpha1.Condition{ Type: "IdentityProvidersFound", Status: "True", ObservedGeneration: observedGeneration, LastTransitionTime: time, Reason: "LegacyConfigurationSuccess", 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)", idpName), } } happyIdentityProvidersFoundConditionSuccess := func(time metav1.Time, observedGeneration int64) configv1alpha1.Condition { return configv1alpha1.Condition{ Type: "IdentityProvidersFound", Status: "True", ObservedGeneration: observedGeneration, LastTransitionTime: time, Reason: "Success", Message: "the resources specified by .spec.identityProviders[].objectRef were found", } } sadIdentityProvidersFoundConditionLegacyConfigurationIdentityProviderNotFound := func(time metav1.Time, observedGeneration int64) configv1alpha1.Condition { return configv1alpha1.Condition{ Type: "IdentityProvidersFound", Status: "False", ObservedGeneration: observedGeneration, LastTransitionTime: time, 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", } } sadIdentityProvidersFoundConditionIdentityProviderNotSpecified := func(idpCRsCount int, time metav1.Time, observedGeneration int64) configv1alpha1.Condition { return configv1alpha1.Condition{ Type: "IdentityProvidersFound", Status: "False", ObservedGeneration: observedGeneration, LastTransitionTime: time, Reason: "IdentityProviderNotSpecified", Message: fmt.Sprintf("no resources were specified by .spec.identityProviders[].objectRef "+ "and %q identity provider resources have been found: "+ "please update .spec.identityProviders to specify which identity providers "+ "this federation domain should use", idpCRsCount), } } sadIdentityProvidersFoundConditionIdentityProvidersObjectRefsNotFound := func(idpsNotFound string, time metav1.Time, observedGeneration int64) configv1alpha1.Condition { return configv1alpha1.Condition{ Type: "IdentityProvidersFound", Status: "False", ObservedGeneration: observedGeneration, LastTransitionTime: time, Reason: "IdentityProvidersObjectRefsNotFound", Message: fmt.Sprintf(".spec.identityProviders[].objectRef identifies resource(s) that cannot be found: %s", idpsNotFound), } } happyDisplayNamesUniqueCondition := func(time metav1.Time, observedGeneration int64) configv1alpha1.Condition { return configv1alpha1.Condition{ Type: "IdentityProvidersDisplayNamesUnique", Status: "True", ObservedGeneration: observedGeneration, LastTransitionTime: time, Reason: "Success", Message: "the names specified by .spec.identityProviders[].displayName are unique", } } sadDisplayNamesUniqueCondition := func(duplicateNames string, time metav1.Time, observedGeneration int64) configv1alpha1.Condition { return configv1alpha1.Condition{ Type: "IdentityProvidersDisplayNamesUnique", Status: "False", ObservedGeneration: observedGeneration, LastTransitionTime: time, Reason: "DuplicateDisplayNames", Message: fmt.Sprintf("the names specified by .spec.identityProviders[].displayName contain duplicates: %s", duplicateNames), } } happyConstNamesUniqueCondition := func(time metav1.Time, observedGeneration int64) configv1alpha1.Condition { return configv1alpha1.Condition{ Type: "TransformsConstantsNamesUnique", Status: "True", ObservedGeneration: observedGeneration, LastTransitionTime: time, Reason: "Success", Message: "the names specified by .spec.identityProviders[].transforms.constants[].name are unique", } } sadConstNamesUniqueCondition := func(duplicateNames string, time metav1.Time, observedGeneration int64) configv1alpha1.Condition { return configv1alpha1.Condition{ Type: "TransformsConstantsNamesUnique", Status: "False", ObservedGeneration: observedGeneration, LastTransitionTime: time, Reason: "DuplicateConstantsNames", Message: fmt.Sprintf("the names specified by .spec.identityProviders[].transforms.constants[].name contain duplicates: %s", duplicateNames), } } happyTransformationExpressionsCondition := func(time metav1.Time, observedGeneration int64) configv1alpha1.Condition { return configv1alpha1.Condition{ Type: "TransformsExpressionsValid", Status: "True", ObservedGeneration: observedGeneration, LastTransitionTime: time, Reason: "Success", Message: "the expressions specified by .spec.identityProviders[].transforms.expressions[] are valid", } } sadTransformationExpressionsCondition := func(errorMessages string, time metav1.Time, observedGeneration int64) configv1alpha1.Condition { return configv1alpha1.Condition{ Type: "TransformsExpressionsValid", Status: "False", ObservedGeneration: observedGeneration, LastTransitionTime: time, Reason: "InvalidTransformsExpressions", Message: fmt.Sprintf("the expressions specified by .spec.identityProviders[].transforms.expressions[] were invalid:\n\n%s", errorMessages), } } happyTransformationExamplesCondition := func(time metav1.Time, observedGeneration int64) configv1alpha1.Condition { return configv1alpha1.Condition{ Type: "TransformsExamplesPassed", Status: "True", ObservedGeneration: observedGeneration, LastTransitionTime: time, Reason: "Success", Message: "the examples specified by .spec.identityProviders[].transforms.examples[] had no errors", } } sadTransformationExamplesCondition := func(errorMessages string, time metav1.Time, observedGeneration int64) configv1alpha1.Condition { return configv1alpha1.Condition{ Type: "TransformsExamplesPassed", Status: "False", ObservedGeneration: observedGeneration, LastTransitionTime: time, Reason: "TransformsExamplesFailed", Message: fmt.Sprintf("the examples specified by .spec.identityProviders[].transforms.examples[] had errors:\n\n%s", errorMessages), } } unknownTransformationExamplesCondition := func(time metav1.Time, observedGeneration int64) configv1alpha1.Condition { return configv1alpha1.Condition{ Type: "TransformsExamplesPassed", Status: "Unknown", ObservedGeneration: observedGeneration, LastTransitionTime: time, Reason: "UnableToValidate", Message: "unable to check if the examples specified by .spec.identityProviders[].transforms.examples[] had errors because an expression was invalid", } } happyAPIGroupSuffixCondition := func(time metav1.Time, observedGeneration int64) configv1alpha1.Condition { return configv1alpha1.Condition{ Type: "IdentityProvidersObjectRefAPIGroupSuffixValid", Status: "True", ObservedGeneration: observedGeneration, LastTransitionTime: time, Reason: "Success", Message: "the API groups specified by .spec.identityProviders[].objectRef.apiGroup are recognized", } } sadAPIGroupSuffixCondition := func(badApiGroups string, time metav1.Time, observedGeneration int64) configv1alpha1.Condition { return configv1alpha1.Condition{ Type: "IdentityProvidersObjectRefAPIGroupSuffixValid", Status: "False", ObservedGeneration: observedGeneration, LastTransitionTime: time, Reason: "APIGroupUnrecognized", Message: fmt.Sprintf("the API groups specified by .spec.identityProviders[].objectRef.apiGroup "+ "are not recognized (should be \"idp.supervisor.%s\"): %s", apiGroupSuffix, badApiGroups), } } happyKindCondition := func(time metav1.Time, observedGeneration int64) configv1alpha1.Condition { return configv1alpha1.Condition{ Type: "IdentityProvidersObjectRefKindValid", Status: "True", ObservedGeneration: observedGeneration, LastTransitionTime: time, Reason: "Success", Message: "the kinds specified by .spec.identityProviders[].objectRef.kind are recognized", } } sadKindCondition := func(badKinds string, time metav1.Time, observedGeneration int64) configv1alpha1.Condition { return configv1alpha1.Condition{ Type: "IdentityProvidersObjectRefKindValid", Status: "False", ObservedGeneration: observedGeneration, LastTransitionTime: time, Reason: "KindUnrecognized", Message: fmt.Sprintf(`the kinds specified by .spec.identityProviders[].objectRef.kind are `+ `not recognized (should be one of "ActiveDirectoryIdentityProvider", "LDAPIdentityProvider", "OIDCIdentityProvider"): %s`, badKinds), } } sortConditionsByType := func(c []configv1alpha1.Condition) []configv1alpha1.Condition { cp := make([]configv1alpha1.Condition, len(c)) copy(cp, c) sort.SliceStable(cp, func(i, j int) bool { return cp[i].Type < cp[j].Type }) return cp } replaceConditions := func(conditions []configv1alpha1.Condition, sadConditions []configv1alpha1.Condition) []configv1alpha1.Condition { for _, sadReplaceCondition := range sadConditions { for origIndex, origCondition := range conditions { if origCondition.Type == sadReplaceCondition.Type { conditions[origIndex] = sadReplaceCondition break } } } return conditions } allHappyConditionsSuccess := func(issuer string, time metav1.Time, observedGeneration int64) []configv1alpha1.Condition { return sortConditionsByType([]configv1alpha1.Condition{ happyTransformationExamplesCondition(frozenMetav1Now, 123), happyTransformationExpressionsCondition(frozenMetav1Now, 123), happyConstNamesUniqueCondition(frozenMetav1Now, 123), happyKindCondition(frozenMetav1Now, 123), happyAPIGroupSuffixCondition(frozenMetav1Now, 123), happyDisplayNamesUniqueCondition(frozenMetav1Now, 123), happyIdentityProvidersFoundConditionSuccess(frozenMetav1Now, 123), happyIssuerIsUniqueCondition(frozenMetav1Now, 123), happyIssuerURLValidCondition(frozenMetav1Now, 123), happyOneTLSSecretPerIssuerHostnameCondition(frozenMetav1Now, 123), happyReadyCondition(issuer, frozenMetav1Now, 123), }) } allHappyConditionsLegacyConfigurationSuccess := func(issuer string, idpName string, time metav1.Time, observedGeneration int64) []configv1alpha1.Condition { return replaceConditions( allHappyConditionsSuccess(issuer, time, observedGeneration), []configv1alpha1.Condition{ happyIdentityProvidersFoundConditionLegacyConfigurationSuccess(idpName, time, observedGeneration), }, ) } invalidIssuerURL := ":/host//path" _, err := url.Parse(invalidIssuerURL) //nolint:staticcheck // Yes, this URL is intentionally invalid. require.Error(t, err) tests := []struct { name string inputObjects []runtime.Object configClient func(*pinnipedfake.Clientset) wantErr string wantStatusUpdates []*configv1alpha1.FederationDomain wantFDIssuers []*federationdomainproviders.FederationDomainIssuer }{ { name: "when there are no FederationDomains, no update actions happen and the list of FederationDomainIssuers is set to the empty list", inputObjects: []runtime.Object{}, wantFDIssuers: []*federationdomainproviders.FederationDomainIssuer{}, }, { name: "legacy config: when no identity provider is specified on federation domains, but exactly one identity " + "provider resource exists on cluster, the controller will set a default IDP on each federation domain " + "matching the only identity provider found", inputObjects: []runtime.Object{ federationDomain1, federationDomain2, oidcIdentityProvider, }, wantFDIssuers: []*federationdomainproviders.FederationDomainIssuer{ federationDomainIssuerWithDefaultIDP(t, federationDomain1.Spec.Issuer, oidcIdentityProvider.ObjectMeta), federationDomainIssuerWithDefaultIDP(t, federationDomain2.Spec.Issuer, oidcIdentityProvider.ObjectMeta), }, wantStatusUpdates: []*configv1alpha1.FederationDomain{ expectedFederationDomainStatusUpdate(federationDomain1, configv1alpha1.FederationDomainPhaseReady, allHappyConditionsLegacyConfigurationSuccess(federationDomain1.Spec.Issuer, oidcIdentityProvider.Name, frozenMetav1Now, 123), ), expectedFederationDomainStatusUpdate(federationDomain2, configv1alpha1.FederationDomainPhaseReady, allHappyConditionsLegacyConfigurationSuccess(federationDomain2.Spec.Issuer, oidcIdentityProvider.Name, frozenMetav1Now, 123), ), }, }, { name: "when there are two valid FederationDomains, but one is already up to date, the sync loop only updates " + "the out-of-date FederationDomain", inputObjects: []runtime.Object{ oidcIdentityProvider, &configv1alpha1.FederationDomain{ ObjectMeta: metav1.ObjectMeta{Name: federationDomain1.Name, Namespace: federationDomain1.Namespace, Generation: 123}, Spec: configv1alpha1.FederationDomainSpec{Issuer: federationDomain1.Spec.Issuer}, Status: configv1alpha1.FederationDomainStatus{ Phase: configv1alpha1.FederationDomainPhaseReady, Conditions: allHappyConditionsLegacyConfigurationSuccess(federationDomain1.Spec.Issuer, oidcIdentityProvider.Name, frozenMetav1Now, 123), }, }, federationDomain2, }, wantFDIssuers: []*federationdomainproviders.FederationDomainIssuer{ federationDomainIssuerWithDefaultIDP(t, federationDomain1.Spec.Issuer, oidcIdentityProvider.ObjectMeta), federationDomainIssuerWithDefaultIDP(t, federationDomain2.Spec.Issuer, oidcIdentityProvider.ObjectMeta), }, wantStatusUpdates: []*configv1alpha1.FederationDomain{ // only one update, because the other FederationDomain already had the right status expectedFederationDomainStatusUpdate(federationDomain2, configv1alpha1.FederationDomainPhaseReady, allHappyConditionsLegacyConfigurationSuccess(federationDomain2.Spec.Issuer, oidcIdentityProvider.Name, frozenMetav1Now, 123), ), }, }, { name: "when there are two valid FederationDomains, but updating one fails, the status on the FederationDomain will not change", inputObjects: []runtime.Object{ federationDomain1, federationDomain2, oidcIdentityProvider, }, configClient: func(client *pinnipedfake.Clientset) { client.PrependReactor( "update", "federationdomains", func(action coretesting.Action) (bool, runtime.Object, error) { fd := action.(coretesting.UpdateAction).GetObject().(*configv1alpha1.FederationDomain) if fd.Name == federationDomain1.Name { return true, nil, errors.New("some update error") } return false, nil, nil }, ) }, wantErr: "could not update status: some update error", wantFDIssuers: []*federationdomainproviders.FederationDomainIssuer{ federationDomainIssuerWithDefaultIDP(t, federationDomain1.Spec.Issuer, oidcIdentityProvider.ObjectMeta), federationDomainIssuerWithDefaultIDP(t, federationDomain2.Spec.Issuer, oidcIdentityProvider.ObjectMeta), }, wantStatusUpdates: []*configv1alpha1.FederationDomain{ expectedFederationDomainStatusUpdate(federationDomain1, configv1alpha1.FederationDomainPhaseReady, allHappyConditionsLegacyConfigurationSuccess(federationDomain1.Spec.Issuer, oidcIdentityProvider.Name, frozenMetav1Now, 123), ), expectedFederationDomainStatusUpdate(federationDomain2, configv1alpha1.FederationDomainPhaseReady, allHappyConditionsLegacyConfigurationSuccess(federationDomain2.Spec.Issuer, oidcIdentityProvider.Name, frozenMetav1Now, 123), ), }, }, { name: "when there are both valid and invalid FederationDomains, the status will be correctly set on each " + "FederationDomain individually", inputObjects: []runtime.Object{ invalidIssuerURLFederationDomain, federationDomain2, oidcIdentityProvider, }, wantFDIssuers: []*federationdomainproviders.FederationDomainIssuer{ // only the valid FederationDomain federationDomainIssuerWithDefaultIDP(t, federationDomain2.Spec.Issuer, oidcIdentityProvider.ObjectMeta), }, wantStatusUpdates: []*configv1alpha1.FederationDomain{ expectedFederationDomainStatusUpdate(invalidIssuerURLFederationDomain, configv1alpha1.FederationDomainPhaseError, replaceConditions( allHappyConditionsLegacyConfigurationSuccess(federationDomain2.Spec.Issuer, oidcIdentityProvider.Name, frozenMetav1Now, 123), []configv1alpha1.Condition{ sadIssuerURLValidConditionCannotHaveQuery(frozenMetav1Now, 123), sadReadyCondition(frozenMetav1Now, 123), }), ), expectedFederationDomainStatusUpdate(federationDomain2, configv1alpha1.FederationDomainPhaseReady, allHappyConditionsLegacyConfigurationSuccess(federationDomain2.Spec.Issuer, oidcIdentityProvider.Name, frozenMetav1Now, 123), ), }, }, { name: "when there are both valid and invalid FederationDomains, but updating the invalid one fails, the " + "existing status will be unchanged", inputObjects: []runtime.Object{ invalidIssuerURLFederationDomain, federationDomain2, oidcIdentityProvider, }, configClient: func(client *pinnipedfake.Clientset) { client.PrependReactor( "update", "federationdomains", func(action coretesting.Action) (bool, runtime.Object, error) { fd := action.(coretesting.UpdateAction).GetObject().(*configv1alpha1.FederationDomain) if fd.Name == invalidIssuerURLFederationDomain.Name { return true, nil, errors.New("some update error") } return false, nil, nil }, ) }, wantErr: "could not update status: some update error", wantFDIssuers: []*federationdomainproviders.FederationDomainIssuer{ // only the valid FederationDomain federationDomainIssuerWithDefaultIDP(t, federationDomain2.Spec.Issuer, oidcIdentityProvider.ObjectMeta), }, wantStatusUpdates: []*configv1alpha1.FederationDomain{ expectedFederationDomainStatusUpdate(invalidIssuerURLFederationDomain, configv1alpha1.FederationDomainPhaseError, replaceConditions( allHappyConditionsLegacyConfigurationSuccess(federationDomain2.Spec.Issuer, oidcIdentityProvider.Name, frozenMetav1Now, 123), []configv1alpha1.Condition{ sadIssuerURLValidConditionCannotHaveQuery(frozenMetav1Now, 123), sadReadyCondition(frozenMetav1Now, 123), }), ), expectedFederationDomainStatusUpdate(federationDomain2, configv1alpha1.FederationDomainPhaseReady, allHappyConditionsLegacyConfigurationSuccess(federationDomain2.Spec.Issuer, oidcIdentityProvider.Name, frozenMetav1Now, 123), ), }, }, { name: "when there are FederationDomains with duplicate issuer strings these particular FederationDomains " + "will report error on IssuerUnique conditions", inputObjects: []runtime.Object{ &configv1alpha1.FederationDomain{ ObjectMeta: metav1.ObjectMeta{Name: "duplicate1", Namespace: namespace, Generation: 123}, Spec: configv1alpha1.FederationDomainSpec{Issuer: "https://iSSueR-duPlicAte.cOm/a"}, }, &configv1alpha1.FederationDomain{ ObjectMeta: metav1.ObjectMeta{Name: "duplicate2", Namespace: namespace, Generation: 123}, Spec: configv1alpha1.FederationDomainSpec{Issuer: "https://issuer-duplicate.com/a"}, }, &configv1alpha1.FederationDomain{ ObjectMeta: metav1.ObjectMeta{Name: "not-duplicate", Namespace: namespace, Generation: 123}, Spec: configv1alpha1.FederationDomainSpec{Issuer: "https://issuer-duplicate.com/A"}, // different path (paths are case-sensitive) }, oidcIdentityProvider, }, wantFDIssuers: []*federationdomainproviders.FederationDomainIssuer{ federationDomainIssuerWithDefaultIDP(t, "https://issuer-duplicate.com/A", oidcIdentityProvider.ObjectMeta), }, wantStatusUpdates: []*configv1alpha1.FederationDomain{ expectedFederationDomainStatusUpdate( &configv1alpha1.FederationDomain{ ObjectMeta: metav1.ObjectMeta{Name: "duplicate1", Namespace: namespace, Generation: 123}, }, configv1alpha1.FederationDomainPhaseError, replaceConditions( allHappyConditionsLegacyConfigurationSuccess("https://iSSueR-duPlicAte.cOm/a", oidcIdentityProvider.Name, frozenMetav1Now, 123), []configv1alpha1.Condition{ sadIssuerIsUniqueCondition(frozenMetav1Now, 123), sadReadyCondition(frozenMetav1Now, 123), }), ), expectedFederationDomainStatusUpdate( &configv1alpha1.FederationDomain{ ObjectMeta: metav1.ObjectMeta{Name: "duplicate2", Namespace: namespace, Generation: 123}, }, configv1alpha1.FederationDomainPhaseError, replaceConditions( allHappyConditionsLegacyConfigurationSuccess("https://issuer-duplicate.com/a", oidcIdentityProvider.Name, frozenMetav1Now, 123), []configv1alpha1.Condition{ sadIssuerIsUniqueCondition(frozenMetav1Now, 123), sadReadyCondition(frozenMetav1Now, 123), }), ), expectedFederationDomainStatusUpdate( &configv1alpha1.FederationDomain{ ObjectMeta: metav1.ObjectMeta{Name: "not-duplicate", Namespace: namespace, Generation: 123}, }, configv1alpha1.FederationDomainPhaseReady, allHappyConditionsLegacyConfigurationSuccess("https://issuer-duplicate.com/A", oidcIdentityProvider.Name, frozenMetav1Now, 123), ), }, }, { name: "when there are FederationDomains with the same issuer DNS hostname using different secretNames these " + "particular FederationDomains will report errors on OneTLSSecretPerIssuerHostname conditions", inputObjects: []runtime.Object{ &configv1alpha1.FederationDomain{ ObjectMeta: metav1.ObjectMeta{Name: "fd1", Namespace: namespace, Generation: 123}, Spec: configv1alpha1.FederationDomainSpec{ Issuer: "https://iSSueR-duPlicAte-adDress.cOm/path1", TLS: &configv1alpha1.FederationDomainTLSSpec{SecretName: "secret1"}, }, }, &configv1alpha1.FederationDomain{ ObjectMeta: metav1.ObjectMeta{Name: "fd2", Namespace: namespace, Generation: 123}, Spec: configv1alpha1.FederationDomainSpec{ // Validation treats these as the same DNS hostname even though they have different port numbers, // because SNI information on the incoming requests is not going to include port numbers. Issuer: "https://issuer-duplicate-address.com:1234/path2", TLS: &configv1alpha1.FederationDomainTLSSpec{SecretName: "secret2"}, }, }, &configv1alpha1.FederationDomain{ ObjectMeta: metav1.ObjectMeta{Name: "differentIssuerAddressFederationDomain", Namespace: namespace, Generation: 123}, Spec: configv1alpha1.FederationDomainSpec{ Issuer: "https://issuer-not-duplicate.com", TLS: &configv1alpha1.FederationDomainTLSSpec{SecretName: "secret1"}, }, }, &configv1alpha1.FederationDomain{ ObjectMeta: metav1.ObjectMeta{Name: "invalidIssuerURLFederationDomain", Namespace: namespace, Generation: 123}, Spec: configv1alpha1.FederationDomainSpec{ Issuer: invalidIssuerURL, TLS: &configv1alpha1.FederationDomainTLSSpec{SecretName: "secret1"}, }, }, oidcIdentityProvider, }, wantFDIssuers: []*federationdomainproviders.FederationDomainIssuer{ federationDomainIssuerWithDefaultIDP(t, "https://issuer-not-duplicate.com", oidcIdentityProvider.ObjectMeta), }, wantStatusUpdates: []*configv1alpha1.FederationDomain{ expectedFederationDomainStatusUpdate( &configv1alpha1.FederationDomain{ ObjectMeta: metav1.ObjectMeta{Name: "fd1", Namespace: namespace, Generation: 123}, }, configv1alpha1.FederationDomainPhaseError, replaceConditions( allHappyConditionsLegacyConfigurationSuccess("https://iSSueR-duPlicAte-adDress.cOm/path1", oidcIdentityProvider.Name, frozenMetav1Now, 123), []configv1alpha1.Condition{ sadOneTLSSecretPerIssuerHostnameCondition(frozenMetav1Now, 123), sadReadyCondition(frozenMetav1Now, 123), }), ), expectedFederationDomainStatusUpdate( &configv1alpha1.FederationDomain{ ObjectMeta: metav1.ObjectMeta{Name: "fd2", Namespace: namespace, Generation: 123}, }, configv1alpha1.FederationDomainPhaseError, replaceConditions( allHappyConditionsLegacyConfigurationSuccess("https://issuer-duplicate-address.com:1234/path2", oidcIdentityProvider.Name, frozenMetav1Now, 123), []configv1alpha1.Condition{ sadOneTLSSecretPerIssuerHostnameCondition(frozenMetav1Now, 123), sadReadyCondition(frozenMetav1Now, 123), }), ), expectedFederationDomainStatusUpdate( &configv1alpha1.FederationDomain{ ObjectMeta: metav1.ObjectMeta{Name: "invalidIssuerURLFederationDomain", Namespace: namespace, Generation: 123}, }, configv1alpha1.FederationDomainPhaseError, replaceConditions( allHappyConditionsLegacyConfigurationSuccess(invalidIssuerURL, oidcIdentityProvider.Name, frozenMetav1Now, 123), []configv1alpha1.Condition{ unknownIssuerIsUniqueCondition(frozenMetav1Now, 123), sadIssuerURLValidConditionCannotParse(frozenMetav1Now, 123), unknownOneTLSSecretPerIssuerHostnameCondition(frozenMetav1Now, 123), sadReadyCondition(frozenMetav1Now, 123), }), ), expectedFederationDomainStatusUpdate( &configv1alpha1.FederationDomain{ ObjectMeta: metav1.ObjectMeta{Name: "differentIssuerAddressFederationDomain", Namespace: namespace, Generation: 123}, }, configv1alpha1.FederationDomainPhaseReady, allHappyConditionsLegacyConfigurationSuccess("https://issuer-not-duplicate.com", oidcIdentityProvider.Name, frozenMetav1Now, 123), ), }, }, { name: "legacy config: no identity provider specified in federation domain and no identity providers found results in not found status", inputObjects: []runtime.Object{ federationDomain1, federationDomain2, }, wantFDIssuers: []*federationdomainproviders.FederationDomainIssuer{}, wantStatusUpdates: []*configv1alpha1.FederationDomain{ expectedFederationDomainStatusUpdate(federationDomain1, configv1alpha1.FederationDomainPhaseError, replaceConditions( allHappyConditionsLegacyConfigurationSuccess(federationDomain1.Spec.Issuer, "", frozenMetav1Now, 123), []configv1alpha1.Condition{ sadIdentityProvidersFoundConditionLegacyConfigurationIdentityProviderNotFound(frozenMetav1Now, 123), sadReadyCondition(frozenMetav1Now, 123), }), ), expectedFederationDomainStatusUpdate(federationDomain2, configv1alpha1.FederationDomainPhaseError, replaceConditions( allHappyConditionsLegacyConfigurationSuccess(federationDomain2.Spec.Issuer, "", frozenMetav1Now, 123), []configv1alpha1.Condition{ sadIdentityProvidersFoundConditionLegacyConfigurationIdentityProviderNotFound(frozenMetav1Now, 123), sadReadyCondition(frozenMetav1Now, 123), }), ), }, }, { name: "legacy config: no identity provider specified in federation domain and multiple identity providers found results in not specified status", inputObjects: []runtime.Object{ federationDomain1, oidcIdentityProvider, ldapIdentityProvider, adIdentityProvider, }, wantFDIssuers: []*federationdomainproviders.FederationDomainIssuer{}, wantStatusUpdates: []*configv1alpha1.FederationDomain{ expectedFederationDomainStatusUpdate(federationDomain1, configv1alpha1.FederationDomainPhaseError, replaceConditions( allHappyConditionsLegacyConfigurationSuccess(federationDomain1.Spec.Issuer, "", frozenMetav1Now, 123), []configv1alpha1.Condition{ sadIdentityProvidersFoundConditionIdentityProviderNotSpecified(3, frozenMetav1Now, 123), sadReadyCondition(frozenMetav1Now, 123), }), ), }, }, { name: "the federation domain specifies identity providers that cannot be found", inputObjects: []runtime.Object{ &configv1alpha1.FederationDomain{ ObjectMeta: metav1.ObjectMeta{Name: "config1", Namespace: namespace, Generation: 123}, Spec: configv1alpha1.FederationDomainSpec{ Issuer: "https://issuer1.com", IdentityProviders: []configv1alpha1.FederationDomainIdentityProvider{ { DisplayName: "cant-find-me", ObjectRef: corev1.TypedLocalObjectReference{ APIGroup: pointer.String(apiGroupSupervisor), Kind: "OIDCIdentityProvider", Name: "cant-find-me-name", }, }, { DisplayName: "cant-find-me-either", ObjectRef: corev1.TypedLocalObjectReference{ APIGroup: pointer.String(apiGroupSupervisor), Kind: "OIDCIdentityProvider", Name: "cant-find-me-either-name", }, }, { DisplayName: "cant-find-me-still", ObjectRef: corev1.TypedLocalObjectReference{ APIGroup: pointer.String(apiGroupSupervisor), Kind: "ActiveDirectoryIdentityProvider", Name: "cant-find-me-still-name", }, }, }, }, }, }, wantFDIssuers: []*federationdomainproviders.FederationDomainIssuer{}, wantStatusUpdates: []*configv1alpha1.FederationDomain{ expectedFederationDomainStatusUpdate( &configv1alpha1.FederationDomain{ ObjectMeta: metav1.ObjectMeta{Name: "config1", Namespace: namespace, Generation: 123}, }, configv1alpha1.FederationDomainPhaseError, replaceConditions( allHappyConditionsSuccess("https://issuer1.com", frozenMetav1Now, 123), []configv1alpha1.Condition{ sadIdentityProvidersFoundConditionIdentityProvidersObjectRefsNotFound( `.spec.identityProviders[0] with displayName "cant-find-me", `+ `.spec.identityProviders[1] with displayName "cant-find-me-either", `+ `.spec.identityProviders[2] with displayName "cant-find-me-still"`, frozenMetav1Now, 123), sadReadyCondition(frozenMetav1Now, 123), }), ), }, }, { name: "the federation domain specifies identity providers that all exist", inputObjects: []runtime.Object{ oidcIdentityProvider, ldapIdentityProvider, adIdentityProvider, &configv1alpha1.FederationDomain{ ObjectMeta: metav1.ObjectMeta{Name: "config1", Namespace: namespace, Generation: 123}, Spec: configv1alpha1.FederationDomainSpec{ Issuer: "https://issuer1.com", IdentityProviders: []configv1alpha1.FederationDomainIdentityProvider{ { DisplayName: "can-find-me", ObjectRef: corev1.TypedLocalObjectReference{ APIGroup: pointer.String(apiGroupSupervisor), Kind: "OIDCIdentityProvider", Name: oidcIdentityProvider.Name, }, }, { DisplayName: "can-find-me-too", ObjectRef: corev1.TypedLocalObjectReference{ APIGroup: pointer.String(apiGroupSupervisor), Kind: "LDAPIdentityProvider", Name: ldapIdentityProvider.Name, }, }, { DisplayName: "can-find-me-three", ObjectRef: corev1.TypedLocalObjectReference{ APIGroup: pointer.String(apiGroupSupervisor), Kind: "ActiveDirectoryIdentityProvider", Name: adIdentityProvider.Name, }, }, }, }, }, }, wantFDIssuers: []*federationdomainproviders.FederationDomainIssuer{ federationDomainIssuerWithIDPs(t, "https://issuer1.com", []*federationdomainproviders.FederationDomainIdentityProvider{ { DisplayName: "can-find-me", UID: oidcIdentityProvider.UID, Transforms: idtransform.NewTransformationPipeline(), }, { DisplayName: "can-find-me-too", UID: ldapIdentityProvider.UID, Transforms: idtransform.NewTransformationPipeline(), }, { DisplayName: "can-find-me-three", UID: adIdentityProvider.UID, Transforms: idtransform.NewTransformationPipeline(), }, }), }, wantStatusUpdates: []*configv1alpha1.FederationDomain{ expectedFederationDomainStatusUpdate( &configv1alpha1.FederationDomain{ ObjectMeta: metav1.ObjectMeta{Name: "config1", Namespace: namespace, Generation: 123}, }, configv1alpha1.FederationDomainPhaseReady, allHappyConditionsSuccess("https://issuer1.com", frozenMetav1Now, 123), ), }, }, { name: "the federation domain has duplicate display names for IDPs", inputObjects: []runtime.Object{ oidcIdentityProvider, ldapIdentityProvider, adIdentityProvider, &configv1alpha1.FederationDomain{ ObjectMeta: metav1.ObjectMeta{Name: "config1", Namespace: namespace, Generation: 123}, Spec: configv1alpha1.FederationDomainSpec{ Issuer: "https://issuer1.com", IdentityProviders: []configv1alpha1.FederationDomainIdentityProvider{ { DisplayName: "duplicate1", ObjectRef: corev1.TypedLocalObjectReference{ APIGroup: pointer.String(apiGroupSupervisor), Kind: "OIDCIdentityProvider", Name: oidcIdentityProvider.Name, }, }, { DisplayName: "duplicate1", ObjectRef: corev1.TypedLocalObjectReference{ APIGroup: pointer.String(apiGroupSupervisor), Kind: "LDAPIdentityProvider", Name: ldapIdentityProvider.Name, }, }, { DisplayName: "duplicate1", ObjectRef: corev1.TypedLocalObjectReference{ APIGroup: pointer.String(apiGroupSupervisor), Kind: "LDAPIdentityProvider", Name: ldapIdentityProvider.Name, }, }, { DisplayName: "unique", ObjectRef: corev1.TypedLocalObjectReference{ APIGroup: pointer.String(apiGroupSupervisor), Kind: "ActiveDirectoryIdentityProvider", Name: adIdentityProvider.Name, }, }, { DisplayName: "duplicate2", ObjectRef: corev1.TypedLocalObjectReference{ APIGroup: pointer.String(apiGroupSupervisor), Kind: "LDAPIdentityProvider", Name: ldapIdentityProvider.Name, }, }, { DisplayName: "duplicate2", ObjectRef: corev1.TypedLocalObjectReference{ APIGroup: pointer.String(apiGroupSupervisor), Kind: "ActiveDirectoryIdentityProvider", Name: adIdentityProvider.Name, }, }, }, }, }, }, wantFDIssuers: []*federationdomainproviders.FederationDomainIssuer{}, wantStatusUpdates: []*configv1alpha1.FederationDomain{ expectedFederationDomainStatusUpdate( &configv1alpha1.FederationDomain{ ObjectMeta: metav1.ObjectMeta{Name: "config1", Namespace: namespace, Generation: 123}, }, configv1alpha1.FederationDomainPhaseError, replaceConditions( allHappyConditionsSuccess("https://issuer1.com", frozenMetav1Now, 123), []configv1alpha1.Condition{ sadDisplayNamesUniqueCondition(`"duplicate1", "duplicate2"`, frozenMetav1Now, 123), sadReadyCondition(frozenMetav1Now, 123), }), ), }, }, { name: "the federation domain has unrecognized api group names in objectRefs", inputObjects: []runtime.Object{ oidcIdentityProvider, ldapIdentityProvider, adIdentityProvider, &configv1alpha1.FederationDomain{ ObjectMeta: metav1.ObjectMeta{Name: "config1", Namespace: namespace, Generation: 123}, Spec: configv1alpha1.FederationDomainSpec{ Issuer: "https://issuer1.com", IdentityProviders: []configv1alpha1.FederationDomainIdentityProvider{ { DisplayName: "name1", ObjectRef: corev1.TypedLocalObjectReference{ APIGroup: pointer.String("wrong.example.com"), Kind: "OIDCIdentityProvider", Name: oidcIdentityProvider.Name, }, }, { DisplayName: "name2", ObjectRef: corev1.TypedLocalObjectReference{ APIGroup: pointer.String(""), // empty string is wrong Kind: "LDAPIdentityProvider", Name: ldapIdentityProvider.Name, }, }, { DisplayName: "name3", ObjectRef: corev1.TypedLocalObjectReference{ APIGroup: nil, // nil is wrong, and gets treated like an empty string in the error condition Kind: "LDAPIdentityProvider", Name: ldapIdentityProvider.Name, }, }, { DisplayName: "name4", ObjectRef: corev1.TypedLocalObjectReference{ APIGroup: pointer.String(apiGroupSupervisor), // correct Kind: "ActiveDirectoryIdentityProvider", Name: adIdentityProvider.Name, }, }, }, }, }, }, wantFDIssuers: []*federationdomainproviders.FederationDomainIssuer{}, wantStatusUpdates: []*configv1alpha1.FederationDomain{ expectedFederationDomainStatusUpdate( &configv1alpha1.FederationDomain{ ObjectMeta: metav1.ObjectMeta{Name: "config1", Namespace: namespace, Generation: 123}, }, configv1alpha1.FederationDomainPhaseError, replaceConditions( allHappyConditionsSuccess("https://issuer1.com", frozenMetav1Now, 123), []configv1alpha1.Condition{ sadAPIGroupSuffixCondition(`"", "", "wrong.example.com"`, frozenMetav1Now, 123), sadIdentityProvidersFoundConditionIdentityProvidersObjectRefsNotFound( `.spec.identityProviders[0] with displayName "name1", `+ `.spec.identityProviders[1] with displayName "name2", `+ `.spec.identityProviders[2] with displayName "name3"`, frozenMetav1Now, 123), sadReadyCondition(frozenMetav1Now, 123), }), ), }, }, { name: "the federation domain has unrecognized kind names in objectRefs", inputObjects: []runtime.Object{ oidcIdentityProvider, ldapIdentityProvider, adIdentityProvider, &configv1alpha1.FederationDomain{ ObjectMeta: metav1.ObjectMeta{Name: "config1", Namespace: namespace, Generation: 123}, Spec: configv1alpha1.FederationDomainSpec{ Issuer: "https://issuer1.com", IdentityProviders: []configv1alpha1.FederationDomainIdentityProvider{ { DisplayName: "name1", ObjectRef: corev1.TypedLocalObjectReference{ APIGroup: pointer.String(apiGroupSupervisor), Kind: "OIDCIdentityProvider", // correct Name: oidcIdentityProvider.Name, }, }, { DisplayName: "name2", ObjectRef: corev1.TypedLocalObjectReference{ APIGroup: pointer.String(apiGroupSupervisor), Kind: "wrong", Name: ldapIdentityProvider.Name, }, }, { DisplayName: "name3", ObjectRef: corev1.TypedLocalObjectReference{ APIGroup: pointer.String(apiGroupSupervisor), Kind: "", // empty is also wrong Name: ldapIdentityProvider.Name, }, }, }, }, }, }, wantFDIssuers: []*federationdomainproviders.FederationDomainIssuer{}, wantStatusUpdates: []*configv1alpha1.FederationDomain{ expectedFederationDomainStatusUpdate( &configv1alpha1.FederationDomain{ ObjectMeta: metav1.ObjectMeta{Name: "config1", Namespace: namespace, Generation: 123}, }, configv1alpha1.FederationDomainPhaseError, replaceConditions( allHappyConditionsSuccess("https://issuer1.com", frozenMetav1Now, 123), []configv1alpha1.Condition{ sadKindCondition(`"", "wrong"`, frozenMetav1Now, 123), sadIdentityProvidersFoundConditionIdentityProvidersObjectRefsNotFound( `.spec.identityProviders[1] with displayName "name2", `+ `.spec.identityProviders[2] with displayName "name3"`, frozenMetav1Now, 123), sadReadyCondition(frozenMetav1Now, 123), }), ), }, }, { name: "the federation domain has duplicate transformation const names", inputObjects: []runtime.Object{ oidcIdentityProvider, &configv1alpha1.FederationDomain{ ObjectMeta: metav1.ObjectMeta{Name: "config1", Namespace: namespace, Generation: 123}, Spec: configv1alpha1.FederationDomainSpec{ Issuer: "https://issuer1.com", IdentityProviders: []configv1alpha1.FederationDomainIdentityProvider{ { DisplayName: "name1", ObjectRef: corev1.TypedLocalObjectReference{ APIGroup: pointer.String(apiGroupSupervisor), Kind: "OIDCIdentityProvider", Name: oidcIdentityProvider.Name, }, Transforms: configv1alpha1.FederationDomainTransforms{ Constants: []configv1alpha1.FederationDomainTransformsConstant{ {Name: "duplicate1", Type: "string", StringValue: "abc"}, {Name: "duplicate1", Type: "string", StringValue: "def"}, {Name: "duplicate1", Type: "string", StringValue: "efg"}, {Name: "duplicate2", Type: "string", StringValue: "123"}, {Name: "duplicate2", Type: "string", StringValue: "456"}, {Name: "uniqueName", Type: "string", StringValue: "hij"}, }, }, }, }, }, }, }, wantFDIssuers: []*federationdomainproviders.FederationDomainIssuer{}, wantStatusUpdates: []*configv1alpha1.FederationDomain{ expectedFederationDomainStatusUpdate( &configv1alpha1.FederationDomain{ ObjectMeta: metav1.ObjectMeta{Name: "config1", Namespace: namespace, Generation: 123}, }, configv1alpha1.FederationDomainPhaseError, replaceConditions( allHappyConditionsSuccess("https://issuer1.com", frozenMetav1Now, 123), []configv1alpha1.Condition{ sadConstNamesUniqueCondition(`"duplicate1", "duplicate2"`, frozenMetav1Now, 123), sadReadyCondition(frozenMetav1Now, 123), }), ), }, }, { name: "the federation domain has transformation expressions which don't compile", inputObjects: []runtime.Object{ oidcIdentityProvider, &configv1alpha1.FederationDomain{ ObjectMeta: metav1.ObjectMeta{Name: "config1", Namespace: namespace, Generation: 123}, Spec: configv1alpha1.FederationDomainSpec{ Issuer: "https://issuer1.com", IdentityProviders: []configv1alpha1.FederationDomainIdentityProvider{ { DisplayName: "name1", ObjectRef: corev1.TypedLocalObjectReference{ APIGroup: pointer.String(apiGroupSupervisor), Kind: "OIDCIdentityProvider", Name: oidcIdentityProvider.Name, }, Transforms: configv1alpha1.FederationDomainTransforms{ Expressions: []configv1alpha1.FederationDomainTransformsExpression{ {Type: "username/v1", Expression: "this is not a valid cel expression"}, {Type: "groups/v1", Expression: "this is also not a valid cel expression"}, {Type: "username/v1", Expression: "username"}, // valid {Type: "policy/v1", Expression: "still not a valid cel expression"}, }, }, }, }, }, }, }, wantFDIssuers: []*federationdomainproviders.FederationDomainIssuer{}, wantStatusUpdates: []*configv1alpha1.FederationDomain{ expectedFederationDomainStatusUpdate( &configv1alpha1.FederationDomain{ ObjectMeta: metav1.ObjectMeta{Name: "config1", Namespace: namespace, Generation: 123}, }, configv1alpha1.FederationDomainPhaseError, replaceConditions( allHappyConditionsSuccess("https://issuer1.com", frozenMetav1Now, 123), []configv1alpha1.Condition{ sadTransformationExpressionsCondition( here.Doc( `spec.identityProvider[0].transforms.expressions[0].expression was invalid: CEL expression compile error: ERROR: :1:6: Syntax error: mismatched input 'is' expecting | this is not a valid cel expression | .....^ spec.identityProvider[0].transforms.expressions[1].expression was invalid: CEL expression compile error: ERROR: :1:6: Syntax error: mismatched input 'is' expecting | this is also not a valid cel expression | .....^ spec.identityProvider[0].transforms.expressions[3].expression was invalid: CEL expression compile error: ERROR: :1:7: Syntax error: mismatched input 'not' expecting | still not a valid cel expression | ......^`, ), frozenMetav1Now, 123), unknownTransformationExamplesCondition(frozenMetav1Now, 123), sadReadyCondition(frozenMetav1Now, 123), }), ), }, }, { name: "the federation domain has transformation examples which don't pass", inputObjects: []runtime.Object{ oidcIdentityProvider, &configv1alpha1.FederationDomain{ ObjectMeta: metav1.ObjectMeta{Name: "config1", Namespace: namespace, Generation: 123}, Spec: configv1alpha1.FederationDomainSpec{ Issuer: "https://issuer1.com", IdentityProviders: []configv1alpha1.FederationDomainIdentityProvider{ { DisplayName: "name1", ObjectRef: corev1.TypedLocalObjectReference{ APIGroup: pointer.String(apiGroupSupervisor), Kind: "OIDCIdentityProvider", Name: oidcIdentityProvider.Name, }, Transforms: configv1alpha1.FederationDomainTransforms{ Expressions: []configv1alpha1.FederationDomainTransformsExpression{ {Type: "policy/v1", Expression: `username == "ryan" || username == "rejectMeWithDefaultMessage"`, Message: "only ryan allowed"}, {Type: "policy/v1", Expression: `username != "rejectMeWithDefaultMessage"`}, // no message specified {Type: "username/v1", Expression: `"pre:" + username`}, {Type: "groups/v1", Expression: `groups.map(g, "pre:" + g)`}, }, Examples: []configv1alpha1.FederationDomainTransformsExample{ { // this example should pass Username: "ryan", Groups: []string{"a", "b"}, Expects: configv1alpha1.FederationDomainTransformsExampleExpects{ Username: "pre:ryan", Groups: []string{"pre:b", "pre:a", "pre:b", "pre:a"}, // order and repeats don't matter, treated like a set Rejected: false, }, }, { // this example should pass Username: "other", Expects: configv1alpha1.FederationDomainTransformsExampleExpects{ Rejected: true, Message: "only ryan allowed", }, }, { // this example should fail because it expects the user to be rejected but the user was actually not rejected Username: "ryan", Groups: []string{"a", "b"}, Expects: configv1alpha1.FederationDomainTransformsExampleExpects{ Rejected: true, Message: "this input is ignored in this case", }, }, { // this example should fail because it expects the user not to be rejected but they were actually rejected Username: "other", Groups: []string{"a", "b"}, Expects: configv1alpha1.FederationDomainTransformsExampleExpects{ Username: "pre:other", Groups: []string{"pre:a", "pre:b"}, Rejected: false, }, }, { // this example should fail because it expects the wrong rejection message Username: "other", Groups: []string{"a", "b"}, Expects: configv1alpha1.FederationDomainTransformsExampleExpects{ Rejected: true, Message: "wrong message", }, }, { // this example should pass even though it does not make any assertion about the rejection message // because the message assertions defaults to asserting the default rejection message Username: "rejectMeWithDefaultMessage", Groups: []string{"a", "b"}, Expects: configv1alpha1.FederationDomainTransformsExampleExpects{ Rejected: true, }, }, { // this example should fail because it expects both the wrong username and groups Username: "ryan", Groups: []string{"b", "a"}, Expects: configv1alpha1.FederationDomainTransformsExampleExpects{ Username: "wrong", Groups: []string{}, Rejected: false, }, }, { // this example should fail because it expects the wrong username only Username: "ryan", Groups: []string{"a", "b"}, Expects: configv1alpha1.FederationDomainTransformsExampleExpects{ Username: "wrong", Groups: []string{"pre:b", "pre:a"}, Rejected: false, }, }, { // this example should fail because it expects the wrong groups only Username: "ryan", Groups: []string{"b", "a"}, Expects: configv1alpha1.FederationDomainTransformsExampleExpects{ Username: "pre:ryan", Groups: []string{"wrong2", "wrong1"}, Rejected: false, }, }, { // this example should fail because it does not expect anything but the auth actually was successful Username: "ryan", Groups: []string{"b", "a"}, Expects: configv1alpha1.FederationDomainTransformsExampleExpects{}, }, }, }, }, }, }, }, }, wantFDIssuers: []*federationdomainproviders.FederationDomainIssuer{}, wantStatusUpdates: []*configv1alpha1.FederationDomain{ expectedFederationDomainStatusUpdate( &configv1alpha1.FederationDomain{ ObjectMeta: metav1.ObjectMeta{Name: "config1", Namespace: namespace, Generation: 123}, }, configv1alpha1.FederationDomainPhaseError, replaceConditions( allHappyConditionsSuccess("https://issuer1.com", frozenMetav1Now, 123), []configv1alpha1.Condition{ sadTransformationExamplesCondition( here.Doc( `.spec.identityProviders[0].transforms.examples[2] example failed: expected: authentication to be rejected actual: authentication was not rejected .spec.identityProviders[0].transforms.examples[3] example failed: expected: authentication not to be rejected actual: authentication was rejected with message "only ryan allowed" .spec.identityProviders[0].transforms.examples[4] example failed: expected: authentication rejection message "wrong message" actual: authentication rejection message "only ryan allowed" .spec.identityProviders[0].transforms.examples[6] example failed: expected: username "wrong" actual: username "pre:ryan" .spec.identityProviders[0].transforms.examples[6] example failed: expected: groups [] actual: groups ["pre:a", "pre:b"] .spec.identityProviders[0].transforms.examples[7] example failed: expected: username "wrong" actual: username "pre:ryan" .spec.identityProviders[0].transforms.examples[8] example failed: expected: groups ["wrong1", "wrong2"] actual: groups ["pre:a", "pre:b"] .spec.identityProviders[0].transforms.examples[9] example failed: expected: username "" actual: username "pre:ryan" .spec.identityProviders[0].transforms.examples[9] example failed: expected: groups [] actual: groups ["pre:a", "pre:b"]`, ), frozenMetav1Now, 123), sadReadyCondition(frozenMetav1Now, 123), }), ), }, }, { name: "the federation domain has transformation expressions that return illegal values with examples which exercise them", inputObjects: []runtime.Object{ oidcIdentityProvider, &configv1alpha1.FederationDomain{ ObjectMeta: metav1.ObjectMeta{Name: "config1", Namespace: namespace, Generation: 123}, Spec: configv1alpha1.FederationDomainSpec{ Issuer: "https://issuer1.com", IdentityProviders: []configv1alpha1.FederationDomainIdentityProvider{ { DisplayName: "name1", ObjectRef: corev1.TypedLocalObjectReference{ APIGroup: pointer.String(apiGroupSupervisor), Kind: "OIDCIdentityProvider", Name: oidcIdentityProvider.Name, }, Transforms: configv1alpha1.FederationDomainTransforms{ Expressions: []configv1alpha1.FederationDomainTransformsExpression{ {Type: "username/v1", Expression: `username == "ryan" ? "" : username`}, // not allowed to return an empty string as the transformed username }, Examples: []configv1alpha1.FederationDomainTransformsExample{ { // every example which encounters an unexpected error should fail because the transformation pipeline returned an error Username: "ryan", Groups: []string{"a", "b"}, Expects: configv1alpha1.FederationDomainTransformsExampleExpects{}, }, { // every example which encounters an unexpected error should fail because the transformation pipeline returned an error Username: "ryan", Groups: []string{"a", "b"}, Expects: configv1alpha1.FederationDomainTransformsExampleExpects{}, }, { // this should pass Username: "other", Groups: []string{"a", "b"}, Expects: configv1alpha1.FederationDomainTransformsExampleExpects{ Username: "other", Groups: []string{"a", "b"}, Rejected: false, }, }, }, }, }, }, }, }, }, wantFDIssuers: []*federationdomainproviders.FederationDomainIssuer{}, wantStatusUpdates: []*configv1alpha1.FederationDomain{ expectedFederationDomainStatusUpdate( &configv1alpha1.FederationDomain{ ObjectMeta: metav1.ObjectMeta{Name: "config1", Namespace: namespace, Generation: 123}, }, configv1alpha1.FederationDomainPhaseError, replaceConditions( allHappyConditionsSuccess("https://issuer1.com", frozenMetav1Now, 123), []configv1alpha1.Condition{ sadTransformationExamplesCondition( here.Doc( `.spec.identityProviders[0].transforms.examples[0] example failed: expected: no transformation errors actual: transformations resulted in an unexpected error "identity transformation returned an empty username, which is not allowed" .spec.identityProviders[0].transforms.examples[1] example failed: expected: no transformation errors actual: transformations resulted in an unexpected error "identity transformation returned an empty username, which is not allowed"`, ), frozenMetav1Now, 123), sadReadyCondition(frozenMetav1Now, 123), }), ), }, }, { name: "the federation domain specifies illegal const type, which shouldn't really happen since the CRD validates it", inputObjects: []runtime.Object{ oidcIdentityProvider, &configv1alpha1.FederationDomain{ ObjectMeta: metav1.ObjectMeta{Name: "config1", Namespace: namespace, Generation: 123}, Spec: configv1alpha1.FederationDomainSpec{ Issuer: "https://issuer1.com", IdentityProviders: []configv1alpha1.FederationDomainIdentityProvider{ { DisplayName: "can-find-me", ObjectRef: corev1.TypedLocalObjectReference{ APIGroup: pointer.String(apiGroupSupervisor), Kind: "OIDCIdentityProvider", Name: oidcIdentityProvider.Name, }, Transforms: configv1alpha1.FederationDomainTransforms{ Constants: []configv1alpha1.FederationDomainTransformsConstant{ { Type: "this is illegal", }, }, }, }, }, }, }, }, wantErr: `one of spec.identityProvider[].transforms.constants[].type is invalid: "this is illegal"`, }, { name: "the federation domain specifies illegal expression type, which shouldn't really happen since the CRD validates it", inputObjects: []runtime.Object{ oidcIdentityProvider, &configv1alpha1.FederationDomain{ ObjectMeta: metav1.ObjectMeta{Name: "config1", Namespace: namespace, Generation: 123}, Spec: configv1alpha1.FederationDomainSpec{ Issuer: "https://issuer1.com", IdentityProviders: []configv1alpha1.FederationDomainIdentityProvider{ { DisplayName: "can-find-me", ObjectRef: corev1.TypedLocalObjectReference{ APIGroup: pointer.String(apiGroupSupervisor), Kind: "OIDCIdentityProvider", Name: oidcIdentityProvider.Name, }, Transforms: configv1alpha1.FederationDomainTransforms{ Expressions: []configv1alpha1.FederationDomainTransformsExpression{ { Type: "this is illegal", }, }, }, }, }, }, }, }, wantErr: `one of spec.identityProvider[].transforms.expressions[].type is invalid: "this is illegal"`, }, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() federationDomainsSetter := &fakeFederationDomainsSetter{} pinnipedAPIClient := pinnipedfake.NewSimpleClientset() pinnipedInformerClient := pinnipedfake.NewSimpleClientset() for _, o := range tt.inputObjects { require.NoError(t, pinnipedAPIClient.Tracker().Add(o)) require.NoError(t, pinnipedInformerClient.Tracker().Add(o)) } if tt.configClient != nil { tt.configClient(pinnipedAPIClient) } pinnipedInformers := pinnipedinformers.NewSharedInformerFactory(pinnipedInformerClient, 0) controller := NewFederationDomainWatcherController( federationDomainsSetter, apiGroupSuffix, clocktesting.NewFakeClock(frozenNow), pinnipedAPIClient, pinnipedInformers.Config().V1alpha1().FederationDomains(), pinnipedInformers.IDP().V1alpha1().OIDCIdentityProviders(), pinnipedInformers.IDP().V1alpha1().LDAPIdentityProviders(), pinnipedInformers.IDP().V1alpha1().ActiveDirectoryIdentityProviders(), controllerlib.WithInformer, ) ctx, cancel := context.WithCancel(context.Background()) defer cancel() pinnipedInformers.Start(ctx.Done()) controllerlib.TestRunSynchronously(t, controller) syncCtx := controllerlib.Context{Context: ctx, Key: controllerlib.Key{Namespace: namespace, Name: "config-name"}} if err := controllerlib.TestSync(t, controller, syncCtx); tt.wantErr != "" { require.EqualError(t, err, tt.wantErr) } else { require.NoError(t, err) } if tt.wantFDIssuers != nil { require.True(t, federationDomainsSetter.SetFederationDomainsWasCalled) require.ElementsMatch(t, tt.wantFDIssuers, federationDomainsSetter.FederationDomainsReceived) } else { require.False(t, federationDomainsSetter.SetFederationDomainsWasCalled) } if tt.wantStatusUpdates != nil { // This controller should only perform updates to FederationDomain statuses. // In this controller we don't actually care about the order of the actions, since the FederationDomains // statuses can be updated in any order. Therefore, we are sorting here so we can use require.Equal // to make the test output easier to read. Unfortunately the timezone nested in the condition can still // make the test failure diffs ugly sometimes, but we do want to assert about timestamps so there's not // much we can do about those. actualFederationDomainUpdates := getFederationDomainStatusUpdates(t, pinnipedAPIClient.Actions()) sortFederationDomainsByName(actualFederationDomainUpdates) sortFederationDomainsByName(tt.wantStatusUpdates) // Use require.Equal instead of require.ElementsMatch because require.Equal prints a nice diff. require.Equal(t, tt.wantStatusUpdates, actualFederationDomainUpdates) } else { require.Empty(t, pinnipedAPIClient.Actions()) } }) } } func expectedFederationDomainStatusUpdate( fd *configv1alpha1.FederationDomain, phase configv1alpha1.FederationDomainPhase, conditions []configv1alpha1.Condition, ) *configv1alpha1.FederationDomain { fdCopy := fd.DeepCopy() // We don't care about the spec of a FederationDomain in an update status action, // so clear it out to make it easier to write expected values. fdCopy.Spec = configv1alpha1.FederationDomainSpec{} fdCopy.Status.Phase = phase fdCopy.Status.Conditions = conditions return fdCopy } func getFederationDomainStatusUpdates(t *testing.T, actions []coretesting.Action) []*configv1alpha1.FederationDomain { federationDomains := []*configv1alpha1.FederationDomain{} for _, action := range actions { updateAction, ok := action.(coretesting.UpdateAction) require.True(t, ok, "failed to cast an action as an coretesting.UpdateAction: %#v", action) require.Equal(t, federationDomainGVR, updateAction.GetResource(), "an update action should have updated a FederationDomain but updated something else") require.Equal(t, "status", updateAction.GetSubresource(), "an update action should have updated the status subresource but updated something else") fd, ok := updateAction.GetObject().(*configv1alpha1.FederationDomain) require.True(t, ok, "failed to cast an action's object as a FederationDomain: %#v", updateAction.GetObject()) require.Equal(t, fd.Namespace, updateAction.GetNamespace(), "an update action might have been called on the wrong namespace for a FederationDomain") // We don't care about the spec of a FederationDomain in an update status action, // so clear it out to make it easier to write expected values. copyOfFD := fd.DeepCopy() copyOfFD.Spec = configv1alpha1.FederationDomainSpec{} federationDomains = append(federationDomains, copyOfFD) } return federationDomains } func sortFederationDomainsByName(federationDomains []*configv1alpha1.FederationDomain) { sort.SliceStable(federationDomains, func(a, b int) bool { return federationDomains[a].GetName() < federationDomains[b].GetName() }) }