ContainerImage.Pinniped/internal/controller/supervisorconfig/federation_domain_watcher_test.go

2294 lines
94 KiB
Go
Raw Permalink Normal View History

// 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"
"k8s.io/apimachinery/pkg/types"
coretesting "k8s.io/client-go/testing"
Update all deps to latest where possible, bump Kube deps to v0.23.1 Highlights from this dep bump: 1. Made a copy of the v0.4.0 github.com/go-logr/stdr implementation for use in tests. We must bump this dep as Kube code uses a newer version now. We would have to rewrite hundreds of test log assertions without this copy. 2. Use github.com/felixge/httpsnoop to undo the changes made by ory/fosite#636 for CLI based login flows. This is required for backwards compatibility with older versions of our CLI. A separate change after this will update the CLI to be more flexible (it is purposefully not part of this change to confirm that we did not break anything). For all browser login flows, we now redirect using http.StatusSeeOther instead of http.StatusFound. 3. Drop plog.RemoveKlogGlobalFlags as klog no longer mutates global process flags 4. Only bump github.com/ory/x to v0.0.297 instead of the latest v0.0.321 because v0.0.298+ pulls in a newer version of go.opentelemetry.io/otel/semconv which breaks k8s.io/apiserver. We should update k8s.io/apiserver to use the newer code. 5. Migrate all code from k8s.io/apimachinery/pkg/util/clock to k8s.io/utils/clock and k8s.io/utils/clock/testing 6. Delete testutil.NewDeleteOptionsRecorder and migrate to the new kubetesting.NewDeleteActionWithOptions 7. Updated ExpectedAuthorizeCodeSessionJSONFromFuzzing caused by fosite's new rotated_secrets OAuth client field. This new field is currently not relevant to us as we have no private clients. Signed-off-by: Monis Khan <mok@vmware.com>
2021-12-10 22:22:36 +00:00
clocktesting "k8s.io/utils/clock/testing"
"k8s.io/utils/ptr"
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/celtransformer"
"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) metav1.Condition {
return metav1.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) metav1.Condition {
return metav1.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) metav1.Condition {
return metav1.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) metav1.Condition {
return metav1.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) metav1.Condition {
return metav1.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) metav1.Condition {
return metav1.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) metav1.Condition {
return metav1.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) metav1.Condition {
return metav1.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) metav1.Condition {
return metav1.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) metav1.Condition {
return metav1.Condition{
Type: "IssuerURLValid",
Status: "False",
ObservedGeneration: observedGeneration,
LastTransitionTime: time,
Reason: "InvalidIssuerURL",
Message: "issuer must not have query",
}
}
sadIssuerURLValidConditionCannotParse := func(time metav1.Time, observedGeneration int64) metav1.Condition {
return metav1.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) metav1.Condition {
return metav1.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) metav1.Condition {
return metav1.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) metav1.Condition {
return metav1.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) metav1.Condition {
return metav1.Condition{
Type: "IdentityProvidersFound",
Status: "False",
ObservedGeneration: observedGeneration,
LastTransitionTime: time,
Reason: "IdentityProviderNotSpecified",
Message: fmt.Sprintf("no resources were specified by .spec.identityProviders[].objectRef "+
"and %d identity provider resources have been found: "+
"please update .spec.identityProviders to specify which identity providers "+
"this federation domain should use", idpCRsCount),
}
}
sadIdentityProvidersFoundConditionIdentityProvidersObjectRefsNotFound := func(errorMessages string, time metav1.Time, observedGeneration int64) metav1.Condition {
return metav1.Condition{
Type: "IdentityProvidersFound",
Status: "False",
ObservedGeneration: observedGeneration,
LastTransitionTime: time,
Reason: "IdentityProvidersObjectRefsNotFound",
Message: errorMessages,
}
}
happyDisplayNamesUniqueCondition := func(time metav1.Time, observedGeneration int64) metav1.Condition {
return metav1.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) metav1.Condition {
return metav1.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),
}
}
happyTransformationExpressionsCondition := func(time metav1.Time, observedGeneration int64) metav1.Condition {
return metav1.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) metav1.Condition {
return metav1.Condition{
Type: "TransformsExpressionsValid",
Status: "False",
ObservedGeneration: observedGeneration,
LastTransitionTime: time,
Reason: "InvalidTransformsExpressions",
Message: errorMessages,
}
}
happyTransformationExamplesCondition := func(time metav1.Time, observedGeneration int64) metav1.Condition {
return metav1.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) metav1.Condition {
return metav1.Condition{
Type: "TransformsExamplesPassed",
Status: "False",
ObservedGeneration: observedGeneration,
LastTransitionTime: time,
Reason: "TransformsExamplesFailed",
Message: errorMessages,
}
}
happyAPIGroupSuffixCondition := func(time metav1.Time, observedGeneration int64) metav1.Condition {
return metav1.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) metav1.Condition {
return metav1.Condition{
Type: "IdentityProvidersObjectRefAPIGroupSuffixValid",
Status: "False",
ObservedGeneration: observedGeneration,
LastTransitionTime: time,
Reason: "APIGroupUnrecognized",
Message: fmt.Sprintf("some 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) metav1.Condition {
return metav1.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) metav1.Condition {
return metav1.Condition{
Type: "IdentityProvidersObjectRefKindValid",
Status: "False",
ObservedGeneration: observedGeneration,
LastTransitionTime: time,
Reason: "KindUnrecognized",
Message: fmt.Sprintf(`some kinds specified by .spec.identityProviders[].objectRef.kind are `+
`not recognized (should be one of "ActiveDirectoryIdentityProvider", "LDAPIdentityProvider", "OIDCIdentityProvider"): %s`, badKinds),
}
}
sortConditionsByType := func(c []metav1.Condition) []metav1.Condition {
cp := make([]metav1.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 []metav1.Condition, sadConditions []metav1.Condition) []metav1.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) []metav1.Condition {
return sortConditionsByType([]metav1.Condition{
happyTransformationExamplesCondition(frozenMetav1Now, 123),
happyTransformationExpressionsCondition(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) []metav1.Condition {
return replaceConditions(
allHappyConditionsSuccess(issuer, time, observedGeneration),
[]metav1.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 OIDC 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: "legacy config: when no identity provider is specified on federation domains, but exactly one LDAP 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,
ldapIdentityProvider,
},
wantFDIssuers: []*federationdomainproviders.FederationDomainIssuer{
federationDomainIssuerWithDefaultIDP(t, federationDomain1.Spec.Issuer, ldapIdentityProvider.ObjectMeta),
federationDomainIssuerWithDefaultIDP(t, federationDomain2.Spec.Issuer, ldapIdentityProvider.ObjectMeta),
},
wantStatusUpdates: []*configv1alpha1.FederationDomain{
expectedFederationDomainStatusUpdate(federationDomain1,
configv1alpha1.FederationDomainPhaseReady,
allHappyConditionsLegacyConfigurationSuccess(federationDomain1.Spec.Issuer, ldapIdentityProvider.Name, frozenMetav1Now, 123),
),
expectedFederationDomainStatusUpdate(federationDomain2,
configv1alpha1.FederationDomainPhaseReady,
allHappyConditionsLegacyConfigurationSuccess(federationDomain2.Spec.Issuer, ldapIdentityProvider.Name, frozenMetav1Now, 123),
),
},
},
{
name: "legacy config: when no identity provider is specified on federation domains, but exactly one AD 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,
adIdentityProvider,
},
wantFDIssuers: []*federationdomainproviders.FederationDomainIssuer{
federationDomainIssuerWithDefaultIDP(t, federationDomain1.Spec.Issuer, adIdentityProvider.ObjectMeta),
federationDomainIssuerWithDefaultIDP(t, federationDomain2.Spec.Issuer, adIdentityProvider.ObjectMeta),
},
wantStatusUpdates: []*configv1alpha1.FederationDomain{
expectedFederationDomainStatusUpdate(federationDomain1,
configv1alpha1.FederationDomainPhaseReady,
allHappyConditionsLegacyConfigurationSuccess(federationDomain1.Spec.Issuer, adIdentityProvider.Name, frozenMetav1Now, 123),
),
expectedFederationDomainStatusUpdate(federationDomain2,
configv1alpha1.FederationDomainPhaseReady,
allHappyConditionsLegacyConfigurationSuccess(federationDomain2.Spec.Issuer, adIdentityProvider.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 the status of the FederationDomains is based on an old generation, it is updated",
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,
2, // this is an older generation
),
},
},
},
wantFDIssuers: []*federationdomainproviders.FederationDomainIssuer{
federationDomainIssuerWithDefaultIDP(t, federationDomain1.Spec.Issuer, oidcIdentityProvider.ObjectMeta),
},
wantStatusUpdates: []*configv1alpha1.FederationDomain{
// only one update, because the other FederationDomain already had the right status
expectedFederationDomainStatusUpdate(federationDomain1,
configv1alpha1.FederationDomainPhaseReady,
allHappyConditionsLegacyConfigurationSuccess(
federationDomain1.Spec.Issuer,
oidcIdentityProvider.Name,
frozenMetav1Now,
123, // all conditions are updated to the new observed generation
),
),
},
},
{
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),
[]metav1.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),
[]metav1.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),
[]metav1.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),
[]metav1.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),
[]metav1.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),
[]metav1.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),
[]metav1.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),
[]metav1.Condition{
sadIdentityProvidersFoundConditionLegacyConfigurationIdentityProviderNotFound(frozenMetav1Now, 123),
sadReadyCondition(frozenMetav1Now, 123),
}),
),
expectedFederationDomainStatusUpdate(federationDomain2,
configv1alpha1.FederationDomainPhaseError,
replaceConditions(
allHappyConditionsLegacyConfigurationSuccess(federationDomain2.Spec.Issuer, "", frozenMetav1Now, 123),
[]metav1.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),
[]metav1.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: ptr.To(apiGroupSupervisor),
Kind: "OIDCIdentityProvider",
Name: "cant-find-me-name",
},
},
{
DisplayName: "cant-find-me-either",
ObjectRef: corev1.TypedLocalObjectReference{
APIGroup: ptr.To(apiGroupSupervisor),
Kind: "OIDCIdentityProvider",
Name: "cant-find-me-either-name",
},
},
{
DisplayName: "cant-find-me-still",
ObjectRef: corev1.TypedLocalObjectReference{
APIGroup: ptr.To(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),
[]metav1.Condition{
sadIdentityProvidersFoundConditionIdentityProvidersObjectRefsNotFound(here.Doc(
`cannot find resource specified by .spec.identityProviders[0].objectRef (with name "cant-find-me-name")
cannot find resource specified by .spec.identityProviders[1].objectRef (with name "cant-find-me-either-name")
cannot find resource specified by .spec.identityProviders[2].objectRef (with name "cant-find-me-still-name")`,
), 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: ptr.To(apiGroupSupervisor),
Kind: "OIDCIdentityProvider",
Name: oidcIdentityProvider.Name,
},
},
{
DisplayName: "can-find-me-too",
ObjectRef: corev1.TypedLocalObjectReference{
APIGroup: ptr.To(apiGroupSupervisor),
Kind: "LDAPIdentityProvider",
Name: ldapIdentityProvider.Name,
},
},
{
DisplayName: "can-find-me-three",
ObjectRef: corev1.TypedLocalObjectReference{
APIGroup: ptr.To(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: ptr.To(apiGroupSupervisor),
Kind: "OIDCIdentityProvider",
Name: oidcIdentityProvider.Name,
},
},
{
DisplayName: "duplicate1",
ObjectRef: corev1.TypedLocalObjectReference{
APIGroup: ptr.To(apiGroupSupervisor),
Kind: "LDAPIdentityProvider",
Name: ldapIdentityProvider.Name,
},
},
{
DisplayName: "duplicate1",
ObjectRef: corev1.TypedLocalObjectReference{
APIGroup: ptr.To(apiGroupSupervisor),
Kind: "LDAPIdentityProvider",
Name: ldapIdentityProvider.Name,
},
},
{
DisplayName: "unique",
ObjectRef: corev1.TypedLocalObjectReference{
APIGroup: ptr.To(apiGroupSupervisor),
Kind: "ActiveDirectoryIdentityProvider",
Name: adIdentityProvider.Name,
},
},
{
DisplayName: "duplicate2",
ObjectRef: corev1.TypedLocalObjectReference{
APIGroup: ptr.To(apiGroupSupervisor),
Kind: "LDAPIdentityProvider",
Name: ldapIdentityProvider.Name,
},
},
{
DisplayName: "duplicate2",
ObjectRef: corev1.TypedLocalObjectReference{
APIGroup: ptr.To(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),
[]metav1.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: ptr.To("wrong.example.com"),
Kind: "OIDCIdentityProvider",
Name: oidcIdentityProvider.Name,
},
},
{
DisplayName: "name2",
ObjectRef: corev1.TypedLocalObjectReference{
APIGroup: ptr.To(""), // 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: ptr.To(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),
[]metav1.Condition{
sadAPIGroupSuffixCondition(`"", "", "wrong.example.com"`, frozenMetav1Now, 123),
sadIdentityProvidersFoundConditionIdentityProvidersObjectRefsNotFound(here.Doc(
`cannot find resource specified by .spec.identityProviders[0].objectRef (with name "some-oidc-idp")
cannot find resource specified by .spec.identityProviders[1].objectRef (with name "some-ldap-idp")
cannot find resource specified by .spec.identityProviders[2].objectRef (with name "some-ldap-idp")`,
), 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: ptr.To(apiGroupSupervisor),
Kind: "OIDCIdentityProvider", // correct
Name: oidcIdentityProvider.Name,
},
},
{
DisplayName: "name2",
ObjectRef: corev1.TypedLocalObjectReference{
APIGroup: ptr.To(apiGroupSupervisor),
Kind: "wrong",
Name: ldapIdentityProvider.Name,
},
},
{
DisplayName: "name3",
ObjectRef: corev1.TypedLocalObjectReference{
APIGroup: ptr.To(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),
[]metav1.Condition{
sadKindCondition(`"", "wrong"`, frozenMetav1Now, 123),
sadIdentityProvidersFoundConditionIdentityProvidersObjectRefsNotFound(here.Doc(
`cannot find resource specified by .spec.identityProviders[1].objectRef (with name "some-ldap-idp")
cannot find resource specified by .spec.identityProviders[2].objectRef (with name "some-ldap-idp")`,
), 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: ptr.To(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),
[]metav1.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),
sadTransformationExamplesCondition(
"unable to check if the examples specified by .spec.identityProviders[0].transforms.examples[] had errors because an expression was invalid",
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: ptr.To(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),
[]metav1.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: ptr.To(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),
[]metav1.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 has lots of errors including errors from multiple IDPs, which are all shown in the status conditions using IDP indices in the messages",
inputObjects: []runtime.Object{
oidcIdentityProvider,
&configv1alpha1.FederationDomain{
ObjectMeta: metav1.ObjectMeta{Name: "config1", Namespace: namespace, Generation: 123},
Spec: configv1alpha1.FederationDomainSpec{
Issuer: "https://not-unique.com",
IdentityProviders: []configv1alpha1.FederationDomainIdentityProvider{
{
DisplayName: "not unique",
ObjectRef: corev1.TypedLocalObjectReference{
APIGroup: ptr.To(apiGroupSupervisor),
Kind: "OIDCIdentityProvider",
Name: "this will not be found",
},
Transforms: configv1alpha1.FederationDomainTransforms{
Constants: []configv1alpha1.FederationDomainTransformsConstant{
{Name: "foo", Type: "string", StringValue: "bar"},
{Name: "bar", Type: "string", StringValue: "baz"},
},
Expressions: []configv1alpha1.FederationDomainTransformsExpression{
{Type: "username/v1", Expression: `username + ":suffix"`},
},
Examples: []configv1alpha1.FederationDomainTransformsExample{
{ // this should fail
Username: "ryan",
Groups: []string{"a", "b"},
Expects: configv1alpha1.FederationDomainTransformsExampleExpects{
Username: "this is wrong string",
Groups: []string{"this is wrong string list"},
},
},
{ // this should fail
Username: "ryan",
Groups: []string{"a", "b"},
Expects: configv1alpha1.FederationDomainTransformsExampleExpects{
Username: "this is also wrong string",
Groups: []string{"this is also wrong string list"},
},
},
},
},
},
{
DisplayName: "not unique",
ObjectRef: corev1.TypedLocalObjectReference{
APIGroup: ptr.To(apiGroupSupervisor),
Kind: "this is wrong",
Name: "foo",
},
Transforms: configv1alpha1.FederationDomainTransforms{
Constants: []configv1alpha1.FederationDomainTransformsConstant{
{Name: "foo", Type: "string", StringValue: "bar"},
{Name: "bar", Type: "string", StringValue: "baz"},
},
Expressions: []configv1alpha1.FederationDomainTransformsExpression{
{Type: "username/v1", Expression: `username + ":suffix"`},
},
Examples: []configv1alpha1.FederationDomainTransformsExample{
{ // this should pass
Username: "ryan",
Groups: []string{"a", "b"},
Expects: configv1alpha1.FederationDomainTransformsExampleExpects{
Username: "ryan:suffix",
Groups: []string{"a", "b"},
},
},
{ // this should fail
Username: "ryan",
Groups: []string{"a", "b"},
Expects: configv1alpha1.FederationDomainTransformsExampleExpects{
Username: "this is still wrong string",
Groups: []string{"this is still wrong string list"},
},
},
},
},
},
{
DisplayName: "name1",
ObjectRef: corev1.TypedLocalObjectReference{
APIGroup: ptr.To("this is wrong"),
Kind: "OIDCIdentityProvider",
Name: "foo",
},
Transforms: configv1alpha1.FederationDomainTransforms{
Expressions: []configv1alpha1.FederationDomainTransformsExpression{
{Type: "username/v1", Expression: `username`},
{Type: "username/v1", Expression: `this does not compile`},
{Type: "username/v1", Expression: `username`},
{Type: "username/v1", Expression: `this also does not compile`},
},
},
},
},
},
},
&configv1alpha1.FederationDomain{
ObjectMeta: metav1.ObjectMeta{Name: "config2", Namespace: namespace, Generation: 123},
Spec: configv1alpha1.FederationDomainSpec{
Issuer: "https://not-unique.com",
IdentityProviders: []configv1alpha1.FederationDomainIdentityProvider{
{
DisplayName: "name1",
ObjectRef: corev1.TypedLocalObjectReference{
APIGroup: ptr.To(apiGroupSupervisor),
Kind: "OIDCIdentityProvider",
Name: oidcIdentityProvider.Name,
},
Transforms: configv1alpha1.FederationDomainTransforms{
Expressions: []configv1alpha1.FederationDomainTransformsExpression{
{Type: "username/v1", Expression: `username`},
{Type: "username/v1", Expression: `this still does not compile`},
{Type: "username/v1", Expression: `username`},
{Type: "username/v1", Expression: `this really does not compile`},
},
},
},
},
},
},
},
wantFDIssuers: []*federationdomainproviders.FederationDomainIssuer{},
wantStatusUpdates: []*configv1alpha1.FederationDomain{
expectedFederationDomainStatusUpdate(
&configv1alpha1.FederationDomain{
ObjectMeta: metav1.ObjectMeta{Name: "config1", Namespace: namespace, Generation: 123},
},
configv1alpha1.FederationDomainPhaseError,
replaceConditions(
allHappyConditionsSuccess("https://not-unique.com", frozenMetav1Now, 123),
[]metav1.Condition{
sadAPIGroupSuffixCondition(`"this is wrong"`, frozenMetav1Now, 123),
sadDisplayNamesUniqueCondition(`"not unique"`, frozenMetav1Now, 123),
sadIdentityProvidersFoundConditionIdentityProvidersObjectRefsNotFound(here.Doc(
`cannot find resource specified by .spec.identityProviders[0].objectRef (with name "this will not be found")
cannot find resource specified by .spec.identityProviders[1].objectRef (with name "foo")
cannot find resource specified by .spec.identityProviders[2].objectRef (with name "foo")`,
), frozenMetav1Now, 123),
sadIssuerIsUniqueCondition(frozenMetav1Now, 123),
sadKindCondition(`"this is wrong"`, frozenMetav1Now, 123),
sadTransformationExpressionsCondition(here.Doc(
`spec.identityProvider[2].transforms.expressions[1].expression was invalid:
CEL expression compile error: ERROR: <input>:1:6: Syntax error: mismatched input 'does' expecting <EOF>
| this does not compile
| .....^
spec.identityProvider[2].transforms.expressions[3].expression was invalid:
CEL expression compile error: ERROR: <input>:1:6: Syntax error: mismatched input 'also' expecting <EOF>
| this also does not compile
| .....^`,
), frozenMetav1Now, 123),
sadTransformationExamplesCondition(here.Doc(
`.spec.identityProviders[0].transforms.examples[0] example failed:
expected: username "this is wrong string"
actual: username "ryan:suffix"
.spec.identityProviders[0].transforms.examples[0] example failed:
expected: groups ["this is wrong string list"]
actual: groups ["a", "b"]
.spec.identityProviders[0].transforms.examples[1] example failed:
expected: username "this is also wrong string"
actual: username "ryan:suffix"
.spec.identityProviders[0].transforms.examples[1] example failed:
expected: groups ["this is also wrong string list"]
actual: groups ["a", "b"]
.spec.identityProviders[1].transforms.examples[1] example failed:
expected: username "this is still wrong string"
actual: username "ryan:suffix"
.spec.identityProviders[1].transforms.examples[1] example failed:
expected: groups ["this is still wrong string list"]
actual: groups ["a", "b"]
unable to check if the examples specified by .spec.identityProviders[2].transforms.examples[] had errors because an expression was invalid`,
), frozenMetav1Now, 123),
sadReadyCondition(frozenMetav1Now, 123),
}),
),
expectedFederationDomainStatusUpdate(
&configv1alpha1.FederationDomain{
ObjectMeta: metav1.ObjectMeta{Name: "config2", Namespace: namespace, Generation: 123},
},
configv1alpha1.FederationDomainPhaseError,
replaceConditions(
allHappyConditionsSuccess("https://not-unique.com", frozenMetav1Now, 123),
[]metav1.Condition{
sadIssuerIsUniqueCondition(frozenMetav1Now, 123),
sadTransformationExpressionsCondition(here.Doc(
`spec.identityProvider[0].transforms.expressions[1].expression was invalid:
CEL expression compile error: ERROR: <input>:1:6: Syntax error: mismatched input 'still' expecting <EOF>
| this still does not compile
| .....^
spec.identityProvider[0].transforms.expressions[3].expression was invalid:
CEL expression compile error: ERROR: <input>:1:6: Syntax error: mismatched input 'really' expecting <EOF>
| this really does not compile
| .....^`,
), frozenMetav1Now, 123),
sadTransformationExamplesCondition(
"unable to check if the examples specified by .spec.identityProviders[0].transforms.examples[] had errors because an expression was invalid",
frozenMetav1Now, 123),
sadReadyCondition(frozenMetav1Now, 123),
}),
),
},
},
{
name: "the federation domain has valid IDPs and transformations and examples",
inputObjects: []runtime.Object{
oidcIdentityProvider,
ldapIdentityProvider,
&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: ptr.To(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)`},
},
Constants: []configv1alpha1.FederationDomainTransformsConstant{
{Name: "str", Type: "string", StringValue: "abc"},
{Name: "strL", Type: "stringList", StringListValue: []string{"def"}},
},
Examples: []configv1alpha1.FederationDomainTransformsExample{
{
Username: "ryan",
Groups: []string{"a", "b"},
Expects: configv1alpha1.FederationDomainTransformsExampleExpects{
Username: "pre:ryan",
Groups: []string{"pre:b", "pre:a"},
Rejected: false,
},
},
{
Username: "other",
Expects: configv1alpha1.FederationDomainTransformsExampleExpects{
Rejected: true,
Message: "only ryan allowed",
},
},
{
Username: "rejectMeWithDefaultMessage",
Expects: configv1alpha1.FederationDomainTransformsExampleExpects{
Rejected: true,
// Not specifying message is the same as expecting the default message.
},
},
{
Username: "rejectMeWithDefaultMessage",
Expects: configv1alpha1.FederationDomainTransformsExampleExpects{
Rejected: true,
Message: "authentication was rejected by a configured policy", // this is the default message
},
},
},
},
},
{
DisplayName: "name2",
ObjectRef: corev1.TypedLocalObjectReference{
APIGroup: ptr.To(apiGroupSupervisor),
Kind: "LDAPIdentityProvider",
Name: ldapIdentityProvider.Name,
},
Transforms: configv1alpha1.FederationDomainTransforms{
Expressions: []configv1alpha1.FederationDomainTransformsExpression{
{Type: "username/v1", Expression: `"pre:" + username`},
},
Examples: []configv1alpha1.FederationDomainTransformsExample{
{
Username: "ryan",
Groups: []string{"a", "b"},
Expects: configv1alpha1.FederationDomainTransformsExampleExpects{
Username: "pre:ryan",
Groups: []string{"b", "a"},
Rejected: false,
},
},
},
},
},
},
},
},
},
wantFDIssuers: []*federationdomainproviders.FederationDomainIssuer{
federationDomainIssuerWithIDPs(t, "https://issuer1.com", []*federationdomainproviders.FederationDomainIdentityProvider{
{
DisplayName: "name1",
UID: oidcIdentityProvider.UID,
Transforms: newTransformationPipeline(t, &celtransformer.TransformationConstants{
StringConstants: map[string]string{"str": "abc"},
StringListConstants: map[string][]string{"strL": {"def"}},
},
&celtransformer.AllowAuthenticationPolicy{
Expression: `username == "ryan" || username == "rejectMeWithDefaultMessage"`,
RejectedAuthenticationMessage: "only ryan allowed",
},
&celtransformer.AllowAuthenticationPolicy{Expression: `username != "rejectMeWithDefaultMessage"`},
&celtransformer.UsernameTransformation{Expression: `"pre:" + username`},
&celtransformer.GroupsTransformation{Expression: `groups.map(g, "pre:" + g)`},
),
},
{
DisplayName: "name2",
UID: ldapIdentityProvider.UID,
Transforms: newTransformationPipeline(t, &celtransformer.TransformationConstants{},
&celtransformer.UsernameTransformation{Expression: `"pre:" + username`},
),
},
}),
},
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 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: ptr.To(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: ptr.To(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)
// This is ugly, but we cannot test equality on compiled identity transformations because cel.Program
// cannot be compared for equality. This converts them to a type which can be tested for equality,
// which should be good enough for the purposes of this test.
require.ElementsMatch(t,
convertToComparableType(tt.wantFDIssuers),
convertToComparableType(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())
}
})
}
}
type comparableFederationDomainIssuer struct {
issuer string
identityProviders []*comparableFederationDomainIdentityProvider
defaultIdentityProvider *comparableFederationDomainIdentityProvider
}
type comparableFederationDomainIdentityProvider struct {
DisplayName string
UID types.UID
TransformsSource []interface{}
}
func makeFederationDomainIdentityProviderComparable(fdi *federationdomainproviders.FederationDomainIdentityProvider) *comparableFederationDomainIdentityProvider {
if fdi == nil {
return nil
}
return &comparableFederationDomainIdentityProvider{
DisplayName: fdi.DisplayName,
UID: fdi.UID,
TransformsSource: fdi.Transforms.Source(),
}
}
func convertToComparableType(fdis []*federationdomainproviders.FederationDomainIssuer) []*comparableFederationDomainIssuer {
result := []*comparableFederationDomainIssuer{}
for _, fdi := range fdis {
comparableFDIs := make([]*comparableFederationDomainIdentityProvider, len(fdi.IdentityProviders()))
for _, idp := range fdi.IdentityProviders() {
comparableFDIs = append(comparableFDIs, makeFederationDomainIdentityProviderComparable(idp))
}
converted := &comparableFederationDomainIssuer{
issuer: fdi.Issuer(),
identityProviders: comparableFDIs,
defaultIdentityProvider: makeFederationDomainIdentityProviderComparable(fdi.DefaultIdentityProvider()),
}
result = append(result, converted)
}
return result
}
func expectedFederationDomainStatusUpdate(
fd *configv1alpha1.FederationDomain,
phase configv1alpha1.FederationDomainPhase,
conditions []metav1.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()
})
}
func newTransformationPipeline(
t *testing.T,
consts *celtransformer.TransformationConstants,
transformations ...celtransformer.CELTransformation,
) *idtransform.TransformationPipeline {
pipeline := idtransform.NewTransformationPipeline()
compiler, err := celtransformer.NewCELTransformer(celTransformerMaxExpressionRuntime)
require.NoError(t, err)
if consts.StringConstants == nil {
consts.StringConstants = map[string]string{}
}
if consts.StringListConstants == nil {
consts.StringListConstants = map[string][]string{}
}
for _, transform := range transformations {
compiledTransform, err := compiler.CompileTransformation(transform, consts)
require.NoError(t, err)
pipeline.AppendTransformation(compiledTransform)
}
return pipeline
}
func TestTransformationPipelinesCanBeTestedForEqualityUsingSourceToMakeTestingEasier(t *testing.T) {
compiler, err := celtransformer.NewCELTransformer(5 * time.Second)
require.NoError(t, err)
transforms := []celtransformer.CELTransformation{
&celtransformer.AllowAuthenticationPolicy{
Expression: `username == "ryan" || username == "rejectMeWithDefaultMessage"`,
RejectedAuthenticationMessage: "only ryan allowed",
},
&celtransformer.UsernameTransformation{Expression: `"pre:" + username`},
&celtransformer.GroupsTransformation{Expression: `groups.map(g, "pre:" + g)`},
}
differentTransforms := []celtransformer.CELTransformation{
&celtransformer.AllowAuthenticationPolicy{
Expression: `username == "ryan" || username == "different"`,
RejectedAuthenticationMessage: "different",
},
&celtransformer.UsernameTransformation{Expression: `"different:" + username`},
&celtransformer.GroupsTransformation{Expression: `groups.map(g, "different:" + g)`},
}
consts := &celtransformer.TransformationConstants{
StringConstants: map[string]string{
"foo": "bar",
"baz": "bat",
},
StringListConstants: map[string][]string{
"foo": {"a", "b"},
"bar": {"c", "d"},
},
}
differentConsts := &celtransformer.TransformationConstants{
StringConstants: map[string]string{
"foo": "barDifferent",
"baz": "bat",
},
StringListConstants: map[string][]string{
"foo": {"aDifferent", "b"},
"bar": {"c", "d"},
},
}
pipeline := idtransform.NewTransformationPipeline()
equalPipeline := idtransform.NewTransformationPipeline()
differentPipeline1 := idtransform.NewTransformationPipeline()
differentPipeline2 := idtransform.NewTransformationPipeline()
expectedSourceList := []interface{}{}
for i, transform := range transforms {
// Compile and append to a pipeline.
compiledTransform1, err := compiler.CompileTransformation(transform, consts)
require.NoError(t, err)
pipeline.AppendTransformation(compiledTransform1)
// Recompile the same thing and append it to another pipeline.
// This pipeline should end up being equal to the first one.
compiledTransform2, err := compiler.CompileTransformation(transform, consts)
require.NoError(t, err)
equalPipeline.AppendTransformation(compiledTransform2)
// Build up a test expectation value.
expectedSourceList = append(expectedSourceList, &celtransformer.CELTransformationSource{Expr: transform, Consts: consts})
// Compile a different expression using the same constants and append it to a different pipeline.
// This should not be equal to the other pipelines.
compiledDifferentExpressionSameConsts, err := compiler.CompileTransformation(differentTransforms[i], consts)
require.NoError(t, err)
differentPipeline1.AppendTransformation(compiledDifferentExpressionSameConsts)
// Compile the same expression using the different constants and append it to a different pipeline.
// This should not be equal to the other pipelines.
compiledSameExpressionDifferentConsts, err := compiler.CompileTransformation(transform, differentConsts)
require.NoError(t, err)
differentPipeline2.AppendTransformation(compiledSameExpressionDifferentConsts)
}
require.Equal(t, expectedSourceList, pipeline.Source())
require.Equal(t, expectedSourceList, equalPipeline.Source())
// The source of compiled pipelines can be compared to each other in this way for testing purposes.
require.Equal(t, pipeline.Source(), equalPipeline.Source())
require.NotEqual(t, pipeline.Source(), differentPipeline1.Source())
require.NotEqual(t, pipeline.Source(), differentPipeline2.Source())
}