diff --git a/internal/controller/supervisorconfig/federation_domain_watcher.go b/internal/controller/supervisorconfig/federation_domain_watcher.go index 3cf2becb..37cd203d 100644 --- a/internal/controller/supervisorconfig/federation_domain_watcher.go +++ b/internal/controller/supervisorconfig/federation_domain_watcher.go @@ -10,6 +10,7 @@ import ( "strings" "time" + corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/equality" "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -68,6 +69,8 @@ type federationDomainWatcherController struct { oidcIdentityProviderInformer idpinformers.OIDCIdentityProviderInformer ldapIdentityProviderInformer idpinformers.LDAPIdentityProviderInformer activeDirectoryIdentityProviderInformer idpinformers.ActiveDirectoryIdentityProviderInformer + + celTransformer *celtransformer.CELTransformer } // NewFederationDomainWatcherController creates a controllerlib.Controller that watches @@ -125,306 +128,52 @@ func NewFederationDomainWatcherController( } // Sync implements controllerlib.Syncer. -func (c *federationDomainWatcherController) Sync(ctx controllerlib.Context) error { //nolint:funlen,gocyclo +func (c *federationDomainWatcherController) Sync(ctx controllerlib.Context) error { federationDomains, err := c.federationDomainInformer.Lister().List(labels.Everything()) if err != nil { return err } + if c.celTransformer == nil { + c.celTransformer, _ = celtransformer.NewCELTransformer(celTransformerMaxExpressionRuntime) // TODO: what is a good duration limit here? + // TODO: handle err from NewCELTransformer() above + } + + // Process each FederationDomain to validate its spec and to turn it into a FederationDomainIssuer. + federationDomainIssuers, fdToConditionsMap, _ := c.processAllFederationDomains(federationDomains) + // TODO: handle err + + // Load the endpoints of every valid FederationDomain. Removes the endpoints of any + // previous FederationDomains which no longer exist or are no longer valid. + c.federationDomainsSetter.SetFederationDomains(federationDomainIssuers...) + + // Now that the endpoints of every valid FederationDomain are available, update the + // statuses. This allows clients to wait for Ready without any race conditions in the + // endpoints being available. var errs []error + for federationDomain, conditions := range fdToConditionsMap { + if err = c.updateStatus(ctx.Context, federationDomain, conditions); err != nil { + errs = append(errs, fmt.Errorf("could not update status: %w", err)) + } + } + + return errorsutil.NewAggregate(errs) +} + +func (c *federationDomainWatcherController) processAllFederationDomains( + federationDomains []*configv1alpha1.FederationDomain, +) ([]*federationdomainproviders.FederationDomainIssuer, map[*configv1alpha1.FederationDomain][]*configv1alpha1.Condition, error) { federationDomainIssuers := make([]*federationdomainproviders.FederationDomainIssuer, 0) - crossDomainConfigValidator := newCrossFederationDomainConfigValidator(federationDomains) fdToConditionsMap := map[*configv1alpha1.FederationDomain][]*configv1alpha1.Condition{} + crossDomainConfigValidator := newCrossFederationDomainConfigValidator(federationDomains) for _, federationDomain := range federationDomains { - conditions := make([]*configv1alpha1.Condition, 0, 4) + conditions := make([]*configv1alpha1.Condition, 0) conditions = crossDomainConfigValidator.Validate(federationDomain, conditions) - // TODO: Move all this identity provider stuff into helper functions. This is just a sketch of how the code would - // work in the sense that this is not doing error handling, is not validating everything that it should, and - // is not updating the status of the FederationDomain with anything related to these identity providers. - // This code may crash on invalid inputs since it is not handling any errors. However, when given valid inputs, - // this correctly implements the multiple IDPs features. - // Create the list of IDPs for this FederationDomain. - // Don't worry if the IDP CRs themselves is phase=Ready because those which are not ready will not be loaded - // into the provider cache, so they cannot actually be used to authenticate. - federationDomainIdentityProviders := []*federationdomainproviders.FederationDomainIdentityProvider{} - var defaultFederationDomainIdentityProvider *federationdomainproviders.FederationDomainIdentityProvider - if len(federationDomain.Spec.IdentityProviders) == 0 { - // When the FederationDomain does not list any IDPs, then we might be in backwards compatibility mode. - oidcIdentityProviders, _ := c.oidcIdentityProviderInformer.Lister().List(labels.Everything()) - ldapIdentityProviders, _ := c.ldapIdentityProviderInformer.Lister().List(labels.Everything()) - activeDirectoryIdentityProviders, _ := c.activeDirectoryIdentityProviderInformer.Lister().List(labels.Everything()) - // TODO handle err return value for each of the above three lines - - // Check if that there is exactly one IDP defined in the Supervisor namespace of any IDP CRD type. - idpCRsCount := len(oidcIdentityProviders) + len(ldapIdentityProviders) + len(activeDirectoryIdentityProviders) - - switch { - case idpCRsCount == 1: - foundIDPName := "" - // If so, default that IDP's DisplayName to be the same as its resource Name. - defaultFederationDomainIdentityProvider = &federationdomainproviders.FederationDomainIdentityProvider{} - switch { - case len(oidcIdentityProviders) == 1: - defaultFederationDomainIdentityProvider.DisplayName = oidcIdentityProviders[0].Name - defaultFederationDomainIdentityProvider.UID = oidcIdentityProviders[0].UID - foundIDPName = oidcIdentityProviders[0].Name - case len(ldapIdentityProviders) == 1: - defaultFederationDomainIdentityProvider.DisplayName = ldapIdentityProviders[0].Name - defaultFederationDomainIdentityProvider.UID = ldapIdentityProviders[0].UID - foundIDPName = ldapIdentityProviders[0].Name - case len(activeDirectoryIdentityProviders) == 1: - defaultFederationDomainIdentityProvider.DisplayName = activeDirectoryIdentityProviders[0].Name - defaultFederationDomainIdentityProvider.UID = activeDirectoryIdentityProviders[0].UID - foundIDPName = activeDirectoryIdentityProviders[0].Name - } - // Backwards compatibility mode always uses an empty identity transformation pipeline since no - // transformations are defined on the FederationDomain. - defaultFederationDomainIdentityProvider.Transforms = idtransform.NewTransformationPipeline() - conditions = append(conditions, &configv1alpha1.Condition{ - Type: typeIdentityProvidersFound, - Status: configv1alpha1.ConditionTrue, - Reason: reasonLegacyConfigurationSuccess, - Message: fmt.Sprintf("no resources were specified by .spec.identityProviders[].objectRef but exactly one "+ - "identity provider resource has been found: using %q as "+ - "identity provider: please explicitly list identity providers in .spec.identityProviders "+ - "(this legacy configuration mode may be removed in a future version of Pinniped)", foundIDPName), - }) - case idpCRsCount > 1: - conditions = append(conditions, &configv1alpha1.Condition{ - Type: typeIdentityProvidersFound, - Status: configv1alpha1.ConditionFalse, - Reason: reasonIdentityProviderNotSpecified, // vs LegacyConfigurationIdentityProviderNotFound as this is more specific - 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), - }) - default: - conditions = append(conditions, &configv1alpha1.Condition{ - Type: typeIdentityProvidersFound, - Status: configv1alpha1.ConditionFalse, - Reason: reasonLegacyConfigurationIdentityProviderNotFound, - Message: "no resources were specified by .spec.identityProviders[].objectRef and no identity provider " + - "resources have been found: please create an identity provider resource", - }) - } - } - - // If there is an explicit list of IDPs on the FederationDomain, then process the list. - celTransformer, _ := celtransformer.NewCELTransformer(celTransformerMaxExpressionRuntime) // TODO: what is a good duration limit here? - // TODO: handle err from NewCELTransformer() above - - idpNotFoundIndices := []int{} - for index, idp := range federationDomain.Spec.IdentityProviders { - // TODO: Validate that all displayNames are unique within this FederationDomain's spec's list of identity providers. - // TODO: Validate that idp.ObjectRef.APIGroup is the expected APIGroup for IDP CRs "idp.supervisor.pinniped.dev" - // Validate that each objectRef resolves to an existing IDP. It does not matter if the IDP itself - // is phase=Ready, because it will not be loaded into the cache if not ready. For each objectRef - // that does not resolve, put an error on the FederationDomain status. - var idpResourceUID types.UID - var foundIDP metav1.Object - switch idp.ObjectRef.Kind { - case "LDAPIdentityProvider": - foundIDP, err = c.ldapIdentityProviderInformer.Lister().LDAPIdentityProviders(federationDomain.Namespace).Get(idp.ObjectRef.Name) - case "ActiveDirectoryIdentityProvider": - foundIDP, err = c.activeDirectoryIdentityProviderInformer.Lister().ActiveDirectoryIdentityProviders(federationDomain.Namespace).Get(idp.ObjectRef.Name) - case "OIDCIdentityProvider": - foundIDP, err = c.oidcIdentityProviderInformer.Lister().OIDCIdentityProviders(federationDomain.Namespace).Get(idp.ObjectRef.Name) - default: - // TODO: handle an IDP type that we do not understand. - } - switch { - case err == nil: - idpResourceUID = foundIDP.GetUID() - case errors.IsNotFound(err): - idpNotFoundIndices = append(idpNotFoundIndices, index) - default: - // TODO: handle unexpected errors - } - - // Prepare the transformations. - pipeline := idtransform.NewTransformationPipeline() - consts := &celtransformer.TransformationConstants{ - StringConstants: map[string]string{}, - StringListConstants: map[string][]string{}, - } - // Read all the declared constants. - for _, c := range idp.Transforms.Constants { - switch c.Type { - case "string": - consts.StringConstants[c.Name] = c.StringValue - case "stringList": - consts.StringListConstants[c.Name] = c.StringListValue - default: - // TODO: this shouldn't really happen since the CRD validates it, but handle it as an error - } - } - // Compile all the expressions and add them to the pipeline. - for idx, e := range idp.Transforms.Expressions { - var rawTransform celtransformer.CELTransformation - switch e.Type { - case "username/v1": - rawTransform = &celtransformer.UsernameTransformation{Expression: e.Expression} - case "groups/v1": - rawTransform = &celtransformer.GroupsTransformation{Expression: e.Expression} - case "policy/v1": - rawTransform = &celtransformer.AllowAuthenticationPolicy{ - Expression: e.Expression, - RejectedAuthenticationMessage: e.Message, - } - default: - // TODO: this shouldn't really happen since the CRD validates it, but handle it as an error - } - compiledTransform, err := celTransformer.CompileTransformation(rawTransform, consts) - if err != nil { - // TODO: handle compile err - plog.Error("error compiling identity transformation", err, - "federationDomain", federationDomain.Name, - "idpDisplayName", idp.DisplayName, - "transformationIndex", idx, - "transformationType", e.Type, - "transformationExpression", e.Expression, - ) - } - pipeline.AppendTransformation(compiledTransform) - plog.Debug("successfully compiled identity transformation expression", - "type", e.Type, - "expr", e.Expression, - "policyMessage", e.Message, - ) - } - // Run all the provided transform examples. If any fail, put errors on the FederationDomain status. - for idx, e := range idp.Transforms.Examples { - // TODO: use a real context param below - result, _ := pipeline.Evaluate(context.TODO(), e.Username, e.Groups) - // TODO: handle err - resultWasAuthRejected := !result.AuthenticationAllowed - if e.Expects.Rejected && !resultWasAuthRejected { //nolint:gocritic,nestif - // TODO: handle this failed example - plog.Warning("FederationDomain identity provider transformations example failed: expected authentication to be rejected but it was not", - "federationDomain", federationDomain.Name, - "idpDisplayName", idp.DisplayName, - "exampleIndex", idx, - "expectedRejected", e.Expects.Rejected, - "actualRejectedResult", resultWasAuthRejected, - "expectedMessage", e.Expects.Message, - "actualMessageResult", result.RejectedAuthenticationMessage, - ) - } else if !e.Expects.Rejected && resultWasAuthRejected { - // TODO: handle this failed example - plog.Warning("FederationDomain identity provider transformations example failed: expected authentication not to be rejected but it was rejected", - "federationDomain", federationDomain.Name, - "idpDisplayName", idp.DisplayName, - "exampleIndex", idx, - "expectedRejected", e.Expects.Rejected, - "actualRejectedResult", resultWasAuthRejected, - "expectedMessage", e.Expects.Message, - "actualMessageResult", result.RejectedAuthenticationMessage, - ) - } else if e.Expects.Rejected && resultWasAuthRejected && e.Expects.Message != result.RejectedAuthenticationMessage { - // TODO: when expected message is blank, then treat it like it expects the default message - // TODO: handle this failed example - plog.Warning("FederationDomain identity provider transformations example failed: expected a different authentication rejection message", - "federationDomain", federationDomain.Name, - "idpDisplayName", idp.DisplayName, - "exampleIndex", idx, - "expectedRejected", e.Expects.Rejected, - "actualRejectedResult", resultWasAuthRejected, - "expectedMessage", e.Expects.Message, - "actualMessageResult", result.RejectedAuthenticationMessage, - ) - } else if result.AuthenticationAllowed { - // In the case where the user expected the auth to be allowed and it was allowed, then compare - // the expected username and group names to the actual username and group names. - // TODO: when both of these fail, put both errors onto the status (not just the first one) - if e.Expects.Username != result.Username { - // TODO: handle this failed example - plog.Warning("FederationDomain identity provider transformations example failed: expected a different transformed username", - "federationDomain", federationDomain.Name, - "idpDisplayName", idp.DisplayName, - "exampleIndex", idx, - "expectedUsername", e.Expects.Username, - "actualUsernameResult", result.Username, - ) - } - if !stringSlicesEqual(e.Expects.Groups, result.Groups) { - // TODO: Do we need to make this insensitive to ordering, or should the transformations evaluator be changed to always return sorted group names at the end of the pipeline? - // TODO: What happens if the user did not write any group expectation? Treat it like expecting an empty list of groups? - // TODO: handle this failed example - plog.Warning("FederationDomain identity provider transformations example failed: expected a different transformed groups list", - "federationDomain", federationDomain.Name, - "idpDisplayName", idp.DisplayName, - "exampleIndex", idx, - "expectedGroups", e.Expects.Groups, - "actualGroupsResult", result.Groups, - ) - } - } - } - // For each valid IDP (unique displayName, valid objectRef + valid transforms), add it to the list. - federationDomainIdentityProviders = append(federationDomainIdentityProviders, &federationdomainproviders.FederationDomainIdentityProvider{ - DisplayName: idp.DisplayName, - UID: idpResourceUID, - Transforms: pipeline, - }) - plog.Debug("loaded FederationDomain identity provider", - "federationDomain", federationDomain.Name, - "identityProviderDisplayName", idp.DisplayName, - "identityProviderResourceUID", idpResourceUID, - ) - } - - if len(idpNotFoundIndices) != 0 { - msgs := []string{} - for _, idpNotFoundIndex := range idpNotFoundIndices { - msgs = append(msgs, fmt.Sprintf(".spec.identityProviders[%d] with displayName %q", idpNotFoundIndex, - federationDomain.Spec.IdentityProviders[idpNotFoundIndex].DisplayName)) - } - conditions = append(conditions, &configv1alpha1.Condition{ - Type: typeIdentityProvidersFound, - Status: configv1alpha1.ConditionFalse, - Reason: reasonIdentityProvidersObjectRefsNotFound, - Message: fmt.Sprintf(".spec.identityProviders[].objectRef identifies resource(s) that cannot be found: %s", - strings.Join(msgs, ", ")), - }) - } else if len(federationDomain.Spec.IdentityProviders) != 0 { - conditions = append(conditions, &configv1alpha1.Condition{ - Type: typeIdentityProvidersFound, - Status: configv1alpha1.ConditionTrue, - Reason: reasonSuccess, - Message: "the resources specified by .spec.identityProviders[].objectRef were found", - }) - } - - // Now that we have the list of IDPs for this FederationDomain, create the issuer. - var federationDomainIssuer *federationdomainproviders.FederationDomainIssuer - if defaultFederationDomainIdentityProvider != nil { - // This is the constructor for the backwards compatibility mode. - federationDomainIssuer, err = federationdomainproviders.NewFederationDomainIssuerWithDefaultIDP(federationDomain.Spec.Issuer, defaultFederationDomainIdentityProvider) - } else { - // This is the constructor for any other case, including when there is an empty list of IDPs. - federationDomainIssuer, err = federationdomainproviders.NewFederationDomainIssuer(federationDomain.Spec.Issuer, federationDomainIdentityProviders) - } - if err != nil { - // Note that the FederationDomainIssuer constructors only validate the Issuer URL, - // so these are always issuer URL validation errors. - conditions = append(conditions, &configv1alpha1.Condition{ - Type: typeIssuerURLValid, - Status: configv1alpha1.ConditionFalse, - Reason: reasonInvalidIssuerURL, - Message: err.Error(), - }) - } else { - conditions = append(conditions, &configv1alpha1.Condition{ - Type: typeIssuerURLValid, - Status: configv1alpha1.ConditionTrue, - Reason: reasonSuccess, - Message: "spec.issuer is a valid URL", - }) - } + federationDomainIssuer, conditions, _ := c.makeFederationDomainIssuer(federationDomain, conditions) + // TODO: handle err // Now that we have determined the conditions, save them for after the loop. // For a valid FederationDomain, want to update the conditions after we have @@ -437,20 +186,340 @@ func (c *federationDomainWatcherController) Sync(ctx controllerlib.Context) erro } } - // Load the endpoints of every valid FederationDomain. Removes the endpoints of any - // previous FederationDomains which no longer exist or are no longer valid. - c.federationDomainsSetter.SetFederationDomains(federationDomainIssuers...) + return federationDomainIssuers, fdToConditionsMap, nil +} - // Now that the endpoints of every valid FederationDomain are available, update the - // statuses. This allows clients to wait for Ready without any race conditions in the - // endpoints being available. - for federationDomain, conditions := range fdToConditionsMap { - if err = c.updateStatus(ctx.Context, federationDomain, conditions); err != nil { - errs = append(errs, fmt.Errorf("could not update status: %w", err)) +func (c *federationDomainWatcherController) makeFederationDomainIssuer( + federationDomain *configv1alpha1.FederationDomain, + conditions []*configv1alpha1.Condition, +) (*federationdomainproviders.FederationDomainIssuer, []*configv1alpha1.Condition, error) { + // Create the list of IDPs for this FederationDomain. + // Don't worry if the IDP CRs themselves is phase=Ready because those which are not ready will not be loaded + // into the provider cache, so they cannot actually be used to authenticate. + var federationDomainIssuer *federationdomainproviders.FederationDomainIssuer + if len(federationDomain.Spec.IdentityProviders) == 0 { + federationDomainIssuer, conditions, _ = c.makeLegacyFederationDomainIssuer(federationDomain, conditions) + // TODO handle err + } else { + federationDomainIssuer, conditions, _ = c.makeFederationDomainIssuerWithExplicitIDPs(federationDomain, conditions) + // TODO handle err + } + + return federationDomainIssuer, conditions, nil +} + +func (c *federationDomainWatcherController) makeLegacyFederationDomainIssuer( + federationDomain *configv1alpha1.FederationDomain, + conditions []*configv1alpha1.Condition, +) (*federationdomainproviders.FederationDomainIssuer, []*configv1alpha1.Condition, error) { + var defaultFederationDomainIdentityProvider *federationdomainproviders.FederationDomainIdentityProvider + + // When the FederationDomain does not list any IDPs, then we might be in backwards compatibility mode. + oidcIdentityProviders, _ := c.oidcIdentityProviderInformer.Lister().List(labels.Everything()) + ldapIdentityProviders, _ := c.ldapIdentityProviderInformer.Lister().List(labels.Everything()) + activeDirectoryIdentityProviders, _ := c.activeDirectoryIdentityProviderInformer.Lister().List(labels.Everything()) + // TODO handle err return value for each of the above three lines + + // Check if that there is exactly one IDP defined in the Supervisor namespace of any IDP CRD type. + idpCRsCount := len(oidcIdentityProviders) + len(ldapIdentityProviders) + len(activeDirectoryIdentityProviders) + + switch { + case idpCRsCount == 1: + foundIDPName := "" + // If so, default that IDP's DisplayName to be the same as its resource Name. + defaultFederationDomainIdentityProvider = &federationdomainproviders.FederationDomainIdentityProvider{} + switch { + case len(oidcIdentityProviders) == 1: + defaultFederationDomainIdentityProvider.DisplayName = oidcIdentityProviders[0].Name + defaultFederationDomainIdentityProvider.UID = oidcIdentityProviders[0].UID + foundIDPName = oidcIdentityProviders[0].Name + case len(ldapIdentityProviders) == 1: + defaultFederationDomainIdentityProvider.DisplayName = ldapIdentityProviders[0].Name + defaultFederationDomainIdentityProvider.UID = ldapIdentityProviders[0].UID + foundIDPName = ldapIdentityProviders[0].Name + case len(activeDirectoryIdentityProviders) == 1: + defaultFederationDomainIdentityProvider.DisplayName = activeDirectoryIdentityProviders[0].Name + defaultFederationDomainIdentityProvider.UID = activeDirectoryIdentityProviders[0].UID + foundIDPName = activeDirectoryIdentityProviders[0].Name + } + // Backwards compatibility mode always uses an empty identity transformation pipeline since no + // transformations are defined on the FederationDomain. + defaultFederationDomainIdentityProvider.Transforms = idtransform.NewTransformationPipeline() + conditions = append(conditions, &configv1alpha1.Condition{ + Type: typeIdentityProvidersFound, + Status: configv1alpha1.ConditionTrue, + Reason: reasonLegacyConfigurationSuccess, + Message: fmt.Sprintf("no resources were specified by .spec.identityProviders[].objectRef but exactly one "+ + "identity provider resource has been found: using %q as "+ + "identity provider: please explicitly list identity providers in .spec.identityProviders "+ + "(this legacy configuration mode may be removed in a future version of Pinniped)", foundIDPName), + }) + case idpCRsCount > 1: + conditions = append(conditions, &configv1alpha1.Condition{ + Type: typeIdentityProvidersFound, + Status: configv1alpha1.ConditionFalse, + Reason: reasonIdentityProviderNotSpecified, // vs LegacyConfigurationIdentityProviderNotFound as this is more specific + 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), + }) + default: + conditions = append(conditions, &configv1alpha1.Condition{ + Type: typeIdentityProvidersFound, + Status: configv1alpha1.ConditionFalse, + Reason: reasonLegacyConfigurationIdentityProviderNotFound, + Message: "no resources were specified by .spec.identityProviders[].objectRef and no identity provider " + + "resources have been found: please create an identity provider resource", + }) + } + + // This is the constructor for the backwards compatibility mode. + federationDomainIssuer, err := federationdomainproviders.NewFederationDomainIssuerWithDefaultIDP(federationDomain.Spec.Issuer, defaultFederationDomainIdentityProvider) + conditions = appendIssuerURLValidCondition(err, conditions) + + return federationDomainIssuer, conditions, nil +} + +func (c *federationDomainWatcherController) makeFederationDomainIssuerWithExplicitIDPs( + federationDomain *configv1alpha1.FederationDomain, + conditions []*configv1alpha1.Condition, +) (*federationdomainproviders.FederationDomainIssuer, []*configv1alpha1.Condition, error) { + var err error + federationDomainIdentityProviders := []*federationdomainproviders.FederationDomainIdentityProvider{} + idpNotFoundIndices := []int{} + + for index, idp := range federationDomain.Spec.IdentityProviders { + // TODO: Validate that all displayNames are unique within this FederationDomain's spec's list of identity providers. + // TODO: Validate that idp.ObjectRef.APIGroup is the expected APIGroup for IDP CRs "idp.supervisor.pinniped.dev" + // Validate that each objectRef resolves to an existing IDP. It does not matter if the IDP itself + // is phase=Ready, because it will not be loaded into the cache if not ready. For each objectRef + // that does not resolve, put an error on the FederationDomain status. + idpResourceUID, idpWasFound, _ := c.findIDPsUIDByObjectRef(idp.ObjectRef, federationDomain.Namespace) + // TODO handle err + if !idpWasFound { + idpNotFoundIndices = append(idpNotFoundIndices, index) + } + + pipeline, _ := c.makeTransformationPipelineForIdentityProvider(idp, federationDomain.Name) + // TODO handle err + + // For each valid IDP (unique displayName, valid objectRef + valid transforms), add it to the list. + federationDomainIdentityProviders = append(federationDomainIdentityProviders, &federationdomainproviders.FederationDomainIdentityProvider{ + DisplayName: idp.DisplayName, + UID: idpResourceUID, + Transforms: pipeline, + }) + plog.Debug("loaded FederationDomain identity provider", + "federationDomain", federationDomain.Name, + "identityProviderDisplayName", idp.DisplayName, + "identityProviderResourceUID", idpResourceUID, + ) + } + + if len(idpNotFoundIndices) != 0 { + msgs := []string{} + for _, idpNotFoundIndex := range idpNotFoundIndices { + msgs = append(msgs, fmt.Sprintf(".spec.identityProviders[%d] with displayName %q", idpNotFoundIndex, + federationDomain.Spec.IdentityProviders[idpNotFoundIndex].DisplayName)) + } + conditions = append(conditions, &configv1alpha1.Condition{ + Type: typeIdentityProvidersFound, + Status: configv1alpha1.ConditionFalse, + Reason: reasonIdentityProvidersObjectRefsNotFound, + Message: fmt.Sprintf(".spec.identityProviders[].objectRef identifies resource(s) that cannot be found: %s", + strings.Join(msgs, ", ")), + }) + } else if len(federationDomain.Spec.IdentityProviders) != 0 { + conditions = append(conditions, &configv1alpha1.Condition{ + Type: typeIdentityProvidersFound, + Status: configv1alpha1.ConditionTrue, + Reason: reasonSuccess, + Message: "the resources specified by .spec.identityProviders[].objectRef were found", + }) + } + + // This is the constructor for any case other than the legacy case, including when there is an empty list of IDPs. + federationDomainIssuer, err := federationdomainproviders.NewFederationDomainIssuer(federationDomain.Spec.Issuer, federationDomainIdentityProviders) + conditions = appendIssuerURLValidCondition(err, conditions) + return federationDomainIssuer, conditions, nil +} + +func (c *federationDomainWatcherController) findIDPsUIDByObjectRef(objectRef corev1.TypedLocalObjectReference, namespace string) (types.UID, bool, error) { + var idpResourceUID types.UID + var foundIDP metav1.Object + var err error + + switch objectRef.Kind { + case "LDAPIdentityProvider": + foundIDP, err = c.ldapIdentityProviderInformer.Lister().LDAPIdentityProviders(namespace).Get(objectRef.Name) + case "ActiveDirectoryIdentityProvider": + foundIDP, err = c.activeDirectoryIdentityProviderInformer.Lister().ActiveDirectoryIdentityProviders(namespace).Get(objectRef.Name) + case "OIDCIdentityProvider": + foundIDP, err = c.oidcIdentityProviderInformer.Lister().OIDCIdentityProviders(namespace).Get(objectRef.Name) + default: + // TODO: handle an IDP type that we do not understand. + } + + switch { + case err == nil: + idpResourceUID = foundIDP.GetUID() + case errors.IsNotFound(err): + return "", false, nil + default: + // TODO: handle unexpected errors + } + return idpResourceUID, true, nil +} + +func (c *federationDomainWatcherController) makeTransformationPipelineForIdentityProvider( + idp configv1alpha1.FederationDomainIdentityProvider, + federationDomainName string, +) (*idtransform.TransformationPipeline, error) { + pipeline := idtransform.NewTransformationPipeline() + consts := &celtransformer.TransformationConstants{ + StringConstants: map[string]string{}, + StringListConstants: map[string][]string{}, + } + + // Read all the declared constants. + for _, c := range idp.Transforms.Constants { + switch c.Type { + case "string": + consts.StringConstants[c.Name] = c.StringValue + case "stringList": + consts.StringListConstants[c.Name] = c.StringListValue + default: + // TODO: this shouldn't really happen since the CRD validates it, but handle it as an error } } - return errorsutil.NewAggregate(errs) + // Compile all the expressions and add them to the pipeline. + for idx, e := range idp.Transforms.Expressions { + var rawTransform celtransformer.CELTransformation + switch e.Type { + case "username/v1": + rawTransform = &celtransformer.UsernameTransformation{Expression: e.Expression} + case "groups/v1": + rawTransform = &celtransformer.GroupsTransformation{Expression: e.Expression} + case "policy/v1": + rawTransform = &celtransformer.AllowAuthenticationPolicy{ + Expression: e.Expression, + RejectedAuthenticationMessage: e.Message, + } + default: + // TODO: this shouldn't really happen since the CRD validates it, but handle it as an error + } + compiledTransform, err := c.celTransformer.CompileTransformation(rawTransform, consts) + if err != nil { + // TODO: handle compile err + plog.Error("error compiling identity transformation", err, + "federationDomain", federationDomainName, + "idpDisplayName", idp.DisplayName, + "transformationIndex", idx, + "transformationType", e.Type, + "transformationExpression", e.Expression, + ) + } + pipeline.AppendTransformation(compiledTransform) + plog.Debug("successfully compiled identity transformation expression", + "type", e.Type, + "expr", e.Expression, + "policyMessage", e.Message, + ) + } + + // Run all the provided transform examples. If any fail, put errors on the FederationDomain status. + for idx, e := range idp.Transforms.Examples { + // TODO: use a real context param below + result, _ := pipeline.Evaluate(context.TODO(), e.Username, e.Groups) + // TODO: handle err + resultWasAuthRejected := !result.AuthenticationAllowed + if e.Expects.Rejected && !resultWasAuthRejected { //nolint:gocritic,nestif + // TODO: handle this failed example + plog.Warning("FederationDomain identity provider transformations example failed: expected authentication to be rejected but it was not", + "federationDomain", federationDomainName, + "idpDisplayName", idp.DisplayName, + "exampleIndex", idx, + "expectedRejected", e.Expects.Rejected, + "actualRejectedResult", resultWasAuthRejected, + "expectedMessage", e.Expects.Message, + "actualMessageResult", result.RejectedAuthenticationMessage, + ) + } else if !e.Expects.Rejected && resultWasAuthRejected { + // TODO: handle this failed example + plog.Warning("FederationDomain identity provider transformations example failed: expected authentication not to be rejected but it was rejected", + "federationDomain", federationDomainName, + "idpDisplayName", idp.DisplayName, + "exampleIndex", idx, + "expectedRejected", e.Expects.Rejected, + "actualRejectedResult", resultWasAuthRejected, + "expectedMessage", e.Expects.Message, + "actualMessageResult", result.RejectedAuthenticationMessage, + ) + } else if e.Expects.Rejected && resultWasAuthRejected && e.Expects.Message != result.RejectedAuthenticationMessage { + // TODO: when expected message is blank, then treat it like it expects the default message + // TODO: handle this failed example + plog.Warning("FederationDomain identity provider transformations example failed: expected a different authentication rejection message", + "federationDomain", federationDomainName, + "idpDisplayName", idp.DisplayName, + "exampleIndex", idx, + "expectedRejected", e.Expects.Rejected, + "actualRejectedResult", resultWasAuthRejected, + "expectedMessage", e.Expects.Message, + "actualMessageResult", result.RejectedAuthenticationMessage, + ) + } else if result.AuthenticationAllowed { + // In the case where the user expected the auth to be allowed and it was allowed, then compare + // the expected username and group names to the actual username and group names. + // TODO: when both of these fail, put both errors onto the status (not just the first one) + if e.Expects.Username != result.Username { + // TODO: handle this failed example + plog.Warning("FederationDomain identity provider transformations example failed: expected a different transformed username", + "federationDomain", federationDomainName, + "idpDisplayName", idp.DisplayName, + "exampleIndex", idx, + "expectedUsername", e.Expects.Username, + "actualUsernameResult", result.Username, + ) + } + if !stringSlicesEqual(e.Expects.Groups, result.Groups) { + // TODO: Do we need to make this insensitive to ordering, or should the transformations evaluator be changed to always return sorted group names at the end of the pipeline? + // TODO: What happens if the user did not write any group expectation? Treat it like expecting an empty list of groups? + // TODO: handle this failed example + plog.Warning("FederationDomain identity provider transformations example failed: expected a different transformed groups list", + "federationDomain", federationDomainName, + "idpDisplayName", idp.DisplayName, + "exampleIndex", idx, + "expectedGroups", e.Expects.Groups, + "actualGroupsResult", result.Groups, + ) + } + } + } + + return pipeline, nil +} + +func appendIssuerURLValidCondition(err error, conditions []*configv1alpha1.Condition) []*configv1alpha1.Condition { + if err != nil { + // Note that the FederationDomainIssuer constructors only validate the Issuer URL, + // so these are always issuer URL validation errors. + conditions = append(conditions, &configv1alpha1.Condition{ + Type: typeIssuerURLValid, + Status: configv1alpha1.ConditionFalse, + Reason: reasonInvalidIssuerURL, + Message: err.Error(), + }) + } else { + conditions = append(conditions, &configv1alpha1.Condition{ + Type: typeIssuerURLValid, + Status: configv1alpha1.ConditionTrue, + Reason: reasonSuccess, + Message: "spec.issuer is a valid URL", + }) + } + return conditions } func (c *federationDomainWatcherController) updateStatus(