ContainerImage.Pinniped/internal/controller/supervisorconfig/federation_domain_watcher_test.go
Ryan Richard c771328bb1 Validate transforms examples in federation_domain_watcher.go
Also changes the transformation pipeline code to sort and uniq
the transformed group names at the end of the pipeline. This makes
the results more predicable without changing the semantics.
2023-09-11 11:14:05 -07:00

1792 lines
74 KiB
Go

// 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: <input>:1:6: Syntax error: mismatched input 'is' expecting <EOF>
| this is not a valid cel expression
| .....^
spec.identityProvider[0].transforms.expressions[1].expression was invalid:
CEL expression compile error: ERROR: <input>:1:6: Syntax error: mismatched input 'is' expecting <EOF>
| this is also not a valid cel expression
| .....^
spec.identityProvider[0].transforms.expressions[3].expression was invalid:
CEL expression compile error: ERROR: <input>:1:7: Syntax error: mismatched input 'not' expecting <EOF>
| 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()
})
}