diff --git a/go.mod b/go.mod index a403741fc0..93465e0433 100644 --- a/go.mod +++ b/go.mod @@ -10,12 +10,13 @@ require ( github.com/google/go-cmp v0.7.0 github.com/onsi/ginkgo/v2 v2.27.2 github.com/onsi/gomega v1.38.2 - github.com/openshift-eng/openshift-tests-extension v0.0.0-20251205182537-ff5553e56f33 + github.com/openshift-eng/openshift-tests-extension v0.0.0-20260408205138-ec501c2bf4a5 github.com/openshift/api v0.0.0-20260429122012-1180c0f5c3e9 - github.com/openshift/build-machinery-go v0.0.0-20250530140348-dc5b2804eeee + github.com/openshift/build-machinery-go v0.0.0-20251023084048-5d77c1a5e5af github.com/openshift/client-go v0.0.0-20260429123927-c81f86abfa6a github.com/openshift/library-go v0.0.0-20260506113849-32460ef09730 github.com/openshift/multi-operator-manager v0.0.0-20241205181422-20aa3906b99d + github.com/openshift/oauth-apiserver v0.0.0-20260430140618-160ac7fb4ea6 github.com/spf13/cobra v1.10.0 github.com/spf13/pflag v1.0.9 github.com/stretchr/testify v1.11.1 @@ -132,3 +133,7 @@ require ( ) replace github.com/onsi/ginkgo/v2 => github.com/openshift/onsi-ginkgo/v2 v2.6.1-0.20251001123353-fd5b1fb35db1 + +replace github.com/openshift/api => github.com/everettraven/openshift-api v0.0.0-20260507192020-4affa2ac4dea + +replace github.com/openshift/oauth-apiserver => github.com/everettraven/openshift-oauth-apiserver v0.0.0-20260512184245-97bf3a3f7bc8 diff --git a/go.sum b/go.sum index 672200d91a..e16241bd5a 100644 --- a/go.sum +++ b/go.sum @@ -43,6 +43,10 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/everettraven/openshift-api v0.0.0-20260507192020-4affa2ac4dea h1:2Bw06Lh1m4KaiQIvjhz5Q06cZ5OBsT1lEwT0CVrY4EE= +github.com/everettraven/openshift-api v0.0.0-20260507192020-4affa2ac4dea/go.mod h1:pyVjK0nZ4sRs4fuQVQ4rubsJdahI1PB94LnQ8sGdvxo= +github.com/everettraven/openshift-oauth-apiserver v0.0.0-20260512184245-97bf3a3f7bc8 h1:JDpc1ZIFFdHV9RB9M7PmzVtv7UwXfVJi7FRVGAEFozI= +github.com/everettraven/openshift-oauth-apiserver v0.0.0-20260512184245-97bf3a3f7bc8/go.mod h1:qPt46oOj0jFGgpabBjMazsgQXwrJ7KYBDwAuaesJLdE= github.com/felixge/fgprof v0.9.3/go.mod h1:RdbpDgzqYVh/T9fPELJyV7EYJuHB55UTEULNun8eiPw= github.com/felixge/fgprof v0.9.4 h1:ocDNwMFlnA0NU0zSB3I52xkO4sFXk80VK9lXjLClu88= github.com/felixge/fgprof v0.9.4/go.mod h1:yKl+ERSa++RYOs32d8K6WEXCB4uXdLls4ZaZPpayhMM= @@ -144,12 +148,10 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A= github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k= -github.com/openshift-eng/openshift-tests-extension v0.0.0-20251205182537-ff5553e56f33 h1:LJf6kWZQ36iako7WXRzdEa5XKrnyrAX8GBhlAcKRaZQ= -github.com/openshift-eng/openshift-tests-extension v0.0.0-20251205182537-ff5553e56f33/go.mod h1:6gkP5f2HL0meusT0Aim8icAspcD1cG055xxBZ9yC68M= -github.com/openshift/api v0.0.0-20260429122012-1180c0f5c3e9 h1:lZw6pYY7El1giNk1lYvkp6hLungiqwIOqLlH+Hm7w9g= -github.com/openshift/api v0.0.0-20260429122012-1180c0f5c3e9/go.mod h1:pyVjK0nZ4sRs4fuQVQ4rubsJdahI1PB94LnQ8sGdvxo= -github.com/openshift/build-machinery-go v0.0.0-20250530140348-dc5b2804eeee h1:+Sp5GGnjHDhT/a/nQ1xdp43UscBMr7G5wxsYotyhzJ4= -github.com/openshift/build-machinery-go v0.0.0-20250530140348-dc5b2804eeee/go.mod h1:8jcm8UPtg2mCAsxfqKil1xrmRMI3a+XU2TZ9fF8A7TE= +github.com/openshift-eng/openshift-tests-extension v0.0.0-20260408205138-ec501c2bf4a5 h1:FJmsOMCeFpAakgnVhHUoITcHLLW9/DrJJSAY1CZaLCA= +github.com/openshift-eng/openshift-tests-extension v0.0.0-20260408205138-ec501c2bf4a5/go.mod h1:6gkP5f2HL0meusT0Aim8icAspcD1cG055xxBZ9yC68M= +github.com/openshift/build-machinery-go v0.0.0-20251023084048-5d77c1a5e5af h1:UiYYMi/CCV+kwWrXuXfuUSOY2yNXOpWpNVgHc6aLQlE= +github.com/openshift/build-machinery-go v0.0.0-20251023084048-5d77c1a5e5af/go.mod h1:8jcm8UPtg2mCAsxfqKil1xrmRMI3a+XU2TZ9fF8A7TE= github.com/openshift/client-go v0.0.0-20260429123927-c81f86abfa6a h1:4GR6seHvlfv0rADe+LCQx63FqSExx6gaSo8uNiyWq+c= github.com/openshift/client-go v0.0.0-20260429123927-c81f86abfa6a/go.mod h1:Lm7X7aYbAaKhGsNhgYaowP7hiLKwfN/w0r+Q6VlQoI8= github.com/openshift/library-go v0.0.0-20260506113849-32460ef09730 h1:XuMXE12qMdjC8RnLd7o5IunE3o9zz17167Wat3lIxk4= diff --git a/pkg/controllers/externaloidc/externaloidc_controller.go b/pkg/controllers/externaloidc/externaloidc_controller.go index 1cae234fd6..02f4cc40f2 100644 --- a/pkg/controllers/externaloidc/externaloidc_controller.go +++ b/pkg/controllers/externaloidc/externaloidc_controller.go @@ -2,66 +2,46 @@ package externaloidc import ( "context" - "crypto/tls" - "crypto/x509" "encoding/json" - "errors" "fmt" - "io" - "net/http" - "net/url" - "strings" - "time" - celgo "github.com/google/cel-go/cel" - "github.com/google/cel-go/common/operators" configv1 "github.com/openshift/api/config/v1" "github.com/openshift/api/features" configinformers "github.com/openshift/client-go/config/informers/externalversions" configv1listers "github.com/openshift/client-go/config/listers/config/v1" + "github.com/openshift/cluster-authentication-operator/pkg/controllers/externaloidc/generation/kubeapiserver" + "github.com/openshift/cluster-authentication-operator/pkg/controllers/externaloidc/generation/oauthapiserver" "github.com/openshift/library-go/pkg/controller/factory" "github.com/openshift/library-go/pkg/operator/configobserver/featuregates" "github.com/openshift/library-go/pkg/operator/events" - "github.com/openshift/library-go/pkg/operator/resource/retry" "github.com/openshift/library-go/pkg/operator/v1helpers" - "golang.org/x/net/http/httpproxy" - exprpb "google.golang.org/genproto/googleapis/api/expr/v1alpha1" "k8s.io/apimachinery/pkg/api/equality" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - apiserverv1beta1 "k8s.io/apiserver/pkg/apis/apiserver/v1beta1" - authenticationcel "k8s.io/apiserver/pkg/authentication/cel" + "k8s.io/apimachinery/pkg/runtime" corev1ac "k8s.io/client-go/applyconfigurations/core/v1" corev1client "k8s.io/client-go/kubernetes/typed/core/v1" corev1listers "k8s.io/client-go/listers/core/v1" - "k8s.io/client-go/util/cert" - "k8s.io/utils/ptr" ) const ( - configNamespace = "openshift-config" - managedNamespace = "openshift-config-managed" - targetAuthConfigCMName = "auth-config" - authConfigDataKey = "auth-config.json" - oidcDiscoveryEndpointPath = "/.well-known/openid-configuration" - kindAuthenticationConfiguration = "AuthenticationConfiguration" + configNamespace = "openshift-config" + managedNamespace = "openshift-config-managed" + targetAuthConfigCMName = "auth-config" + authConfigDataKey = "auth-config.json" ) -// oidcGenerationState holds compilation results gathered during JWT generation -// that are needed for cross-field validation (e.g. email_verified enforcement). -type oidcGenerationState struct { - usernameResult *authenticationcel.CompilationResult - extraResults []authenticationcel.CompilationResult - claimValidationResults []authenticationcel.CompilationResult +type authConfigGenerator interface { + GenerateAuthenticationConfiguration(*configv1.Authentication) (runtime.Object, error) } type externalOIDCController struct { - name string - eventName string - authLister configv1listers.AuthenticationLister - configMapLister corev1listers.ConfigMapLister - configMaps corev1client.ConfigMapsGetter - featureGates featuregates.FeatureGate + name string + eventName string + authLister configv1listers.AuthenticationLister + configMapLister corev1listers.ConfigMapLister + configMaps corev1client.ConfigMapsGetter + authConfigGenerator authConfigGenerator } func NewExternalOIDCController( @@ -72,14 +52,22 @@ func NewExternalOIDCController( recorder events.Recorder, featureGates featuregates.FeatureGate, ) factory.Controller { + var authCfgGenerator authConfigGenerator + + authCfgGenerator = kubeapiserver.NewAuthenticationConfigurationGenerator(kubeInformersForNamespaces.ConfigMapLister(), featureGates) + + if featureGates.Enabled(features.FeatureGateExternalOIDCExternalClaimsSourcing) { + authCfgGenerator = oauthapiserver.NewAuthenticationConfigurationGenerator(kubeInformersForNamespaces.ConfigMapLister(), kubeInformersForNamespaces.SecretLister(), featureGates) + } + c := &externalOIDCController{ name: "ExternalOIDCController", eventName: "external-oidc-controller", - authLister: configInformer.Config().V1().Authentications().Lister(), - configMapLister: kubeInformersForNamespaces.ConfigMapLister(), - configMaps: configMaps, - featureGates: featureGates, + authLister: configInformer.Config().V1().Authentications().Lister(), + configMapLister: kubeInformersForNamespaces.ConfigMapLister(), + configMaps: configMaps, + authConfigGenerator: authCfgGenerator, } return factory.New().WithInformers( @@ -107,12 +95,12 @@ func (c *externalOIDCController) sync(ctx context.Context, syncCtx factory.SyncC return c.deleteAuthConfig(ctx, syncCtx) } - authConfig, err := c.generateAuthConfig(*auth) + authConfig, err := c.authConfigGenerator.GenerateAuthenticationConfiguration(auth) if err != nil { return err } - expectedApplyConfig, err := getExpectedApplyConfig(*authConfig) + expectedApplyConfig, err := getExpectedApplyConfig(authConfig) if err != nil { return err } @@ -126,10 +114,6 @@ func (c *externalOIDCController) sync(ctx context.Context, syncCtx factory.SyncC return nil } - if err := validateAuthConfig(*authConfig); err != nil { - return fmt.Errorf("auth config validation failed: %v", err) - } - if _, err := c.configMaps.ConfigMaps(managedNamespace).Apply(ctx, expectedApplyConfig, metav1.ApplyOptions{FieldManager: c.name, Force: true}); err != nil { return fmt.Errorf("could not apply changes to auth configmap %s/%s: %v", managedNamespace, targetAuthConfigCMName, err) } @@ -157,456 +141,9 @@ func (c *externalOIDCController) deleteAuthConfig(ctx context.Context, syncCtx f return nil } -// generateAuthConfig creates a structured JWT AuthenticationConfiguration for OIDC -// from the configuration found in the authentication/cluster resource. -func (c *externalOIDCController) generateAuthConfig(auth configv1.Authentication) (*apiserverv1beta1.AuthenticationConfiguration, error) { - authConfig := apiserverv1beta1.AuthenticationConfiguration{ - TypeMeta: metav1.TypeMeta{ - Kind: kindAuthenticationConfiguration, - APIVersion: apiserverv1beta1.ConfigSchemeGroupVersion.String(), - }, - } - - errs := []error{} - for _, provider := range auth.Spec.OIDCProviders { - jwt, err := generateJWTForProvider(provider, c.configMapLister, c.featureGates, auth.Spec.ServiceAccountIssuer) - if err != nil { - errs = append(errs, err) - continue - } - - authConfig.JWT = append(authConfig.JWT, jwt) - } - - if len(errs) > 0 { - return nil, errors.Join(errs...) - } - - return &authConfig, nil -} - -func generateJWTForProvider(provider configv1.OIDCProvider, configMapLister corev1listers.ConfigMapLister, featureGates featuregates.FeatureGate, serviceAccountIssuer string) (apiserverv1beta1.JWTAuthenticator, error) { - out := apiserverv1beta1.JWTAuthenticator{} - - issuer, err := generateIssuer(provider.Issuer, configMapLister, serviceAccountIssuer) - if err != nil { - return apiserverv1beta1.JWTAuthenticator{}, fmt.Errorf("generating issuer for provider %q: %v", provider.Name, err) - } - - state := &oidcGenerationState{} - - claimMappings, err := generateClaimMappings(provider.ClaimMappings, issuer.URL, featureGates, state) - if err != nil { - return apiserverv1beta1.JWTAuthenticator{}, fmt.Errorf("generating claimMappings for provider %q: %v", provider.Name, err) - } - - claimValidationRules, err := generateClaimValidationRules(state, provider.ClaimValidationRules...) - if err != nil { - return apiserverv1beta1.JWTAuthenticator{}, fmt.Errorf("generating claimValidationRules for provider %q: %v", provider.Name, err) - } - - if featureGates.Enabled(features.FeatureGateExternalOIDCWithUpstreamParity) { - if err := validateEmailVerifiedUsage( - state.usernameResult, - state.extraResults, - state.claimValidationResults, - ); err != nil { - return apiserverv1beta1.JWTAuthenticator{}, fmt.Errorf("validating email claim usage for provider %q: %v", provider.Name, err) - } - var userValidationRules []apiserverv1beta1.UserValidationRule - userValidationRules, err = generateUserValidationRules(provider.UserValidationRules) - if err != nil { - return apiserverv1beta1.JWTAuthenticator{}, fmt.Errorf("generating userValidationRules for provider %q: %v", provider.Name, err) - } - out.UserValidationRules = userValidationRules - } - out.Issuer = issuer - out.ClaimMappings = claimMappings - out.ClaimValidationRules = claimValidationRules - - return out, nil -} - -func generateIssuer(issuer configv1.TokenIssuer, configMapLister corev1listers.ConfigMapLister, serviceAccountIssuer string) (apiserverv1beta1.Issuer, error) { - out := apiserverv1beta1.Issuer{} - - if len(serviceAccountIssuer) > 0 { - if issuer.URL == serviceAccountIssuer { - return apiserverv1beta1.Issuer{}, errors.New("issuer url cannot overlap with the ServiceAccount issuer url") - } - } - - out.URL = issuer.URL - out.AudienceMatchPolicy = apiserverv1beta1.AudienceMatchPolicyMatchAny - - for _, audience := range issuer.Audiences { - out.Audiences = append(out.Audiences, string(audience)) - } - if len(issuer.DiscoveryURL) > 0 { - // Validate the URL scheme - u, err := url.Parse(issuer.DiscoveryURL) - if err != nil { - return apiserverv1beta1.Issuer{}, fmt.Errorf("invalid discovery URL: %v", err) - } - if strings.TrimRight(issuer.DiscoveryURL, "/") == strings.TrimRight(issuer.URL, "/") { - return apiserverv1beta1.Issuer{}, fmt.Errorf("discovery URL must not be identical to issuer URL") - } - if u.Scheme != "https" { - return apiserverv1beta1.Issuer{}, fmt.Errorf("discovery URL must use https, got %q", u.Scheme) - } - if u.Host == "" { - return apiserverv1beta1.Issuer{}, fmt.Errorf("discovery URL must include a host") - } - if u.User != nil { - return apiserverv1beta1.Issuer{}, fmt.Errorf("discovery URL must not contain user info") - } - if len(u.RawQuery) > 0 { - return apiserverv1beta1.Issuer{}, fmt.Errorf("discovery URL must not contain a query string") - } - if len(u.Fragment) > 0 { - return apiserverv1beta1.Issuer{}, fmt.Errorf("discovery URL must not contain a fragment") - } - out.DiscoveryURL = &issuer.DiscoveryURL - } - if len(issuer.CertificateAuthority.Name) > 0 { - ca, err := getCertificateAuthorityFromConfigMap(issuer.CertificateAuthority.Name, configMapLister) - if err != nil { - return apiserverv1beta1.Issuer{}, fmt.Errorf("getting CertificateAuthority for issuer: %v", err) - } - out.CertificateAuthority = ca - } - - return out, nil -} - -func getCertificateAuthorityFromConfigMap(name string, configMapLister corev1listers.ConfigMapLister) (string, error) { - caConfigMap, err := configMapLister.ConfigMaps(configNamespace).Get(name) - if err != nil { - return "", fmt.Errorf("could not retrieve auth configmap %s/%s to check CA bundle: %v", configNamespace, name, err) - } - - caData, ok := caConfigMap.Data["ca-bundle.crt"] - if !ok || len(caData) == 0 { - return "", fmt.Errorf("configmap %s/%s key \"ca-bundle.crt\" missing or empty", configNamespace, name) - } - - return caData, nil -} - -func generateClaimMappings(claimMappings configv1.TokenClaimMappings, issuerURL string, featureGates featuregates.FeatureGate, state *oidcGenerationState) (apiserverv1beta1.ClaimMappings, error) { - out := apiserverv1beta1.ClaimMappings{} - - username, usernameResult, err := generateUsernameClaimMapping(claimMappings.Username, issuerURL, featureGates) - if err != nil { - return apiserverv1beta1.ClaimMappings{}, fmt.Errorf("generating username claim mapping: %v", err) - } - state.usernameResult = usernameResult - - groups, err := generateGroupsClaimMapping(claimMappings.Groups, featureGates) - if err != nil { - return apiserverv1beta1.ClaimMappings{}, fmt.Errorf("generating group claim mapping: %v", err) - } - out.Username = username - out.Groups = groups - - if featureGates.Enabled(features.FeatureGateExternalOIDCWithAdditionalClaimMappings) { - uid, err := generateUIDClaimMapping(claimMappings.UID) - if err != nil { - return apiserverv1beta1.ClaimMappings{}, fmt.Errorf("generating uid claim mapping: %v", err) - } - - extras, extraResults, err := generateExtraClaimMapping(claimMappings.Extra...) - if err != nil { - return apiserverv1beta1.ClaimMappings{}, fmt.Errorf("generating extra claim mapping: %v", err) - } - - out.UID = uid - out.Extra = extras - state.extraResults = extraResults - } - - return out, nil -} - -func generateUsernameClaimMapping(usernameClaimMapping configv1.UsernameClaimMapping, issuerURL string, featureGates featuregates.FeatureGate) (apiserverv1beta1.PrefixedClaimOrExpression, *authenticationcel.CompilationResult, error) { - if featureGates.Enabled(features.FeatureGateExternalOIDCWithUpstreamParity) { - return generateUsernameClaimMappingWithParity(usernameClaimMapping, issuerURL) - } - return generateUsernameClaimMappingLegacy(usernameClaimMapping, issuerURL) -} - -func generateUsernameClaimMappingWithParity(usernameClaimMapping configv1.UsernameClaimMapping, issuerURL string) (apiserverv1beta1.PrefixedClaimOrExpression, *authenticationcel.CompilationResult, error) { - out := apiserverv1beta1.PrefixedClaimOrExpression{} - - if len(usernameClaimMapping.Expression) == 0 && len(usernameClaimMapping.Claim) == 0 { - return out, nil, fmt.Errorf("username claim mapping is required and either claim or expression must be set") - } - - if len(usernameClaimMapping.Expression) > 0 && len(usernameClaimMapping.Claim) > 0 { - return out, nil, fmt.Errorf("username claim mapping must not set both claim and expression") - } - - if len(usernameClaimMapping.Expression) > 0 && usernameClaimMapping.PrefixPolicy == configv1.Prefix { - return out, nil, fmt.Errorf("username claim mappings cannot have a prefix set when using an expression based mapping. If you want to set a prefix while using an expression mapping, set the prefix in the expression") - } - - if len(usernameClaimMapping.Expression) > 0 { - result, err := validateCELExpression(&authenticationcel.ClaimMappingExpression{ - Expression: usernameClaimMapping.Expression, - }) - if err != nil { - return out, nil, fmt.Errorf("invalid CEL expression: %v", err) - } - out.Expression = usernameClaimMapping.Expression - return out, &result, nil - } - - if len(usernameClaimMapping.Claim) > 0 { - out.Claim = usernameClaimMapping.Claim - - // prefix can only be set when using a direct claim name, so only attempt to set it - // if we are certain we are using a direct claim reference and not an expression - switch usernameClaimMapping.PrefixPolicy { - case configv1.Prefix: - if usernameClaimMapping.Prefix == nil { - return out, nil, fmt.Errorf("nil username prefix while policy expects one") - } - out.Prefix = &usernameClaimMapping.Prefix.PrefixString - case configv1.NoPrefix: - out.Prefix = ptr.To("") - case configv1.NoOpinion: - prefix := "" - if usernameClaimMapping.Claim != "email" { - prefix = issuerURL + "#" - } - out.Prefix = &prefix - default: - return out, nil, fmt.Errorf("invalid username prefix policy: %s", usernameClaimMapping.PrefixPolicy) - } - } - - return out, nil, nil -} - -func generateUsernameClaimMappingLegacy(usernameClaimMapping configv1.UsernameClaimMapping, issuerURL string) (apiserverv1beta1.PrefixedClaimOrExpression, *authenticationcel.CompilationResult, error) { - out := apiserverv1beta1.PrefixedClaimOrExpression{} - - if len(usernameClaimMapping.Claim) == 0 { - return out, nil, fmt.Errorf("username claim is required but an empty value was provided") - } - out.Claim = usernameClaimMapping.Claim - - switch usernameClaimMapping.PrefixPolicy { - case configv1.Prefix: - if usernameClaimMapping.Prefix == nil { - return out, nil, fmt.Errorf("nil username prefix while policy expects one") - } - out.Prefix = &usernameClaimMapping.Prefix.PrefixString - case configv1.NoPrefix: - out.Prefix = ptr.To("") - case configv1.NoOpinion: - prefix := "" - if usernameClaimMapping.Claim != "email" { - prefix = issuerURL + "#" - } - out.Prefix = &prefix - default: - return out, nil, fmt.Errorf("invalid username prefix policy: %s", usernameClaimMapping.PrefixPolicy) - } - - return out, nil, nil -} - -func generateGroupsClaimMapping(groupsMapping configv1.PrefixedClaimMapping, featureGates featuregates.FeatureGate) (apiserverv1beta1.PrefixedClaimOrExpression, error) { - out := apiserverv1beta1.PrefixedClaimOrExpression{} - if featureGates.Enabled(features.FeatureGateExternalOIDCWithUpstreamParity) { - if len(groupsMapping.Expression) > 0 && len(groupsMapping.Claim) > 0 { - return out, fmt.Errorf("groups claim mapping must not set both claim and expression") - } - if len(groupsMapping.Expression) > 0 && len(groupsMapping.Prefix) > 0 { - return apiserverv1beta1.PrefixedClaimOrExpression{}, fmt.Errorf("groups claim mapping must not set prefix when expression is set") - } - - if len(groupsMapping.Expression) > 0 { - if _, err := validateCELExpression(&authenticationcel.ClaimMappingExpression{ - Expression: groupsMapping.Expression, - }); err != nil { - return apiserverv1beta1.PrefixedClaimOrExpression{}, fmt.Errorf("invalid CEL expression: %v", err) - } - out.Expression = groupsMapping.Expression - return out, nil - } - } - - out.Claim = groupsMapping.Claim - out.Prefix = &groupsMapping.Prefix - - return out, nil -} - -func generateUIDClaimMapping(uid *configv1.TokenClaimOrExpressionMapping) (apiserverv1beta1.ClaimOrExpression, error) { - out := apiserverv1beta1.ClaimOrExpression{} - - // UID mapping can only specify either claim or expression, not both. - // This should be rejected at admission time of the authentications.config.openshift.io CRD. - // Even though this is the case, we still perform a runtime validation to ensure we never - // attempt to create an invalid configuration. - // If neither claim or expression is specified, default the claim to "sub" - switch { - case uid == nil: - out.Claim = "sub" - case len(uid.Claim) > 0 && len(uid.Expression) == 0: - out.Claim = uid.Claim - case len(uid.Expression) > 0 && len(uid.Claim) == 0: - if _, err := validateCELExpression(&authenticationcel.ClaimMappingExpression{ - Expression: uid.Expression, - }); err != nil { - return apiserverv1beta1.ClaimOrExpression{}, fmt.Errorf("validating expression: %v", err) - } - out.Expression = uid.Expression - case len(uid.Claim) > 0 && len(uid.Expression) > 0: - return apiserverv1beta1.ClaimOrExpression{}, fmt.Errorf("uid mapping must set either claim or expression, not both: %v", uid) - default: - return apiserverv1beta1.ClaimOrExpression{}, fmt.Errorf("unable to handle uid mapping: %v", uid) - } - - return out, nil -} - -func generateExtraClaimMapping(extraMappings ...configv1.ExtraMapping) ([]apiserverv1beta1.ExtraMapping, []authenticationcel.CompilationResult, error) { - out := []apiserverv1beta1.ExtraMapping{} - var compilationResults []authenticationcel.CompilationResult - errs := []error{} - for _, extraMapping := range extraMappings { - extra, result, err := generateExtraMapping(extraMapping) - if err != nil { - errs = append(errs, err) - continue - } - out = append(out, extra) - if result != nil { - compilationResults = append(compilationResults, *result) - } - } - if len(errs) > 0 { - return nil, nil, errors.Join(errs...) - } - return out, compilationResults, nil -} - -func generateExtraMapping(extraMapping configv1.ExtraMapping) (apiserverv1beta1.ExtraMapping, *authenticationcel.CompilationResult, error) { - out := apiserverv1beta1.ExtraMapping{} - - if len(extraMapping.Key) == 0 { - return apiserverv1beta1.ExtraMapping{}, nil, fmt.Errorf("extra mapping must set a key, but none was provided: %v", extraMapping) - } - - if len(extraMapping.ValueExpression) == 0 { - return apiserverv1beta1.ExtraMapping{}, nil, fmt.Errorf("extra mapping must set a valueExpression, but none was provided: %v", extraMapping) - } - - result, err := validateCELExpression(&authenticationcel.ExtraMappingExpression{ - Key: extraMapping.Key, - Expression: extraMapping.ValueExpression, - }) - if err != nil { - return apiserverv1beta1.ExtraMapping{}, nil, fmt.Errorf("validating expression: %v", err) - } - - out.Key = extraMapping.Key - out.ValueExpression = extraMapping.ValueExpression - - return out, &result, nil -} - -func generateClaimValidationRules(state *oidcGenerationState, claimValidationRules ...configv1.TokenClaimValidationRule) ([]apiserverv1beta1.ClaimValidationRule, error) { - out := []apiserverv1beta1.ClaimValidationRule{} - errs := []error{} - for _, claimValidationRule := range claimValidationRules { - rule, result, err := generateClaimValidationRule(claimValidationRule) - if err != nil { - errs = append(errs, fmt.Errorf("generating claimValidationRule: %v", err)) - continue - } - out = append(out, rule) - if result != nil { - state.claimValidationResults = append(state.claimValidationResults, *result) - } - } - if len(errs) > 0 { - return nil, errors.Join(errs...) - } - return out, nil -} - -func generateClaimValidationRule(claimValidationRule configv1.TokenClaimValidationRule) (apiserverv1beta1.ClaimValidationRule, *authenticationcel.CompilationResult, error) { - out := apiserverv1beta1.ClaimValidationRule{} - switch claimValidationRule.Type { - case configv1.TokenValidationRuleTypeRequiredClaim: - if claimValidationRule.RequiredClaim == nil { - return apiserverv1beta1.ClaimValidationRule{}, nil, fmt.Errorf("claimValidationRule.type is %s and requiredClaim is not set", configv1.TokenValidationRuleTypeRequiredClaim) - } - out.Claim = claimValidationRule.RequiredClaim.Claim - out.RequiredValue = claimValidationRule.RequiredClaim.RequiredValue - case configv1.TokenValidationRuleTypeCEL: - if len(claimValidationRule.CEL.Expression) == 0 { - return apiserverv1beta1.ClaimValidationRule{}, nil, fmt.Errorf("claimValidationRule.type is %s and expression is not set", configv1.TokenValidationRuleTypeCEL) - } - result, err := validateCELExpression(&authenticationcel.ClaimValidationCondition{ - Expression: claimValidationRule.CEL.Expression, - }) - if err != nil { - return apiserverv1beta1.ClaimValidationRule{}, nil, fmt.Errorf("invalid CEL expression: %v", err) - } - out.Expression = claimValidationRule.CEL.Expression - out.Message = claimValidationRule.CEL.Message - return out, &result, nil - default: - return apiserverv1beta1.ClaimValidationRule{}, nil, fmt.Errorf("unknown claimValidationRule type %q", claimValidationRule.Type) - } - return out, nil, nil -} -func generateUserValidationRule(rule configv1.TokenUserValidationRule) (apiserverv1beta1.UserValidationRule, error) { - if len(rule.Expression) == 0 { - return apiserverv1beta1.UserValidationRule{}, fmt.Errorf("userValidationRule expression must be non-empty") - } - - // validate CEL expression - if _, err := validateUserCELExpression(&authenticationcel.UserValidationCondition{ - Expression: rule.Expression, - }); err != nil { - return apiserverv1beta1.UserValidationRule{}, fmt.Errorf("invalid CEL expression: %v", err) - } - - return apiserverv1beta1.UserValidationRule{ - Expression: rule.Expression, - Message: rule.Message, - }, nil -} - -func generateUserValidationRules(rules []configv1.TokenUserValidationRule) ([]apiserverv1beta1.UserValidationRule, error) { - out := []apiserverv1beta1.UserValidationRule{} - errs := []error{} - - for _, r := range rules { - uvr, err := generateUserValidationRule(r) - if err != nil { - errs = append(errs, fmt.Errorf("generating userValidationRule: %v", err)) - continue - } - out = append(out, uvr) - } - - if len(errs) > 0 { - return nil, errors.Join(errs...) - } - - return out, nil -} - // getExpectedApplyConfig serializes the input authConfig into JSON and creates an apply configuration // for a configmap with the serialized authConfig in the right key. -func getExpectedApplyConfig(authConfig apiserverv1beta1.AuthenticationConfiguration) (*corev1ac.ConfigMapApplyConfiguration, error) { +func getExpectedApplyConfig(authConfig any) (*corev1ac.ConfigMapApplyConfiguration, error) { authConfigBytes, err := json.Marshal(authConfig) if err != nil { return nil, fmt.Errorf("could not marshal auth config into JSON: %v", err) @@ -637,230 +174,3 @@ func (c *externalOIDCController) getExistingApplyConfig() (*corev1ac.ConfigMapAp return existingCMApplyConfig, nil } - -// validateAuthConfig performs validations that are not done at the server-side, -// including validation that the provided CA cert (or system CAs if not specified) can be used for -// TLS cert verification. -func validateAuthConfig(auth apiserverv1beta1.AuthenticationConfiguration) error { - for _, jwt := range auth.JWT { - var caCertPool *x509.CertPool - var err error - if len(jwt.Issuer.CertificateAuthority) > 0 { - caCertPool, err = cert.NewPoolFromBytes([]byte(jwt.Issuer.CertificateAuthority)) - if err != nil { - return fmt.Errorf("issuer CA is invalid: %v", err) - } - } - - // make sure we can access the issuer with the given cert pool (system CAs used if pool is empty) - if err := validateCACert(jwt.Issuer.URL, caCertPool); err != nil { - certMessage := "using the specified CA cert" - if caCertPool == nil { - certMessage = "using the system CAs" - } - return fmt.Errorf("could not validate IDP URL %s: %v", certMessage, err) - } - } - - return nil -} - -// validateCACert makes a request to the provider's well-known endpoint using the -// specified CA cert pool to validate that the certs in the pool match the host. -func validateCACert(hostURL string, caCertPool *x509.CertPool) error { - client := &http.Client{ - Transport: &http.Transport{ - TLSClientConfig: &tls.Config{RootCAs: caCertPool}, - Proxy: func(*http.Request) (*url.URL, error) { - if proxyConfig := httpproxy.FromEnvironment(); len(proxyConfig.HTTPSProxy) > 0 { - return url.Parse(proxyConfig.HTTPSProxy) - } - return nil, nil - }, - }, - Timeout: 5 * time.Second, - } - - wellKnown := strings.TrimSuffix(hostURL, "/") + oidcDiscoveryEndpointPath - req, err := http.NewRequest(http.MethodGet, wellKnown, nil) - if err != nil { - return fmt.Errorf("could not create well-known HTTP request: %v", err) - } - - var resp *http.Response - var connErr error - retryCtx, cancel := context.WithTimeout(req.Context(), 10*time.Second) - defer cancel() - retry.RetryOnConnectionErrors(retryCtx, func(ctx context.Context) (done bool, err error) { - resp, connErr = client.Do(req) - return connErr == nil, connErr - }) - if connErr != nil { - return fmt.Errorf("GET well-known error: %v", connErr) - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - body, err := io.ReadAll(resp.Body) - if err != nil { - return fmt.Errorf("unable to read response body; HTTP status: %s; error: %v", resp.Status, err) - } - - return fmt.Errorf("unexpected well-known status code %s: %s", resp.Status, body) - } - - return nil -} - -// validateCELExpression validates a CEL expression using the provided expression accessor. -// It uses the default authentication CEL compiler that the KAS uses and thus defaults to -// validating CEL expressions based on the version of the k8s dependencies used by the -// cluster-authentication-operator. -func validateCELExpression(expressionAccessor authenticationcel.ExpressionAccessor) (authenticationcel.CompilationResult, error) { - return authenticationcel.NewDefaultCompiler().CompileClaimsExpression(expressionAccessor) -} - -// validateUserCELExpression validates a user CEL expression using the user.* scope. -func validateUserCELExpression(expressionAccessor authenticationcel.ExpressionAccessor) (authenticationcel.CompilationResult, error) { - return authenticationcel.NewDefaultCompiler().CompileUserExpression(expressionAccessor) -} - -// validateEmailVerifiedUsage enforces that when claims.email is used in the -// username expression, claims.email_verified must be referenced in at least -// one of: username.expression, extra[*].valueExpression, or -// claimValidationRules[*].cel.expression. -// This mirrors the upstream KAS validation logic. -func validateEmailVerifiedUsage( - usernameResult *authenticationcel.CompilationResult, - extraResults []authenticationcel.CompilationResult, - claimValidationResults []authenticationcel.CompilationResult, -) error { - if usernameResult == nil { - return nil - } - - if !usesEmailClaim(usernameResult.AST) { - return nil - } - - if usesEmailVerifiedClaim(usernameResult.AST) || anyUsesEmailVerifiedClaim(extraResults) || anyUsesEmailVerifiedClaim(claimValidationResults) { - return nil - } - - return fmt.Errorf("claims.email_verified must be used in claimMappings.username.expression or claimMappings.extra[*].valueExpression or claimValidationRules[*].expression when claims.email is used in claimMappings.username.expression") -} - -// usesEmailClaim, usesEmailVerifiedClaim, anyUsesEmailVerifiedClaim, hasSelectExp, -// isIdentOperand, and isConstField are copied from the upstream Kubernetes apiserver -// CEL validation logic introduced in https://github.com/kubernetes/kubernetes/pull/123737 (commit 121607e): -// https://github.com/kubernetes/kubernetes/blob/bfb362c57578518bed8e08a56a7318bab9b57429/staging/src/k8s.io/apiserver/pkg/apis/apiserver/validation/validation.go#L443 -func usesEmailClaim(ast *celgo.Ast) bool { - if ast == nil { - return false - } - return hasSelectExp(ast.Expr(), "claims", "email") -} - -func usesEmailVerifiedClaim(ast *celgo.Ast) bool { - if ast == nil { - return false - } - return hasSelectExp(ast.Expr(), "claims", "email_verified") -} - -func anyUsesEmailVerifiedClaim(results []authenticationcel.CompilationResult) bool { - for _, result := range results { - if usesEmailVerifiedClaim(result.AST) { - return true - } - } - return false -} - -func hasSelectExp(exp *exprpb.Expr, operand, field string) bool { - if exp == nil { - return false - } - switch e := exp.ExprKind.(type) { - case *exprpb.Expr_ConstExpr, - *exprpb.Expr_IdentExpr: - return false - case *exprpb.Expr_SelectExpr: - s := e.SelectExpr - if s == nil { - return false - } - if isIdentOperand(s.Operand, operand) && s.Field == field { - return true - } - return hasSelectExp(s.Operand, operand, field) - case *exprpb.Expr_CallExpr: - c := e.CallExpr - if c == nil { - return false - } - if c.Target == nil && c.Function == operators.OptSelect && len(c.Args) == 2 && - isIdentOperand(c.Args[0], operand) && isConstField(c.Args[1], field) { - return true - } - for _, arg := range c.Args { - if hasSelectExp(arg, operand, field) { - return true - } - } - return hasSelectExp(c.Target, operand, field) - case *exprpb.Expr_ListExpr: - l := e.ListExpr - if l == nil { - return false - } - for _, element := range l.Elements { - if hasSelectExp(element, operand, field) { - return true - } - } - return false - case *exprpb.Expr_StructExpr: - s := e.StructExpr - if s == nil { - return false - } - for _, entry := range s.Entries { - if hasSelectExp(entry.GetMapKey(), operand, field) { - return true - } - if hasSelectExp(entry.Value, operand, field) { - return true - } - } - return false - case *exprpb.Expr_ComprehensionExpr: - c := e.ComprehensionExpr - if c == nil { - return false - } - return hasSelectExp(c.IterRange, operand, field) || - hasSelectExp(c.AccuInit, operand, field) || - hasSelectExp(c.LoopCondition, operand, field) || - hasSelectExp(c.LoopStep, operand, field) || - hasSelectExp(c.Result, operand, field) - default: - return false - } -} - -func isIdentOperand(exp *exprpb.Expr, operand string) bool { - if len(operand) == 0 { - return false - } - id := exp.GetIdentExpr() - return id != nil && id.Name == operand -} - -func isConstField(exp *exprpb.Expr, field string) bool { - if len(field) == 0 { - return false - } - c := exp.GetConstExpr() - return c != nil && c.GetStringValue() == field -} diff --git a/pkg/controllers/externaloidc/externaloidc_controller_test.go b/pkg/controllers/externaloidc/externaloidc_controller_test.go index b97cf3ea60..2fa623139a 100644 --- a/pkg/controllers/externaloidc/externaloidc_controller_test.go +++ b/pkg/controllers/externaloidc/externaloidc_controller_test.go @@ -13,25 +13,22 @@ import ( "crypto/x509/pkix" "encoding/json" "encoding/pem" + "errors" "fmt" "math/big" "net" - "net/http" - "net/http/httptest" "strings" "testing" "time" configv1 "github.com/openshift/api/config/v1" - "github.com/openshift/api/features" configv1listers "github.com/openshift/client-go/config/listers/config/v1" "github.com/openshift/library-go/pkg/controller/factory" - "github.com/openshift/library-go/pkg/operator/configobserver/featuregates" "github.com/openshift/library-go/pkg/operator/events" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/equality" - "k8s.io/apimachinery/pkg/api/errors" + apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/util/diff" @@ -47,80 +44,16 @@ import ( ) var ( - baseCACert, baseCAPrivateKey, testCertData = func() (*x509.Certificate, crypto.Signer, string) { - cert, key, err := generateCAKeyPair() - if err != nil { - panic(err) - } - return cert, key, string(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw})) - }() - - baseAuthResource = *newAuthWithSpec(configv1.AuthenticationSpec{ - Type: configv1.AuthenticationTypeOIDC, - OIDCProviders: []configv1.OIDCProvider{ - { - Name: "test-oidc-provider", - Issuer: configv1.TokenIssuer{ - CertificateAuthority: configv1.ConfigMapNameReference{Name: "oidc-ca-bundle"}, - Audiences: []configv1.TokenAudience{"my-test-aud", "another-aud"}, - }, - OIDCClients: []configv1.OIDCClientConfig{ - { - ComponentName: "console", - ComponentNamespace: "openshift-console", - ClientID: "console-oidc-client", - }, - { - ComponentName: "kube-apiserver", - ComponentNamespace: "openshift-kube-apiserver", - ClientID: "test-oidc-client", - }, - }, - ClaimMappings: configv1.TokenClaimMappings{ - Username: configv1.UsernameClaimMapping{ - Claim: "username", - PrefixPolicy: configv1.Prefix, - Prefix: &configv1.UsernamePrefix{ - PrefixString: "oidc-user:", - }, - }, - Groups: configv1.PrefixedClaimMapping{ - TokenClaimMapping: configv1.TokenClaimMapping{ - Claim: "groups", - }, - Prefix: "oidc-group:", - }, - }, - ClaimValidationRules: []configv1.TokenClaimValidationRule{ - { - Type: configv1.TokenValidationRuleTypeRequiredClaim, - RequiredClaim: &configv1.TokenRequiredClaim{ - Claim: "username", - RequiredValue: "test-username", - }, - }, - { - Type: configv1.TokenValidationRuleTypeRequiredClaim, - RequiredClaim: &configv1.TokenRequiredClaim{ - Claim: "email", - RequiredValue: "test-email", - }, - }, - }, - }, - }, - }) - baseAuthConfig = apiserverv1beta1.AuthenticationConfiguration{ TypeMeta: metav1.TypeMeta{ - Kind: kindAuthenticationConfiguration, + Kind: "AuthenticationConfiguration", APIVersion: apiserverv1beta1.ConfigSchemeGroupVersion.String(), }, JWT: []apiserverv1beta1.JWTAuthenticator{ { Issuer: apiserverv1beta1.Issuer{ Audiences: []string{"my-test-aud", "another-aud"}, - CertificateAuthority: testCertData, + CertificateAuthority: "fake-ca-cert", AudienceMatchPolicy: apiserverv1beta1.AudienceMatchPolicyMatchAny, }, ClaimMappings: apiserverv1beta1.ClaimMappings{ @@ -147,7 +80,7 @@ var ( }, } - baseAuthConfigJSON = fmt.Sprintf(`{"kind":"%s","apiVersion":"apiserver.config.k8s.io/v1beta1","jwt":[{"issuer":{"url":"$URL","certificateAuthority":"%s","audiences":["my-test-aud","another-aud"],"audienceMatchPolicy":"MatchAny"},"claimValidationRules":[{"claim":"username","requiredValue":"test-username"},{"claim":"email","requiredValue":"test-email"}],"claimMappings":{"username":{"claim":"username","prefix":"oidc-user:"},"groups":{"claim":"groups","prefix":"oidc-group:"},"uid":{}}}]}`, kindAuthenticationConfiguration, strings.ReplaceAll(testCertData, "\n", "\\n")) + baseAuthConfigJSON = `{"kind":"AuthenticationConfiguration","apiVersion":"apiserver.config.k8s.io/v1beta1","jwt":[{"issuer":{"url":"$URL","certificateAuthority":"fake-ca-cert","audiences":["my-test-aud","another-aud"],"audienceMatchPolicy":"MatchAny"},"claimValidationRules":[{"claim":"username","requiredValue":"test-username"},{"claim":"email","requiredValue":"test-email"}],"claimMappings":{"username":{"claim":"username","prefix":"oidc-user:"},"groups":{"claim":"groups","prefix":"oidc-group:"},"uid":{}}}]}` baseAuthConfigCM = corev1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{ @@ -168,266 +101,118 @@ var ( authConfigDataKey: baseAuthConfigJSON, }, } - - baseCABundleConfigMap = corev1.ConfigMap{ - ObjectMeta: metav1.ObjectMeta{ - Name: "oidc-ca-bundle", - Namespace: configNamespace, - }, - Data: map[string]string{ - "ca-bundle.crt": testCertData, - }, - } - - caBundleConfigMapInvalidKey = corev1.ConfigMap{ - ObjectMeta: metav1.ObjectMeta{ - Name: "oidc-ca-bundle", - Namespace: configNamespace, - }, - Data: map[string]string{ - "invalid": testCertData, - }, - } - - caBundleConfigMapNoData = corev1.ConfigMap{ - ObjectMeta: metav1.ObjectMeta{ - Name: "oidc-ca-bundle", - Namespace: configNamespace, - }, - Data: map[string]string{ - "ca-bundle.crt": "", - }, - } ) +const testIssuer = "https://issuer.openshift.io" + func TestExternalOIDCController_sync(t *testing.T) { testCtx := context.Background() - testServer, err := createTestServer(baseCACert, baseCAPrivateKey, nil) - if err != nil { - t.Fatalf("could not create test server: %v", err) - } - defer testServer.Close() - testServer.StartTLS() - for _, tt := range []struct { name string configMapIndexer cache.Indexer existingAuthConfigCM *corev1.ConfigMap - caBundleConfigMap *corev1.ConfigMap - auth *configv1.Authentication cmApplyReaction k8stesting.ReactionFunc + configGenerator authConfigGenerator + authType configv1.AuthenticationType - expectedAuthConfigJSON string expectEvents bool expectError bool - featureGates featuregates.FeatureGate + expectConfigMapDeleted bool + excludeAuth bool }{ { name: "auth resource not found", expectError: true, - featureGates: featuregates.NewFeatureGate( - []configv1.FeatureGateName{}, - []configv1.FeatureGateName{ - features.FeatureGateExternalOIDCWithAdditionalClaimMappings, - features.FeatureGateExternalOIDCWithUpstreamParity, - }, - ), + excludeAuth: true, }, { - name: "auth type IntegratedOAuth and no auth configmap", - auth: newAuthWithSpec(configv1.AuthenticationSpec{Type: configv1.AuthenticationTypeIntegratedOAuth}), - featureGates: featuregates.NewFeatureGate( - []configv1.FeatureGateName{}, - []configv1.FeatureGateName{ - features.FeatureGateExternalOIDCWithAdditionalClaimMappings, - features.FeatureGateExternalOIDCWithUpstreamParity, - }, - ), + name: "auth type IntegratedOAuth and no auth configmap", + authType: configv1.AuthenticationTypeIntegratedOAuth, }, { name: "auth type IntegratedOAuth delete error", configMapIndexer: &everFailingIndexer{}, - auth: newAuthWithSpec(configv1.AuthenticationSpec{Type: configv1.AuthenticationTypeIntegratedOAuth}), + authType: configv1.AuthenticationTypeIntegratedOAuth, expectError: true, - featureGates: featuregates.NewFeatureGate( - []configv1.FeatureGateName{}, - []configv1.FeatureGateName{ - features.FeatureGateExternalOIDCWithAdditionalClaimMappings, - features.FeatureGateExternalOIDCWithUpstreamParity, - }, - ), }, { - name: "auth type IntegratedOAuth configmap deleted", - existingAuthConfigCM: &baseAuthConfigCM, - auth: newAuthWithSpec(configv1.AuthenticationSpec{Type: configv1.AuthenticationTypeIntegratedOAuth}), - expectEvents: true, - featureGates: featuregates.NewFeatureGate( - []configv1.FeatureGateName{}, - []configv1.FeatureGateName{ - features.FeatureGateExternalOIDCWithAdditionalClaimMappings, - features.FeatureGateExternalOIDCWithUpstreamParity, - }, - ), + name: "auth type IntegratedOAuth configmap deleted", + existingAuthConfigCM: &baseAuthConfigCM, + authType: configv1.AuthenticationTypeIntegratedOAuth, + expectEvents: true, + expectConfigMapDeleted: true, }, { - name: "auth type OIDC but auth config generation fails", - caBundleConfigMap: &baseCABundleConfigMap, - auth: &baseAuthResource, - configMapIndexer: cache.Indexer(&everFailingIndexer{}), - expectEvents: false, - expectError: true, - featureGates: featuregates.NewFeatureGate( - []configv1.FeatureGateName{}, - []configv1.FeatureGateName{ - features.FeatureGateExternalOIDCWithAdditionalClaimMappings, - features.FeatureGateExternalOIDCWithUpstreamParity, - }, - ), + name: "auth type OIDC but auth config generation fails", + authType: configv1.AuthenticationTypeOIDC, + configMapIndexer: cache.Indexer(&everFailingIndexer{}), + expectEvents: false, + expectError: true, + configGenerator: &mockAuthConfigGenerator[*apiserverv1beta1.AuthenticationConfiguration]{ + err: errors.New("boom"), + }, }, { - name: "auth type OIDC but apply config generation fails", - caBundleConfigMap: &baseCABundleConfigMap, - auth: authWithUpdates(baseAuthResource, []func(auth *configv1.Authentication){ - func(auth *configv1.Authentication) { - auth.Spec.OIDCProviders[0].Issuer.URL = testServer.URL - auth.Spec.OIDCProviders[0].Issuer.CertificateAuthority = configv1.ConfigMapNameReference{} - }, - }), + name: "auth type OIDC but apply config generation fails", + authType: configv1.AuthenticationTypeOIDC, + configGenerator: &mockAuthConfigGenerator[*apiserverv1beta1.AuthenticationConfiguration]{ + cfg: authConfigWithUpdates(baseAuthConfig, []func(authConfig *apiserverv1beta1.AuthenticationConfiguration){ + func(authConfig *apiserverv1beta1.AuthenticationConfiguration) { + authConfig.JWT[0].Issuer.URL = testIssuer + authConfig.JWT[0].Issuer.CertificateAuthority = "ca-certificate" + }, + }), + }, configMapIndexer: cache.Indexer(&everFailingIndexer{}), expectEvents: false, expectError: true, - featureGates: featuregates.NewFeatureGate( - []configv1.FeatureGateName{}, - []configv1.FeatureGateName{ - features.FeatureGateExternalOIDCWithAdditionalClaimMappings, - features.FeatureGateExternalOIDCWithUpstreamParity, - }, - ), }, { name: "auth type OIDC config same as existing", - existingAuthConfigCM: authConfigCMWithIssuerURL(&baseAuthConfigCM, testServer.URL), - auth: authWithUpdates(baseAuthResource, []func(auth *configv1.Authentication){ - func(auth *configv1.Authentication) { - auth.Spec.OIDCProviders[0].Issuer.URL = testServer.URL - }, - }), - expectedAuthConfigJSON: strings.ReplaceAll(baseAuthConfigJSON, "$URL", testServer.URL), - caBundleConfigMap: &baseCABundleConfigMap, - expectEvents: false, - featureGates: featuregates.NewFeatureGate( - []configv1.FeatureGateName{}, - []configv1.FeatureGateName{ - features.FeatureGateExternalOIDCWithAdditionalClaimMappings, - features.FeatureGateExternalOIDCWithUpstreamParity, - }, - ), - }, - { - name: "auth type OIDC error while validating config", - caBundleConfigMap: &baseCABundleConfigMap, - existingAuthConfigCM: &baseAuthConfigCM, - auth: authWithUpdates(baseAuthResource, []func(auth *configv1.Authentication){ - func(auth *configv1.Authentication) { - auth.Spec.OIDCProviders[0].Issuer.Audiences = []configv1.TokenAudience{"my-test-aud", "yet-another-aud"} - }, - }), + existingAuthConfigCM: authConfigCMWithIssuerURL(&baseAuthConfigCM, testIssuer), + authType: configv1.AuthenticationTypeOIDC, + configGenerator: &mockAuthConfigGenerator[*apiserverv1beta1.AuthenticationConfiguration]{ + cfg: authConfigWithUpdates(baseAuthConfig, []func(authConfig *apiserverv1beta1.AuthenticationConfiguration){ + func(authConfig *apiserverv1beta1.AuthenticationConfiguration) { + authConfig.JWT[0].Issuer.URL = testIssuer + }, + }), + }, expectEvents: false, - expectError: true, - featureGates: featuregates.NewFeatureGate( - []configv1.FeatureGateName{}, - []configv1.FeatureGateName{ - features.FeatureGateExternalOIDCWithAdditionalClaimMappings, - features.FeatureGateExternalOIDCWithUpstreamParity, - }, - ), }, { name: "auth type OIDC error while applying config", cmApplyReaction: func(action k8stesting.Action) (bool, runtime.Object, error) { return true, nil, fmt.Errorf("apply failed") }, - caBundleConfigMap: &baseCABundleConfigMap, + authType: configv1.AuthenticationTypeOIDC, existingAuthConfigCM: &baseAuthConfigCM, - auth: authWithUpdates(baseAuthResource, []func(auth *configv1.Authentication){ - func(auth *configv1.Authentication) { - auth.Spec.OIDCProviders[0].Issuer.URL = testServer.URL - auth.Spec.OIDCProviders[0].Issuer.Audiences = []configv1.TokenAudience{"my-test-aud", "yet-another-aud"} - }, - }), + configGenerator: &mockAuthConfigGenerator[*apiserverv1beta1.AuthenticationConfiguration]{ + cfg: authConfigWithUpdates(baseAuthConfig, []func(authConfig *apiserverv1beta1.AuthenticationConfiguration){ + func(authConfig *apiserverv1beta1.AuthenticationConfiguration) { + authConfig.JWT[0].Issuer.URL = testIssuer + authConfig.JWT[0].Issuer.Audiences = []string{"my-test-aud", "yet-another-aud"} + }, + }), + }, expectEvents: false, expectError: true, - featureGates: featuregates.NewFeatureGate( - []configv1.FeatureGateName{}, - []configv1.FeatureGateName{ - features.FeatureGateExternalOIDCWithAdditionalClaimMappings, - features.FeatureGateExternalOIDCWithUpstreamParity, - }, - ), }, { name: "auth type OIDC apply config", - caBundleConfigMap: &baseCABundleConfigMap, - existingAuthConfigCM: authConfigCMWithIssuerURL(&baseAuthConfigCM, testServer.URL), - auth: authWithUpdates(baseAuthResource, []func(auth *configv1.Authentication){ - func(auth *configv1.Authentication) { - auth.Spec.OIDCProviders[0].Issuer.URL = testServer.URL - auth.Spec.OIDCProviders[0].Issuer.Audiences = []configv1.TokenAudience{"my-test-aud", "yet-another-aud"} - }, - }), - expectedAuthConfigJSON: func() string { - str := strings.ReplaceAll(baseAuthConfigJSON, "$URL", testServer.URL) - str = strings.ReplaceAll(str, "another-aud", "yet-another-aud") - return str - }(), - expectEvents: true, - featureGates: featuregates.NewFeatureGate( - []configv1.FeatureGateName{}, - []configv1.FeatureGateName{ - features.FeatureGateExternalOIDCWithAdditionalClaimMappings, - features.FeatureGateExternalOIDCWithUpstreamParity, - }, - ), - }, - { - name: "auth type OIDC with UID+Extra featureGate enabled apply config", - caBundleConfigMap: &baseCABundleConfigMap, - existingAuthConfigCM: authConfigCMWithIssuerURL(&baseAuthConfigCM, testServer.URL), - auth: authWithUpdates(baseAuthResource, []func(auth *configv1.Authentication){ - func(auth *configv1.Authentication) { - auth.Spec.OIDCProviders[0].Issuer.URL = testServer.URL - auth.Spec.OIDCProviders[0].Issuer.Audiences = []configv1.TokenAudience{"my-test-aud", "yet-another-aud"} - - auth.Spec.OIDCProviders[0].ClaimMappings.UID = &configv1.TokenClaimOrExpressionMapping{ - Claim: "sub", - } - }, - }), - expectedAuthConfigJSON: func() string { - authConfig := authConfigWithUpdates(baseAuthConfig, []func(authConfig *apiserverv1beta1.AuthenticationConfiguration){ + authType: configv1.AuthenticationTypeOIDC, + existingAuthConfigCM: authConfigCMWithIssuerURL(&baseAuthConfigCM, testIssuer), + configGenerator: &mockAuthConfigGenerator[*apiserverv1beta1.AuthenticationConfiguration]{ + cfg: authConfigWithUpdates(baseAuthConfig, []func(authConfig *apiserverv1beta1.AuthenticationConfiguration){ func(authConfig *apiserverv1beta1.AuthenticationConfiguration) { - authConfig.JWT[0].Issuer.URL = testServer.URL + authConfig.JWT[0].Issuer.URL = testIssuer authConfig.JWT[0].Issuer.Audiences = []string{"my-test-aud", "yet-another-aud"} - authConfig.JWT[0].ClaimMappings.UID = apiserverv1beta1.ClaimOrExpression{ - Claim: "sub", - } }, - }) - - out, _ := json.Marshal(authConfig) - return string(out) - }(), + }), + }, expectEvents: true, - featureGates: featuregates.NewFeatureGate( - []configv1.FeatureGateName{ - features.FeatureGateExternalOIDCWithAdditionalClaimMappings, - features.FeatureGateExternalOIDCWithUpstreamParity, - }, - []configv1.FeatureGateName{}, - ), }, } { t.Run(tt.name, func(t *testing.T) { @@ -437,16 +222,14 @@ func TestExternalOIDCController_sync(t *testing.T) { return "cluster", nil }, cache.Indexers{}) - if tt.configMapIndexer == nil { - tt.configMapIndexer = cache.NewIndexer(cache.MetaNamespaceKeyFunc, cache.Indexers{}) - } - - if tt.auth != nil { - authIndexer.Add(tt.auth) + if !tt.excludeAuth { + authIndexer.Add(newAuthWithSpec(configv1.AuthenticationSpec{ + Type: tt.authType, + })) } - if tt.caBundleConfigMap != nil { - tt.configMapIndexer.Add(&baseCABundleConfigMap) + if tt.configMapIndexer == nil { + tt.configMapIndexer = cache.NewIndexer(cache.MetaNamespaceKeyFunc, cache.Indexers{}) } if tt.existingAuthConfigCM != nil { @@ -461,11 +244,11 @@ func TestExternalOIDCController_sync(t *testing.T) { } c := externalOIDCController{ - name: "test_oidc_controller", - configMaps: cs.CoreV1(), - authLister: configv1listers.NewAuthenticationLister(authIndexer), - configMapLister: corev1listers.NewConfigMapLister(tt.configMapIndexer), - featureGates: tt.featureGates, + name: "test_oidc_controller", + configMaps: cs.CoreV1(), + authLister: configv1listers.NewAuthenticationLister(authIndexer), + configMapLister: corev1listers.NewConfigMapLister(tt.configMapIndexer), + authConfigGenerator: tt.configGenerator, } eventRecorder := events.NewInMemoryRecorder("externaloidc-test", clocktesting.NewFakePassiveClock(time.Now())) @@ -473,27 +256,41 @@ func TestExternalOIDCController_sync(t *testing.T) { err := c.sync(testCtx, factory.NewSyncContext("externaloidc-test-context", eventRecorder)) if tt.expectError != (err != nil) { - t.Errorf("unexpected error; want: %v; got: %v", tt.expectError, err) + t.Fatalf("unexpected error; want: %v; got: %v", tt.expectError, err) } if tt.expectEvents != (len(eventRecorder.Events()) > 0) { t.Errorf("expected events: %v; got %v", tt.expectEvents, eventRecorder.Events()) } - if tt.auth == nil || err != nil { + if err != nil { // stop assertions here; the ones that follow are not relevant return } cm, err := c.configMaps.ConfigMaps(managedNamespace).Get(testCtx, targetAuthConfigCMName, metav1.GetOptions{}) - if len(tt.expectedAuthConfigJSON) == 0 && err == nil { - t.Errorf("expected auth configmap to be missing, but it was found") - } else if len(tt.expectedAuthConfigJSON) > 0 && errors.IsNotFound(err) { - t.Errorf("expected auth configmap to exist but it was not found; error = %v", err) + + // happy path for deletion behavior, stop here if it matches our expectations. + if apierrors.IsNotFound(err) && (tt.expectConfigMapDeleted || tt.authType != configv1.AuthenticationTypeOIDC) { + return + } + + if err != nil { + t.Fatalf("received an unexpected error when getting ConfigMap with auth-config: %v", err) + } + + cfg, err := tt.configGenerator.GenerateAuthenticationConfiguration(nil) + if err != nil { + t.Fatalf("received an unexpected error when generating auth config: %v", err) } - if len(tt.expectedAuthConfigJSON) > 0 && tt.expectedAuthConfigJSON != cm.Data[authConfigDataKey] { - t.Errorf("got unexpected auth-config data: '%s'\nexpected: '%s'", cm.Data[authConfigDataKey], tt.expectedAuthConfigJSON) + expectedAuthConfigJSON, err := json.Marshal(cfg) + if err != nil { + t.Fatalf("received an unexpected error when marshalling auth config: %v", err) + } + + if string(expectedAuthConfigJSON) != cm.Data[authConfigDataKey] { + t.Errorf("got unexpected auth-config data: '%s'\nexpected: '%s'", cm.Data[authConfigDataKey], string(expectedAuthConfigJSON)) } }) } @@ -584,1020 +381,8 @@ func TestExternalOIDCCOntroller_deleteAuthConfig(t *testing.T) { } _, err = c.configMaps.ConfigMaps(managedNamespace).Get(testCtx, targetAuthConfigCMName, metav1.GetOptions{}) - if tt.expectNotPresent != errors.IsNotFound(err) { - t.Errorf("expected configmap to be deleted=%v; got: %v", tt.expectNotPresent, errors.IsNotFound(err)) - } - }) - } -} - -func TestExternalOIDCController_generateAuthConfig(t *testing.T) { - for _, tt := range []struct { - name string - - auth configv1.Authentication - caBundleConfigMap *corev1.ConfigMap - configMapIndexer cache.Indexer - - expectedAuthConfig *apiserverv1beta1.AuthenticationConfiguration - expectError bool - featureGates featuregates.FeatureGate - }{ - { - name: "ca bundle configmap lister error", - auth: baseAuthResource, - configMapIndexer: cache.Indexer(&everFailingIndexer{}), - expectError: true, - featureGates: featuregates.NewFeatureGate( - []configv1.FeatureGateName{}, - []configv1.FeatureGateName{ - features.FeatureGateExternalOIDCWithAdditionalClaimMappings, - features.FeatureGateExternalOIDCWithUpstreamParity, - }, - ), - }, - { - name: "ca bundle configmap without required key", - auth: baseAuthResource, - caBundleConfigMap: &caBundleConfigMapInvalidKey, - expectError: true, - featureGates: featuregates.NewFeatureGate( - []configv1.FeatureGateName{}, - []configv1.FeatureGateName{ - features.FeatureGateExternalOIDCWithAdditionalClaimMappings, - features.FeatureGateExternalOIDCWithUpstreamParity, - }, - ), - }, - { - name: "ca bundle configmap with no data", - auth: baseAuthResource, - caBundleConfigMap: &caBundleConfigMapNoData, - expectError: true, - featureGates: featuregates.NewFeatureGate( - []configv1.FeatureGateName{}, - []configv1.FeatureGateName{ - features.FeatureGateExternalOIDCWithAdditionalClaimMappings, - features.FeatureGateExternalOIDCWithUpstreamParity, - }, - ), - }, - { - name: "auth config nil prefix when required", - caBundleConfigMap: &baseCABundleConfigMap, - auth: *authWithUpdates(baseAuthResource, []func(auth *configv1.Authentication){ - func(auth *configv1.Authentication) { - for i := range auth.Spec.OIDCProviders { - auth.Spec.OIDCProviders[i].ClaimMappings.Username = configv1.UsernameClaimMapping{ - Claim: "username", - PrefixPolicy: configv1.Prefix, - Prefix: nil, - } - } - }, - }), - expectError: true, - featureGates: featuregates.NewFeatureGate( - []configv1.FeatureGateName{}, - []configv1.FeatureGateName{ - features.FeatureGateExternalOIDCWithAdditionalClaimMappings, - features.FeatureGateExternalOIDCWithUpstreamParity, - }, - ), - }, - { - name: "auth config invalid prefix policy", - caBundleConfigMap: &baseCABundleConfigMap, - auth: *authWithUpdates(baseAuthResource, []func(auth *configv1.Authentication){ - func(auth *configv1.Authentication) { - for i := range auth.Spec.OIDCProviders { - auth.Spec.OIDCProviders[i].ClaimMappings.Username = configv1.UsernameClaimMapping{ - Claim: "username", - PrefixPolicy: configv1.UsernamePrefixPolicy("invalid-policy"), - } - } - }, - }), - expectError: true, - featureGates: featuregates.NewFeatureGate( - []configv1.FeatureGateName{}, - []configv1.FeatureGateName{ - features.FeatureGateExternalOIDCWithAdditionalClaimMappings, - features.FeatureGateExternalOIDCWithUpstreamParity, - }, - ), - }, - { - name: "auth config with nil claim in validation rule", - caBundleConfigMap: &baseCABundleConfigMap, - auth: *authWithUpdates(baseAuthResource, []func(auth *configv1.Authentication){ - func(copy *configv1.Authentication) { - for i := range copy.Spec.OIDCProviders { - if len(copy.Spec.OIDCProviders[i].ClaimValidationRules) == 0 { - copy.Spec.OIDCProviders[i].ClaimValidationRules = make([]configv1.TokenClaimValidationRule, 0) - } - copy.Spec.OIDCProviders[i].ClaimValidationRules = append( - copy.Spec.OIDCProviders[i].ClaimValidationRules, - configv1.TokenClaimValidationRule{ - Type: configv1.TokenValidationRuleTypeRequiredClaim, - RequiredClaim: nil, - }, - ) - } - }, - }), - expectError: true, - featureGates: featuregates.NewFeatureGate( - []configv1.FeatureGateName{}, - []configv1.FeatureGateName{ - features.FeatureGateExternalOIDCWithAdditionalClaimMappings, - features.FeatureGateExternalOIDCWithUpstreamParity, - }, - ), - }, - { - name: "valid auth config", - caBundleConfigMap: &baseCABundleConfigMap, - auth: *authWithUpdates(baseAuthResource, []func(auth *configv1.Authentication){ - func(auth *configv1.Authentication) { - for i := range auth.Spec.OIDCProviders { - auth.Spec.OIDCProviders[i].Issuer.URL = "https://example.com" - } - }, - }), - expectedAuthConfig: authConfigWithUpdates(baseAuthConfig, []func(authConfig *apiserverv1beta1.AuthenticationConfiguration){ - func(authConfig *apiserverv1beta1.AuthenticationConfiguration) { - authConfig.JWT[0].Issuer.URL = "https://example.com" - }, - }), - expectError: false, - featureGates: featuregates.NewFeatureGate( - []configv1.FeatureGateName{}, - []configv1.FeatureGateName{ - features.FeatureGateExternalOIDCWithAdditionalClaimMappings, - features.FeatureGateExternalOIDCWithUpstreamParity, - }, - ), - }, - { - name: "valid auth config with empty CA name", - auth: *authWithUpdates(baseAuthResource, []func(auth *configv1.Authentication){ - func(auth *configv1.Authentication) { - for i := range auth.Spec.OIDCProviders { - auth.Spec.OIDCProviders[i].Issuer.CertificateAuthority.Name = "" - } - }, - }), - expectedAuthConfig: authConfigWithUpdates(baseAuthConfig, []func(authConfig *apiserverv1beta1.AuthenticationConfiguration){ - func(authConfig *apiserverv1beta1.AuthenticationConfiguration) { - for i := range authConfig.JWT { - authConfig.JWT[i].Issuer.CertificateAuthority = "" - } - }, - }), - expectError: false, - featureGates: featuregates.NewFeatureGate( - []configv1.FeatureGateName{}, - []configv1.FeatureGateName{ - features.FeatureGateExternalOIDCWithAdditionalClaimMappings, - features.FeatureGateExternalOIDCWithUpstreamParity, - }, - ), - }, - { - name: "auth config with default prefix policy", - caBundleConfigMap: &baseCABundleConfigMap, - auth: *authWithUpdates(baseAuthResource, []func(auth *configv1.Authentication){ - func(auth *configv1.Authentication) { - for i := range auth.Spec.OIDCProviders { - auth.Spec.OIDCProviders[i].Issuer.URL = "https://example.com" - auth.Spec.OIDCProviders[i].ClaimMappings.Username = configv1.UsernameClaimMapping{ - Claim: "email", - PrefixPolicy: configv1.NoOpinion, - } - } - }, - }), - expectedAuthConfig: authConfigWithUpdates(baseAuthConfig, []func(authConfig *apiserverv1beta1.AuthenticationConfiguration){ - func(authConfig *apiserverv1beta1.AuthenticationConfiguration) { - for i := range authConfig.JWT { - authConfig.JWT[i].Issuer.URL = "https://example.com" - authConfig.JWT[i].ClaimMappings.Username = apiserverv1beta1.PrefixedClaimOrExpression{ - Claim: "email", - Prefix: ptr.To(""), - } - } - }, - }), - expectError: false, - featureGates: featuregates.NewFeatureGate( - []configv1.FeatureGateName{}, - []configv1.FeatureGateName{ - features.FeatureGateExternalOIDCWithAdditionalClaimMappings, - features.FeatureGateExternalOIDCWithUpstreamParity, - }, - ), - }, - { - name: "auth config with default prefix policy and username claim email", - caBundleConfigMap: &baseCABundleConfigMap, - auth: *authWithUpdates(baseAuthResource, []func(auth *configv1.Authentication){ - func(auth *configv1.Authentication) { - for i := range auth.Spec.OIDCProviders { - auth.Spec.OIDCProviders[i].Issuer.URL = "https://example.com" - auth.Spec.OIDCProviders[i].ClaimMappings.Username = configv1.UsernameClaimMapping{ - Claim: "username", - PrefixPolicy: configv1.NoOpinion, - } - } - }, - }), - expectedAuthConfig: authConfigWithUpdates(baseAuthConfig, []func(authConfig *apiserverv1beta1.AuthenticationConfiguration){ - func(authConfig *apiserverv1beta1.AuthenticationConfiguration) { - for i := range authConfig.JWT { - authConfig.JWT[i].Issuer.URL = "https://example.com" - authConfig.JWT[i].ClaimMappings.Username = apiserverv1beta1.PrefixedClaimOrExpression{ - Claim: "username", - Prefix: ptr.To("https://example.com#"), - } - } - }, - }), - expectError: false, - featureGates: featuregates.NewFeatureGate( - []configv1.FeatureGateName{}, - []configv1.FeatureGateName{ - features.FeatureGateExternalOIDCWithAdditionalClaimMappings, - features.FeatureGateExternalOIDCWithUpstreamParity, - }, - ), - }, - { - name: "auth config with no prefix policy", - caBundleConfigMap: &baseCABundleConfigMap, - auth: *authWithUpdates(baseAuthResource, []func(auth *configv1.Authentication){ - func(auth *configv1.Authentication) { - for i := range auth.Spec.OIDCProviders { - auth.Spec.OIDCProviders[i].Issuer.URL = "https://example.com" - auth.Spec.OIDCProviders[i].ClaimMappings.Username = configv1.UsernameClaimMapping{ - Claim: "username", - PrefixPolicy: configv1.NoPrefix, - } - } - }, - }), - expectedAuthConfig: authConfigWithUpdates(baseAuthConfig, []func(authConfig *apiserverv1beta1.AuthenticationConfiguration){ - func(authConfig *apiserverv1beta1.AuthenticationConfiguration) { - for i := range authConfig.JWT { - authConfig.JWT[i].Issuer.URL = "https://example.com" - authConfig.JWT[i].ClaimMappings.Username = apiserverv1beta1.PrefixedClaimOrExpression{ - Claim: "username", - Prefix: ptr.To(""), - } - } - }, - }), - expectError: false, - featureGates: featuregates.NewFeatureGate( - []configv1.FeatureGateName{}, - []configv1.FeatureGateName{ - features.FeatureGateExternalOIDCWithAdditionalClaimMappings, - features.FeatureGateExternalOIDCWithUpstreamParity, - }, - ), - }, - { - name: "auth config with username claim prefix", - caBundleConfigMap: &baseCABundleConfigMap, - auth: *authWithUpdates(baseAuthResource, []func(auth *configv1.Authentication){ - func(auth *configv1.Authentication) { - for i := range auth.Spec.OIDCProviders { - auth.Spec.OIDCProviders[i].Issuer.URL = "https://example.com" - auth.Spec.OIDCProviders[i].ClaimMappings.Username = configv1.UsernameClaimMapping{ - Claim: "username", - PrefixPolicy: configv1.Prefix, - Prefix: &configv1.UsernamePrefix{ - PrefixString: "oidc-user:", - }, - } - } - }, - }), - expectedAuthConfig: authConfigWithUpdates(baseAuthConfig, []func(authConfig *apiserverv1beta1.AuthenticationConfiguration){ - func(authConfig *apiserverv1beta1.AuthenticationConfiguration) { - for i := range authConfig.JWT { - authConfig.JWT[i].Issuer.URL = "https://example.com" - authConfig.JWT[i].ClaimMappings.Username = apiserverv1beta1.PrefixedClaimOrExpression{ - Claim: "username", - Prefix: ptr.To("oidc-user:"), - } - } - }, - }), - expectError: false, - featureGates: featuregates.NewFeatureGate( - []configv1.FeatureGateName{}, - []configv1.FeatureGateName{ - features.FeatureGateExternalOIDCWithAdditionalClaimMappings, - features.FeatureGateExternalOIDCWithUpstreamParity, - }, - ), - }, - { - name: "auth config with empty string for username claim", - caBundleConfigMap: &baseCABundleConfigMap, - auth: *authWithUpdates(baseAuthResource, []func(auth *configv1.Authentication){ - func(auth *configv1.Authentication) { - for i := range auth.Spec.OIDCProviders { - auth.Spec.OIDCProviders[i].ClaimMappings.Username = configv1.UsernameClaimMapping{ - Claim: "", - } - } - }, - }), - expectError: true, - featureGates: featuregates.NewFeatureGate( - []configv1.FeatureGateName{}, - []configv1.FeatureGateName{ - features.FeatureGateExternalOIDCWithAdditionalClaimMappings, - features.FeatureGateExternalOIDCWithUpstreamParity, - }, - ), - }, - { - name: "auth config with no uid claim or expression", - caBundleConfigMap: &baseCABundleConfigMap, - auth: baseAuthResource, - expectedAuthConfig: authConfigWithUpdates(baseAuthConfig, []func(authConfig *apiserverv1beta1.AuthenticationConfiguration){ - func(authConfig *apiserverv1beta1.AuthenticationConfiguration) { - for i := range authConfig.JWT { - authConfig.JWT[i].ClaimMappings.UID.Claim = "sub" - } - }, - }), - expectError: false, - featureGates: featuregates.NewFeatureGate( - []configv1.FeatureGateName{ - features.FeatureGateExternalOIDCWithAdditionalClaimMappings, - features.FeatureGateExternalOIDCWithUpstreamParity, - }, - []configv1.FeatureGateName{}, - ), - }, - { - name: "auth config with uid claim and expression", - caBundleConfigMap: &baseCABundleConfigMap, - auth: *authWithUpdates(baseAuthResource, []func(auth *configv1.Authentication){ - func(auth *configv1.Authentication) { - for i := range auth.Spec.OIDCProviders { - auth.Spec.OIDCProviders[i].ClaimMappings.UID = &configv1.TokenClaimOrExpressionMapping{ - Claim: "sub", - Expression: "claims.sub", - } - } - }, - }), - expectError: true, - featureGates: featuregates.NewFeatureGate( - []configv1.FeatureGateName{ - features.FeatureGateExternalOIDCWithAdditionalClaimMappings, - features.FeatureGateExternalOIDCWithUpstreamParity, - }, - []configv1.FeatureGateName{}, - ), - }, - { - name: "auth config with uid expression", - caBundleConfigMap: &baseCABundleConfigMap, - auth: *authWithUpdates(baseAuthResource, []func(auth *configv1.Authentication){ - func(auth *configv1.Authentication) { - for i := range auth.Spec.OIDCProviders { - auth.Spec.OIDCProviders[i].ClaimMappings.UID = &configv1.TokenClaimOrExpressionMapping{ - Claim: "", - Expression: "claims.sub", - } - } - }, - }), - expectedAuthConfig: authConfigWithUpdates(baseAuthConfig, []func(authConfig *apiserverv1beta1.AuthenticationConfiguration){ - func(authConfig *apiserverv1beta1.AuthenticationConfiguration) { - for i := range authConfig.JWT { - authConfig.JWT[i].ClaimMappings.UID.Claim = "" - authConfig.JWT[i].ClaimMappings.UID.Expression = "claims.sub" - } - }, - }), - expectError: false, - featureGates: featuregates.NewFeatureGate( - []configv1.FeatureGateName{ - features.FeatureGateExternalOIDCWithAdditionalClaimMappings, - features.FeatureGateExternalOIDCWithUpstreamParity, - }, - []configv1.FeatureGateName{}, - ), - }, - { - name: "auth config with extra missing key", - caBundleConfigMap: &baseCABundleConfigMap, - auth: *authWithUpdates(baseAuthResource, []func(auth *configv1.Authentication){ - func(auth *configv1.Authentication) { - for i := range auth.Spec.OIDCProviders { - auth.Spec.OIDCProviders[i].ClaimMappings.Extra = []configv1.ExtraMapping{ - { - ValueExpression: "claims.foo", - }, - } - } - }, - }), - expectError: true, - featureGates: featuregates.NewFeatureGate( - []configv1.FeatureGateName{ - features.FeatureGateExternalOIDCWithAdditionalClaimMappings, - features.FeatureGateExternalOIDCWithUpstreamParity, - }, - []configv1.FeatureGateName{}, - ), - }, - { - name: "auth config with extra missing valueExpression", - caBundleConfigMap: &baseCABundleConfigMap, - auth: *authWithUpdates(baseAuthResource, []func(auth *configv1.Authentication){ - func(auth *configv1.Authentication) { - for i := range auth.Spec.OIDCProviders { - auth.Spec.OIDCProviders[i].ClaimMappings.Extra = []configv1.ExtraMapping{ - { - Key: "foo.example.com/bar", - }, - } - } - }, - }), - expectError: true, - featureGates: featuregates.NewFeatureGate( - []configv1.FeatureGateName{ - features.FeatureGateExternalOIDCWithAdditionalClaimMappings, - features.FeatureGateExternalOIDCWithUpstreamParity, - }, - []configv1.FeatureGateName{}, - ), - }, - { - name: "auth config with valid extra mappings", - caBundleConfigMap: &baseCABundleConfigMap, - auth: *authWithUpdates(baseAuthResource, []func(auth *configv1.Authentication){ - func(auth *configv1.Authentication) { - for i := range auth.Spec.OIDCProviders { - auth.Spec.OIDCProviders[i].ClaimMappings.Extra = []configv1.ExtraMapping{ - { - Key: "foo.example.com/bar", - ValueExpression: "claims.bar", - }, - } - } - }, - }), - expectedAuthConfig: authConfigWithUpdates(baseAuthConfig, []func(authConfig *apiserverv1beta1.AuthenticationConfiguration){ - func(authConfig *apiserverv1beta1.AuthenticationConfiguration) { - for i := range authConfig.JWT { - authConfig.JWT[i].ClaimMappings.UID.Claim = "sub" - authConfig.JWT[i].ClaimMappings.Extra = []apiserverv1beta1.ExtraMapping{ - { - Key: "foo.example.com/bar", - ValueExpression: "claims.bar", - }, - } - } - }, - }), - expectError: false, - featureGates: featuregates.NewFeatureGate( - []configv1.FeatureGateName{ - features.FeatureGateExternalOIDCWithAdditionalClaimMappings, - features.FeatureGateExternalOIDCWithUpstreamParity, - }, - []configv1.FeatureGateName{}, - ), - }, - { - name: "invalid discovery URL (http instead of https)", - auth: *authWithUpdates(baseAuthResource, []func(auth *configv1.Authentication){ - func(auth *configv1.Authentication) { - auth.Spec.OIDCProviders[0].Issuer.DiscoveryURL = "http://insecure-url.com" - }, - }), - caBundleConfigMap: &baseCABundleConfigMap, // ensure CA bundle exists - expectError: true, - featureGates: featuregates.NewFeatureGate( - []configv1.FeatureGateName{features.FeatureGateExternalOIDCWithUpstreamParity}, - []configv1.FeatureGateName{}, - ), - }, - { - name: "invalid discovery URL (identical to issuer URL)", - auth: *authWithUpdates(baseAuthResource, []func(auth *configv1.Authentication){ - func(auth *configv1.Authentication) { - auth.Spec.OIDCProviders[0].Issuer.URL = "https://issuer.example.com" - auth.Spec.OIDCProviders[0].Issuer.DiscoveryURL = auth.Spec.OIDCProviders[0].Issuer.URL - }, - }), - caBundleConfigMap: &baseCABundleConfigMap, - expectError: true, - featureGates: featuregates.NewFeatureGate( - []configv1.FeatureGateName{features.FeatureGateExternalOIDCWithUpstreamParity}, - []configv1.FeatureGateName{}, - ), - }, - { - name: "invalid discovery URL (identical to issuer URL except trailing slash)", - auth: *authWithUpdates(baseAuthResource, []func(auth *configv1.Authentication){ - func(auth *configv1.Authentication) { - auth.Spec.OIDCProviders[0].Issuer.URL = "https://issuer.example.com" - auth.Spec.OIDCProviders[0].Issuer.DiscoveryURL = "https://issuer.example.com/" - }, - }), - caBundleConfigMap: &baseCABundleConfigMap, - expectError: true, - featureGates: featuregates.NewFeatureGate( - []configv1.FeatureGateName{features.FeatureGateExternalOIDCWithUpstreamParity}, - []configv1.FeatureGateName{}, - ), - }, - { - name: "invalid discovery URL (missing host)", - auth: *authWithUpdates(baseAuthResource, []func(auth *configv1.Authentication){ - func(auth *configv1.Authentication) { - auth.Spec.OIDCProviders[0].Issuer.URL = "https://issuer.example.com" - auth.Spec.OIDCProviders[0].Issuer.DiscoveryURL = "https:///path" - }, - }), - caBundleConfigMap: &baseCABundleConfigMap, - expectError: true, - featureGates: featuregates.NewFeatureGate( - []configv1.FeatureGateName{features.FeatureGateExternalOIDCWithUpstreamParity}, - []configv1.FeatureGateName{}, - ), - }, - { - name: "invalid discovery URL (contains user info)", - auth: *authWithUpdates(baseAuthResource, []func(auth *configv1.Authentication){ - func(auth *configv1.Authentication) { - auth.Spec.OIDCProviders[0].Issuer.URL = "https://issuer.example.com" - auth.Spec.OIDCProviders[0].Issuer.DiscoveryURL = "https://user@discovery.example.com/path" - }, - }), - caBundleConfigMap: &baseCABundleConfigMap, - expectError: true, - featureGates: featuregates.NewFeatureGate( - []configv1.FeatureGateName{features.FeatureGateExternalOIDCWithUpstreamParity}, - []configv1.FeatureGateName{}, - ), - }, - { - name: "invalid discovery URL (contains query string)", - auth: *authWithUpdates(baseAuthResource, []func(auth *configv1.Authentication){ - func(auth *configv1.Authentication) { - auth.Spec.OIDCProviders[0].Issuer.URL = "https://issuer.example.com" - auth.Spec.OIDCProviders[0].Issuer.DiscoveryURL = "https://discovery.example.com/path?q=1" - }, - }), - caBundleConfigMap: &baseCABundleConfigMap, - expectError: true, - featureGates: featuregates.NewFeatureGate( - []configv1.FeatureGateName{features.FeatureGateExternalOIDCWithUpstreamParity}, - []configv1.FeatureGateName{}, - ), - }, - { - name: "invalid discovery URL (contains fragment)", - auth: *authWithUpdates(baseAuthResource, []func(auth *configv1.Authentication){ - func(auth *configv1.Authentication) { - auth.Spec.OIDCProviders[0].Issuer.URL = "https://issuer.example.com" - auth.Spec.OIDCProviders[0].Issuer.DiscoveryURL = "https://discovery.example.com/path#fragment" - }, - }), - caBundleConfigMap: &baseCABundleConfigMap, - expectError: true, - featureGates: featuregates.NewFeatureGate( - []configv1.FeatureGateName{features.FeatureGateExternalOIDCWithUpstreamParity}, - []configv1.FeatureGateName{}, - ), - }, - { - name: "invalid discovery URL (parse error)", - auth: *authWithUpdates(baseAuthResource, []func(auth *configv1.Authentication){ - func(auth *configv1.Authentication) { - auth.Spec.OIDCProviders[0].Issuer.URL = "https://issuer.example.com" - auth.Spec.OIDCProviders[0].Issuer.DiscoveryURL = "https://%zz" - }, - }), - caBundleConfigMap: &baseCABundleConfigMap, - expectError: true, - featureGates: featuregates.NewFeatureGate( - []configv1.FeatureGateName{features.FeatureGateExternalOIDCWithUpstreamParity}, - []configv1.FeatureGateName{}, - ), - }, - { - name: "user validation rule invalid expression", - auth: *authWithUpdates(baseAuthResource, []func(auth *configv1.Authentication){ - func(auth *configv1.Authentication) { - auth.Spec.OIDCProviders[0].UserValidationRules = []configv1.TokenUserValidationRule{ - { - Expression: "", // invalid: empty expression - Message: "must have a valid expression", - }, - } - }, - }), - expectError: true, - featureGates: featuregates.NewFeatureGate( - []configv1.FeatureGateName{features.FeatureGateExternalOIDCWithUpstreamParity}, - []configv1.FeatureGateName{}, - ), - }, - { - name: "auth config with invalid username expression, error", - caBundleConfigMap: &baseCABundleConfigMap, - auth: *authWithUpdates(baseAuthResource, []func(auth *configv1.Authentication){ - func(auth *configv1.Authentication) { - for i := range auth.Spec.OIDCProviders { - auth.Spec.OIDCProviders[i].ClaimMappings.Username = configv1.UsernameClaimMapping{ - Expression: "#@!$&*(^)", - } - } - }, - }), - expectError: true, - featureGates: featuregates.NewFeatureGate( - []configv1.FeatureGateName{ - features.FeatureGateExternalOIDCWithAdditionalClaimMappings, - features.FeatureGateExternalOIDCWithUpstreamParity, - }, - []configv1.FeatureGateName{}, - ), - }, - { - name: "auth config with invalid groups expression, error", - caBundleConfigMap: &baseCABundleConfigMap, - auth: *authWithUpdates(baseAuthResource, []func(auth *configv1.Authentication){ - func(auth *configv1.Authentication) { - for i := range auth.Spec.OIDCProviders { - auth.Spec.OIDCProviders[i].ClaimMappings.Groups = configv1.PrefixedClaimMapping{ - TokenClaimMapping: configv1.TokenClaimMapping{ - Expression: "#@!$&*(^)", - }, - } - } - }, - }), - expectError: true, - featureGates: featuregates.NewFeatureGate( - []configv1.FeatureGateName{ - features.FeatureGateExternalOIDCWithAdditionalClaimMappings, - features.FeatureGateExternalOIDCWithUpstreamParity, - }, - []configv1.FeatureGateName{}, - ), - }, - { - name: "auth config with username expression mapping", - caBundleConfigMap: &baseCABundleConfigMap, - auth: *authWithUpdates(baseAuthResource, []func(auth *configv1.Authentication){ - func(auth *configv1.Authentication) { - for i := range auth.Spec.OIDCProviders { - auth.Spec.OIDCProviders[i].ClaimMappings.Username = configv1.UsernameClaimMapping{ - Expression: "claims.sub", - } - } - }, - }), - expectedAuthConfig: authConfigWithUpdates(baseAuthConfig, []func(authConfig *apiserverv1beta1.AuthenticationConfiguration){ - func(authConfig *apiserverv1beta1.AuthenticationConfiguration) { - for i := range authConfig.JWT { - authConfig.JWT[i].ClaimMappings.Username = apiserverv1beta1.PrefixedClaimOrExpression{ - Expression: "claims.sub", - } - authConfig.JWT[i].ClaimMappings.UID = apiserverv1beta1.ClaimOrExpression{ - Claim: "sub", - } - } - }, - }), - expectError: false, - featureGates: featuregates.NewFeatureGate( - []configv1.FeatureGateName{ - features.FeatureGateExternalOIDCWithAdditionalClaimMappings, - features.FeatureGateExternalOIDCWithUpstreamParity, - }, - []configv1.FeatureGateName{}, - ), - }, - { - name: "auth config with groups expression mapping", - caBundleConfigMap: &baseCABundleConfigMap, - auth: *authWithUpdates(baseAuthResource, []func(auth *configv1.Authentication){ - func(auth *configv1.Authentication) { - for i := range auth.Spec.OIDCProviders { - auth.Spec.OIDCProviders[i].ClaimMappings.Groups = configv1.PrefixedClaimMapping{ - TokenClaimMapping: configv1.TokenClaimMapping{ - Expression: "claims.groups", - }, - } - } - }, - }), - expectedAuthConfig: authConfigWithUpdates(baseAuthConfig, []func(authConfig *apiserverv1beta1.AuthenticationConfiguration){ - func(authConfig *apiserverv1beta1.AuthenticationConfiguration) { - for i := range authConfig.JWT { - authConfig.JWT[i].ClaimMappings.Groups = apiserverv1beta1.PrefixedClaimOrExpression{ - Expression: "claims.groups", - } - authConfig.JWT[i].ClaimMappings.UID = apiserverv1beta1.ClaimOrExpression{ - Claim: "sub", - } - } - }, - }), - expectError: false, - featureGates: featuregates.NewFeatureGate( - []configv1.FeatureGateName{ - features.FeatureGateExternalOIDCWithAdditionalClaimMappings, - features.FeatureGateExternalOIDCWithUpstreamParity, - }, - []configv1.FeatureGateName{}, - ), - }, - { - name: "auth config with username claim and expression both set, error", - caBundleConfigMap: &baseCABundleConfigMap, - auth: *authWithUpdates(baseAuthResource, []func(auth *configv1.Authentication){ - func(auth *configv1.Authentication) { - for i := range auth.Spec.OIDCProviders { - auth.Spec.OIDCProviders[i].ClaimMappings.Username = configv1.UsernameClaimMapping{ - Claim: "username", - Expression: "claims.email", - } - } - }, - }), - expectError: true, - featureGates: featuregates.NewFeatureGate( - []configv1.FeatureGateName{ - features.FeatureGateExternalOIDCWithAdditionalClaimMappings, - features.FeatureGateExternalOIDCWithUpstreamParity, - }, - []configv1.FeatureGateName{}, - ), - }, - { - name: "auth config with groups claim and expression both set, error", - caBundleConfigMap: &baseCABundleConfigMap, - auth: *authWithUpdates(baseAuthResource, []func(auth *configv1.Authentication){ - func(auth *configv1.Authentication) { - for i := range auth.Spec.OIDCProviders { - auth.Spec.OIDCProviders[i].ClaimMappings.Groups = configv1.PrefixedClaimMapping{ - TokenClaimMapping: configv1.TokenClaimMapping{ - Claim: "groups", - Expression: "claims.groups", - }, - } - } - }, - }), - expectError: true, - featureGates: featuregates.NewFeatureGate( - []configv1.FeatureGateName{ - features.FeatureGateExternalOIDCWithAdditionalClaimMappings, - features.FeatureGateExternalOIDCWithUpstreamParity, - }, - []configv1.FeatureGateName{}, - ), - }, - { - name: "auth config with username expression and prefix set, error", - caBundleConfigMap: &baseCABundleConfigMap, - auth: *authWithUpdates(baseAuthResource, []func(auth *configv1.Authentication){ - func(auth *configv1.Authentication) { - for i := range auth.Spec.OIDCProviders { - auth.Spec.OIDCProviders[i].ClaimMappings.Username = configv1.UsernameClaimMapping{ - Expression: "claims.email", - PrefixPolicy: configv1.Prefix, - Prefix: &configv1.UsernamePrefix{ - PrefixString: "oidc-user:", - }, - } - } - }, - }), - expectError: true, - featureGates: featuregates.NewFeatureGate( - []configv1.FeatureGateName{ - features.FeatureGateExternalOIDCWithAdditionalClaimMappings, - features.FeatureGateExternalOIDCWithUpstreamParity, - }, - []configv1.FeatureGateName{}, - ), - }, - { - name: "auth config with groups expression and prefix set, error", - caBundleConfigMap: &baseCABundleConfigMap, - auth: *authWithUpdates(baseAuthResource, []func(auth *configv1.Authentication){ - func(auth *configv1.Authentication) { - for i := range auth.Spec.OIDCProviders { - auth.Spec.OIDCProviders[i].ClaimMappings.Groups = configv1.PrefixedClaimMapping{ - TokenClaimMapping: configv1.TokenClaimMapping{ - Expression: "claims.groups", - }, - Prefix: "oidc-group:", - } - } - }, - }), - expectError: true, - featureGates: featuregates.NewFeatureGate( - []configv1.FeatureGateName{ - features.FeatureGateExternalOIDCWithAdditionalClaimMappings, - features.FeatureGateExternalOIDCWithUpstreamParity, - }, - []configv1.FeatureGateName{}, - ), - }, - { - name: "auth config with username expression using claims.email without claims.email_verified, error", - caBundleConfigMap: &baseCABundleConfigMap, - auth: *authWithUpdates(baseAuthResource, []func(auth *configv1.Authentication){ - func(auth *configv1.Authentication) { - for i := range auth.Spec.OIDCProviders { - auth.Spec.OIDCProviders[i].ClaimMappings.Username = configv1.UsernameClaimMapping{ - Expression: "claims.email", - } - } - }, - }), - expectError: true, - featureGates: featuregates.NewFeatureGate( - []configv1.FeatureGateName{ - features.FeatureGateExternalOIDCWithAdditionalClaimMappings, - features.FeatureGateExternalOIDCWithUpstreamParity, - }, - []configv1.FeatureGateName{}, - ), - }, - { - name: "auth config with username expression using claims.email with claims.email_verified in claimValidationRule, success", - caBundleConfigMap: &baseCABundleConfigMap, - auth: *authWithUpdates(baseAuthResource, []func(auth *configv1.Authentication){ - func(auth *configv1.Authentication) { - for i := range auth.Spec.OIDCProviders { - auth.Spec.OIDCProviders[i].ClaimMappings.Username = configv1.UsernameClaimMapping{ - Expression: "claims.email", - } - auth.Spec.OIDCProviders[i].ClaimValidationRules = []configv1.TokenClaimValidationRule{ - { - Type: configv1.TokenValidationRuleTypeCEL, - CEL: configv1.TokenClaimValidationCELRule{ - Expression: "claims.email_verified == true", - Message: "email must be verified", - }, - }, - } - } - }, - }), - expectedAuthConfig: authConfigWithUpdates(baseAuthConfig, []func(authConfig *apiserverv1beta1.AuthenticationConfiguration){ - func(authConfig *apiserverv1beta1.AuthenticationConfiguration) { - for i := range authConfig.JWT { - authConfig.JWT[i].ClaimMappings.Username = apiserverv1beta1.PrefixedClaimOrExpression{ - Expression: "claims.email", - } - authConfig.JWT[i].ClaimMappings.UID = apiserverv1beta1.ClaimOrExpression{ - Claim: "sub", - } - authConfig.JWT[i].ClaimValidationRules = []apiserverv1beta1.ClaimValidationRule{ - { - Expression: "claims.email_verified == true", - Message: "email must be verified", - }, - } - } - }, - }), - expectError: false, - featureGates: featuregates.NewFeatureGate( - []configv1.FeatureGateName{ - features.FeatureGateExternalOIDCWithAdditionalClaimMappings, - features.FeatureGateExternalOIDCWithUpstreamParity, - }, - []configv1.FeatureGateName{}, - ), - }, - { - name: "auth config with username expression using both claims.email and claims.email_verified, success", - caBundleConfigMap: &baseCABundleConfigMap, - auth: *authWithUpdates(baseAuthResource, []func(auth *configv1.Authentication){ - func(auth *configv1.Authentication) { - for i := range auth.Spec.OIDCProviders { - auth.Spec.OIDCProviders[i].ClaimMappings.Username = configv1.UsernameClaimMapping{ - Expression: "claims.email_verified ? claims.email : 'unverified'", - } - } - }, - }), - expectedAuthConfig: authConfigWithUpdates(baseAuthConfig, []func(authConfig *apiserverv1beta1.AuthenticationConfiguration){ - func(authConfig *apiserverv1beta1.AuthenticationConfiguration) { - for i := range authConfig.JWT { - authConfig.JWT[i].ClaimMappings.Username = apiserverv1beta1.PrefixedClaimOrExpression{ - Expression: "claims.email_verified ? claims.email : 'unverified'", - } - authConfig.JWT[i].ClaimMappings.UID = apiserverv1beta1.ClaimOrExpression{ - Claim: "sub", - } - } - }, - }), - expectError: false, - featureGates: featuregates.NewFeatureGate( - []configv1.FeatureGateName{ - features.FeatureGateExternalOIDCWithAdditionalClaimMappings, - features.FeatureGateExternalOIDCWithUpstreamParity, - }, - []configv1.FeatureGateName{}, - ), - }, - { - name: "auth config with username expression using claims.email with claims.email_verified in extra, success", - caBundleConfigMap: &baseCABundleConfigMap, - auth: *authWithUpdates(baseAuthResource, []func(auth *configv1.Authentication){ - func(auth *configv1.Authentication) { - for i := range auth.Spec.OIDCProviders { - auth.Spec.OIDCProviders[i].ClaimMappings.Username = configv1.UsernameClaimMapping{ - Expression: "claims.email", - } - auth.Spec.OIDCProviders[i].ClaimMappings.Extra = []configv1.ExtraMapping{ - { - Key: "example.com/email-verified", - ValueExpression: "claims.email_verified ? 'true' : 'false'", - }, - } - auth.Spec.OIDCProviders[i].ClaimValidationRules = []configv1.TokenClaimValidationRule{} - } - }, - }), - expectedAuthConfig: authConfigWithUpdates(baseAuthConfig, []func(authConfig *apiserverv1beta1.AuthenticationConfiguration){ - func(authConfig *apiserverv1beta1.AuthenticationConfiguration) { - for i := range authConfig.JWT { - authConfig.JWT[i].ClaimMappings.Username = apiserverv1beta1.PrefixedClaimOrExpression{ - Expression: "claims.email", - } - authConfig.JWT[i].ClaimMappings.UID = apiserverv1beta1.ClaimOrExpression{ - Claim: "sub", - } - authConfig.JWT[i].ClaimMappings.Extra = []apiserverv1beta1.ExtraMapping{ - { - Key: "example.com/email-verified", - ValueExpression: "claims.email_verified ? 'true' : 'false'", - }, - } - authConfig.JWT[i].ClaimValidationRules = []apiserverv1beta1.ClaimValidationRule{} - } - }, - }), - expectError: false, - featureGates: featuregates.NewFeatureGate( - []configv1.FeatureGateName{ - features.FeatureGateExternalOIDCWithAdditionalClaimMappings, - features.FeatureGateExternalOIDCWithUpstreamParity, - }, - []configv1.FeatureGateName{}, - ), - }, - } { - t.Run(tt.name, func(t *testing.T) { - if tt.configMapIndexer == nil { - tt.configMapIndexer = cache.NewIndexer(cache.MetaNamespaceKeyFunc, cache.Indexers{}) - } - - if tt.caBundleConfigMap != nil { - tt.configMapIndexer.Add(tt.caBundleConfigMap) - } - - c := externalOIDCController{ - configMapLister: corev1listers.NewConfigMapLister(tt.configMapIndexer), - featureGates: tt.featureGates, - } - - gotConfig, err := c.generateAuthConfig(tt.auth) - if tt.expectError && err == nil { - t.Errorf("expected error but didn't get any") - } - - if !tt.expectError && err != nil { - t.Errorf("did not expect any error but got: %v", err) - } - - if !equality.Semantic.DeepEqual(tt.expectedAuthConfig, gotConfig) { - t.Errorf("unexpected config diff: %s", diff.Diff(tt.expectedAuthConfig, gotConfig)) + if tt.expectNotPresent != apierrors.IsNotFound(err) { + t.Errorf("expected configmap to be deleted=%v; got: %v", tt.expectNotPresent, apierrors.IsNotFound(err)) } }) } @@ -1684,182 +469,6 @@ func TestExternalOIDCController_getExistingApplyConfig(t *testing.T) { } } -func TestExternalOIDCController_validateAuthConfig(t *testing.T) { - testServer, err := createTestServer(baseCACert, baseCAPrivateKey, nil) - if err != nil { - t.Fatalf("could not create test server: %v", err) - } - defer testServer.Close() - testServer.StartTLS() - - for _, tt := range []struct { - name string - authConfig apiserverv1beta1.AuthenticationConfiguration - expectError bool - }{ - { - name: "empty config", - authConfig: apiserverv1beta1.AuthenticationConfiguration{}, - expectError: false, - }, - { - name: "issuer with empty URL", - authConfig: *authConfigWithUpdates(baseAuthConfig, []func(authConfig *apiserverv1beta1.AuthenticationConfiguration){ - func(authConfig *apiserverv1beta1.AuthenticationConfiguration) { - authConfig.JWT[0].Issuer.URL = "" - }, - }), - expectError: true, - }, - { - name: "issuer with http URL", - authConfig: *authConfigWithUpdates(baseAuthConfig, []func(authConfig *apiserverv1beta1.AuthenticationConfiguration){ - func(authConfig *apiserverv1beta1.AuthenticationConfiguration) { - authConfig.JWT[0].Issuer.URL = "http://insecure.com" - }, - }), - expectError: true, - }, - { - name: "issuer with invalid CA", - authConfig: *authConfigWithUpdates(baseAuthConfig, []func(authConfig *apiserverv1beta1.AuthenticationConfiguration){ - func(authConfig *apiserverv1beta1.AuthenticationConfiguration) { - authConfig.JWT[0].Issuer.CertificateAuthority = "invalid CA" - }, - }), - expectError: true, - }, - { - name: "cel expression can not compile", - authConfig: *authConfigWithUpdates(baseAuthConfig, []func(*apiserverv1beta1.AuthenticationConfiguration){ - func(authConfig *apiserverv1beta1.AuthenticationConfiguration) { - authConfig.JWT[0].ClaimMappings.UID = apiserverv1beta1.ClaimOrExpression{ - Expression: "#@!$&*(^)", - } - }, - }), - expectError: true, - }, - { - name: "valid auth config", - authConfig: *authConfigWithUpdates(baseAuthConfig, []func(authConfig *apiserverv1beta1.AuthenticationConfiguration){ - func(authConfig *apiserverv1beta1.AuthenticationConfiguration) { - authConfig.JWT[0].Issuer.URL = testServer.URL - }, - }), - expectError: false, - }, - } { - t.Run(tt.name, func(t *testing.T) { - err := validateAuthConfig(tt.authConfig) - if tt.expectError && err == nil { - t.Errorf("expected error but didn't get any") - } - - if !tt.expectError && err != nil { - t.Errorf("did not expect any error but got: %v", err) - } - }) - } -} - -func TestExternalOIDCController_validateCACert(t *testing.T) { - certPool := x509.NewCertPool() - certPool.AddCert(baseCACert) - - testServer, err := createTestServer(baseCACert, baseCAPrivateKey, nil) - if err != nil { - t.Fatalf("could not create test server: %v", err) - } - defer testServer.Close() - testServer.StartTLS() - - t.Run("nil CA cert to use system CAs", func(t *testing.T) { - err := validateCACert(testServer.URL, nil) - if err == nil { - t.Errorf("did not get an error while expecting one") - } - }) - - t.Run("valid CA cert", func(t *testing.T) { - err := validateCACert(testServer.URL, certPool) - if err != nil { - t.Errorf("got error while not expecting one: %v", err) - } - }) - - t.Run("mismatched CA cert", func(t *testing.T) { - anotherCACert, _, err := generateCAKeyPair() - if err != nil { - t.Errorf("could not generate CA keypair: %v", err) - } - certPool := x509.NewCertPool() - certPool.AddCert(anotherCACert) - err = validateCACert(testServer.URL, certPool) - if err == nil { - t.Errorf("did not get an error while expecting one") - } - }) - - t.Run("unknown URL", func(t *testing.T) { - err = validateCACert("https://does-not-exist.com", certPool) - if err == nil { - t.Errorf("did not get an error while expecting one") - } - }) - - t.Run("nil cert pool", func(t *testing.T) { - err := validateCACert(testServer.URL, nil) - if err == nil { - t.Errorf("did not get an error while expecting one") - } - }) - - t.Run("empty cert pool", func(t *testing.T) { - err := validateCACert(testServer.URL, x509.NewCertPool()) - if err == nil { - t.Errorf("did not get an error while expecting one") - } - }) - - t.Run("well-known request error", func(t *testing.T) { - handlerFunc := func(w http.ResponseWriter, r *http.Request) { - time.Sleep(6 * time.Second) - w.WriteHeader(http.StatusOK) - } - - testServer, err := createTestServer(baseCACert, baseCAPrivateKey, handlerFunc) - if err != nil { - t.Fatalf("could not create test server: %v", err) - } - defer testServer.Close() - testServer.StartTLS() - - err = validateCACert(testServer.URL, certPool) - if err == nil { - t.Errorf("did not get an error while expecting one") - } - }) - - t.Run("well-known status not 200 OK", func(t *testing.T) { - handlerFunc := func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusFound) - } - - testServer, err := createTestServer(baseCACert, baseCAPrivateKey, handlerFunc) - if err != nil { - t.Fatalf("could not create test server: %v", err) - } - defer testServer.Close() - testServer.StartTLS() - - err = validateCACert(testServer.URL, certPool) - if err == nil { - t.Errorf("did not get an error while expecting one") - } - }) -} - func generateCAKeyPair() (*x509.Certificate, crypto.Signer, error) { caPrivateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) if err != nil { @@ -1922,36 +531,6 @@ func generateServingCert(caCert *x509.Certificate, caPrivateKey crypto.Signer) ( return &serverCert, nil } -func createTestServer(caCert *x509.Certificate, caPrivateKey crypto.Signer, handlerFunc http.HandlerFunc) (*httptest.Server, error) { - cert := caCert - key := caPrivateKey - var err error - if caCert == nil { - cert, key, err = generateCAKeyPair() - if err != nil { - return nil, err - } - } - - servingCertPair, err := generateServingCert(cert, key) - if err != nil { - return nil, err - } - - if handlerFunc == nil { - handlerFunc = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) - }) - } - - testServer := httptest.NewUnstartedServer(handlerFunc) - testServer.TLS = &tls.Config{ - Certificates: []tls.Certificate{*servingCertPair}, - } - - return testServer, nil -} - func newAuthWithSpec(spec configv1.AuthenticationSpec) *configv1.Authentication { return &configv1.Authentication{ ObjectMeta: metav1.ObjectMeta{ @@ -1961,14 +540,6 @@ func newAuthWithSpec(spec configv1.AuthenticationSpec) *configv1.Authentication } } -func authWithUpdates(auth configv1.Authentication, updateFuncs []func(auth *configv1.Authentication)) *configv1.Authentication { - copy := auth.DeepCopy() - for _, updateFunc := range updateFuncs { - updateFunc(copy) - } - return copy -} - func authConfigWithUpdates(authConfig apiserverv1beta1.AuthenticationConfiguration, updateFuncs []func(authConfig *apiserverv1beta1.AuthenticationConfiguration)) *apiserverv1beta1.AuthenticationConfiguration { copy := authConfig.DeepCopy() for _, updateFunc := range updateFuncs { @@ -2059,3 +630,12 @@ func (s *everFailingIndexer) Replace(objects []interface{}, sKey string) error { func (s *everFailingIndexer) Resync() error { return fmt.Errorf("Resync method not implemented") } + +type mockAuthConfigGenerator[T runtime.Object] struct { + cfg T + err error +} + +func (macg *mockAuthConfigGenerator[T]) GenerateAuthenticationConfiguration(_ *configv1.Authentication) (runtime.Object, error) { + return macg.cfg, macg.err +} diff --git a/pkg/controllers/externaloidc/generation/kubeapiserver/doc.go b/pkg/controllers/externaloidc/generation/kubeapiserver/doc.go new file mode 100644 index 0000000000..5729600391 --- /dev/null +++ b/pkg/controllers/externaloidc/generation/kubeapiserver/doc.go @@ -0,0 +1,10 @@ +// kubeapiserver serves the purpose of generating the +// AuthenticationConfiguration types for configuring the +// Kubernetes API Server with a direct OIDC provider token authenticator. +// +// TODO: Remove this package once the ExternalOIDCExternalClaimsSourcing feature gate +// has been promoted to the default feature set as the Kubernetes API server +// will no longer be the thing getting configured and thus we will not need +// this generation behavior. +// Tracking Jira ticket: https://redhat.atlassian.net/browse/CNTRLPLANE-3454 +package kubeapiserver diff --git a/pkg/controllers/externaloidc/generation/kubeapiserver/generate.go b/pkg/controllers/externaloidc/generation/kubeapiserver/generate.go new file mode 100644 index 0000000000..ac32c57d8e --- /dev/null +++ b/pkg/controllers/externaloidc/generation/kubeapiserver/generate.go @@ -0,0 +1,751 @@ +package kubeapiserver + +import ( + "context" + "crypto/tls" + "crypto/x509" + "errors" + "fmt" + "io" + "net/http" + "net/url" + "strings" + "time" + + configv1 "github.com/openshift/api/config/v1" + "github.com/openshift/api/features" + "github.com/openshift/library-go/pkg/operator/configobserver/featuregates" + "github.com/openshift/library-go/pkg/operator/resource/retry" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + apiserverv1beta1 "k8s.io/apiserver/pkg/apis/apiserver/v1beta1" + authenticationcel "k8s.io/apiserver/pkg/authentication/cel" + corev1listers "k8s.io/client-go/listers/core/v1" + "k8s.io/client-go/util/cert" + "k8s.io/utils/ptr" + + celgo "github.com/google/cel-go/cel" + "github.com/google/cel-go/common/operators" + exprpb "google.golang.org/genproto/googleapis/api/expr/v1alpha1" +) + +// oidcGenerationState holds compilation results gathered during JWT generation +// that are needed for cross-field validation (e.g. email_verified enforcement). +type oidcGenerationState struct { + UsernameResult *authenticationcel.CompilationResult + ExtraResults []authenticationcel.CompilationResult + ClaimValidationResults []authenticationcel.CompilationResult +} + +const ( + configNamespace = "openshift-config" + kindAuthenticationConfiguration = "AuthenticationConfiguration" + oidcDiscoveryEndpointPath = "/.well-known/openid-configuration" +) + +type validationFunc func(*apiserverv1beta1.AuthenticationConfiguration) error + +type AuthenticationConfigurationGenerator struct { + configMapLister corev1listers.ConfigMapLister + featureGates featuregates.FeatureGate + validationFn validationFunc +} + +func NewAuthenticationConfigurationGenerator(cmlister corev1listers.ConfigMapLister, gates featuregates.FeatureGate) *AuthenticationConfigurationGenerator { + return &AuthenticationConfigurationGenerator{ + configMapLister: cmlister, + featureGates: gates, + validationFn: validateApiserverAuthenticationConfiguration, + } +} + +// GenerateAuthenticationConfiguration creates a structured JWT AuthenticationConfiguration for OIDC +// in the kube-apiserver from the configuration found in the authentication/cluster resource. +func (acg *AuthenticationConfigurationGenerator) GenerateAuthenticationConfiguration(auth *configv1.Authentication) (runtime.Object, error) { + authConfig := &apiserverv1beta1.AuthenticationConfiguration{ + TypeMeta: metav1.TypeMeta{ + Kind: kindAuthenticationConfiguration, + APIVersion: apiserverv1beta1.ConfigSchemeGroupVersion.String(), + }, + } + + errs := []error{} + for _, provider := range auth.Spec.OIDCProviders { + jwt, err := generateJWTForProvider(provider, acg.configMapLister, acg.featureGates, auth.Spec.ServiceAccountIssuer) + if err != nil { + errs = append(errs, err) + continue + } + + authConfig.JWT = append(authConfig.JWT, jwt) + } + + if len(errs) > 0 { + return nil, errors.Join(errs...) + } + + if acg.validationFn != nil { + if err := acg.validationFn(authConfig); err != nil { + return nil, err + } + } + + return authConfig, nil +} + +func generateJWTForProvider(provider configv1.OIDCProvider, configMapLister corev1listers.ConfigMapLister, featureGates featuregates.FeatureGate, serviceAccountIssuer string) (apiserverv1beta1.JWTAuthenticator, error) { + out := apiserverv1beta1.JWTAuthenticator{} + + issuer, err := generateIssuer(provider.Issuer, configMapLister, serviceAccountIssuer) + if err != nil { + return apiserverv1beta1.JWTAuthenticator{}, fmt.Errorf("generating issuer for provider %q: %v", provider.Name, err) + } + + state := &oidcGenerationState{} + + claimMappings, err := generateClaimMappings(provider.ClaimMappings, issuer.URL, featureGates, state) + if err != nil { + return apiserverv1beta1.JWTAuthenticator{}, fmt.Errorf("generating claimMappings for provider %q: %v", provider.Name, err) + } + + claimValidationRules, err := generateClaimValidationRules(state, provider.ClaimValidationRules...) + if err != nil { + return apiserverv1beta1.JWTAuthenticator{}, fmt.Errorf("generating claimValidationRules for provider %q: %v", provider.Name, err) + } + + if featureGates.Enabled(features.FeatureGateExternalOIDCWithUpstreamParity) { + if err := validateEmailVerifiedUsage(state); err != nil { + return apiserverv1beta1.JWTAuthenticator{}, fmt.Errorf("validating email claim usage for provider %q: %v", provider.Name, err) + } + var userValidationRules []apiserverv1beta1.UserValidationRule + userValidationRules, err = generateUserValidationRules(provider.UserValidationRules) + if err != nil { + return apiserverv1beta1.JWTAuthenticator{}, fmt.Errorf("generating userValidationRules for provider %q: %v", provider.Name, err) + } + out.UserValidationRules = userValidationRules + } + out.Issuer = issuer + out.ClaimMappings = claimMappings + out.ClaimValidationRules = claimValidationRules + + return out, nil +} + +func generateIssuer(issuer configv1.TokenIssuer, configMapLister corev1listers.ConfigMapLister, serviceAccountIssuer string) (apiserverv1beta1.Issuer, error) { + out := apiserverv1beta1.Issuer{} + + if len(serviceAccountIssuer) > 0 { + if issuer.URL == serviceAccountIssuer { + return apiserverv1beta1.Issuer{}, errors.New("issuer url cannot overlap with the ServiceAccount issuer url") + } + } + + out.URL = issuer.URL + out.AudienceMatchPolicy = apiserverv1beta1.AudienceMatchPolicyMatchAny + + for _, audience := range issuer.Audiences { + out.Audiences = append(out.Audiences, string(audience)) + } + if len(issuer.DiscoveryURL) > 0 { + // Validate the URL scheme + u, err := url.Parse(issuer.DiscoveryURL) + if err != nil { + return apiserverv1beta1.Issuer{}, fmt.Errorf("invalid discovery URL: %v", err) + } + if strings.TrimRight(issuer.DiscoveryURL, "/") == strings.TrimRight(issuer.URL, "/") { + return apiserverv1beta1.Issuer{}, fmt.Errorf("discovery URL must not be identical to issuer URL") + } + if u.Scheme != "https" { + return apiserverv1beta1.Issuer{}, fmt.Errorf("discovery URL must use https, got %q", u.Scheme) + } + if u.Host == "" { + return apiserverv1beta1.Issuer{}, fmt.Errorf("discovery URL must include a host") + } + if u.User != nil { + return apiserverv1beta1.Issuer{}, fmt.Errorf("discovery URL must not contain user info") + } + if len(u.RawQuery) > 0 { + return apiserverv1beta1.Issuer{}, fmt.Errorf("discovery URL must not contain a query string") + } + if len(u.Fragment) > 0 { + return apiserverv1beta1.Issuer{}, fmt.Errorf("discovery URL must not contain a fragment") + } + out.DiscoveryURL = &issuer.DiscoveryURL + } + if len(issuer.CertificateAuthority.Name) > 0 { + ca, err := getCertificateAuthorityFromConfigMap(issuer.CertificateAuthority.Name, configMapLister) + if err != nil { + return apiserverv1beta1.Issuer{}, fmt.Errorf("getting CertificateAuthority for issuer: %v", err) + } + out.CertificateAuthority = ca + } + + return out, nil +} + +func getCertificateAuthorityFromConfigMap(name string, configMapLister corev1listers.ConfigMapLister) (string, error) { + caConfigMap, err := configMapLister.ConfigMaps(configNamespace).Get(name) + if err != nil { + return "", fmt.Errorf("could not retrieve auth configmap %s/%s to check CA bundle: %v", configNamespace, name, err) + } + + caData, ok := caConfigMap.Data["ca-bundle.crt"] + if !ok || len(caData) == 0 { + return "", fmt.Errorf("configmap %s/%s key \"ca-bundle.crt\" missing or empty", configNamespace, name) + } + + return caData, nil +} + +func generateClaimMappings(claimMappings configv1.TokenClaimMappings, issuerURL string, featureGates featuregates.FeatureGate, state *oidcGenerationState) (apiserverv1beta1.ClaimMappings, error) { + out := apiserverv1beta1.ClaimMappings{} + + username, usernameResult, err := generateUsernameClaimMapping(claimMappings.Username, issuerURL, featureGates) + if err != nil { + return apiserverv1beta1.ClaimMappings{}, fmt.Errorf("generating username claim mapping: %v", err) + } + state.UsernameResult = usernameResult + + groups, err := generateGroupsClaimMapping(claimMappings.Groups, featureGates) + if err != nil { + return apiserverv1beta1.ClaimMappings{}, fmt.Errorf("generating group claim mapping: %v", err) + } + out.Username = username + out.Groups = groups + + if featureGates.Enabled(features.FeatureGateExternalOIDCWithAdditionalClaimMappings) { + uid, err := generateUIDClaimMapping(claimMappings.UID) + if err != nil { + return apiserverv1beta1.ClaimMappings{}, fmt.Errorf("generating uid claim mapping: %v", err) + } + + extras, extraResults, err := generateExtraClaimMapping(claimMappings.Extra...) + if err != nil { + return apiserverv1beta1.ClaimMappings{}, fmt.Errorf("generating extra claim mapping: %v", err) + } + + out.UID = uid + out.Extra = extras + state.ExtraResults = extraResults + } + + return out, nil +} + +func generateUsernameClaimMapping(usernameClaimMapping configv1.UsernameClaimMapping, issuerURL string, featureGates featuregates.FeatureGate) (apiserverv1beta1.PrefixedClaimOrExpression, *authenticationcel.CompilationResult, error) { + if featureGates.Enabled(features.FeatureGateExternalOIDCWithUpstreamParity) { + return generateUsernameClaimMappingWithParity(usernameClaimMapping, issuerURL) + } + return generateUsernameClaimMappingLegacy(usernameClaimMapping, issuerURL) +} + +func generateUsernameClaimMappingWithParity(usernameClaimMapping configv1.UsernameClaimMapping, issuerURL string) (apiserverv1beta1.PrefixedClaimOrExpression, *authenticationcel.CompilationResult, error) { + out := apiserverv1beta1.PrefixedClaimOrExpression{} + + if len(usernameClaimMapping.Expression) == 0 && len(usernameClaimMapping.Claim) == 0 { + return out, nil, fmt.Errorf("username claim mapping is required and either claim or expression must be set") + } + + if len(usernameClaimMapping.Expression) > 0 && len(usernameClaimMapping.Claim) > 0 { + return out, nil, fmt.Errorf("username claim mapping must not set both claim and expression") + } + + if len(usernameClaimMapping.Expression) > 0 && usernameClaimMapping.PrefixPolicy == configv1.Prefix { + return out, nil, fmt.Errorf("username claim mappings cannot have a prefix set when using an expression based mapping. If you want to set a prefix while using an expression mapping, set the prefix in the expression") + } + + if len(usernameClaimMapping.Expression) > 0 { + result, err := validateClaimsCELExpression(&authenticationcel.ClaimMappingExpression{ + Expression: usernameClaimMapping.Expression, + }) + if err != nil { + return out, nil, fmt.Errorf("invalid CEL expression: %v", err) + } + out.Expression = usernameClaimMapping.Expression + return out, &result, nil + } + + if len(usernameClaimMapping.Claim) > 0 { + out.Claim = usernameClaimMapping.Claim + + // prefix can only be set when using a direct claim name, so only attempt to set it + // if we are certain we are using a direct claim reference and not an expression + switch usernameClaimMapping.PrefixPolicy { + case configv1.Prefix: + if usernameClaimMapping.Prefix == nil { + return out, nil, fmt.Errorf("nil username prefix while policy expects one") + } + out.Prefix = &usernameClaimMapping.Prefix.PrefixString + case configv1.NoPrefix: + out.Prefix = ptr.To("") + case configv1.NoOpinion: + prefix := "" + if usernameClaimMapping.Claim != "email" { + prefix = issuerURL + "#" + } + out.Prefix = &prefix + default: + return out, nil, fmt.Errorf("invalid username prefix policy: %s", usernameClaimMapping.PrefixPolicy) + } + } + + return out, nil, nil +} + +func generateUsernameClaimMappingLegacy(usernameClaimMapping configv1.UsernameClaimMapping, issuerURL string) (apiserverv1beta1.PrefixedClaimOrExpression, *authenticationcel.CompilationResult, error) { + out := apiserverv1beta1.PrefixedClaimOrExpression{} + + if len(usernameClaimMapping.Claim) == 0 { + return out, nil, fmt.Errorf("username claim is required but an empty value was provided") + } + out.Claim = usernameClaimMapping.Claim + + switch usernameClaimMapping.PrefixPolicy { + case configv1.Prefix: + if usernameClaimMapping.Prefix == nil { + return out, nil, fmt.Errorf("nil username prefix while policy expects one") + } + out.Prefix = &usernameClaimMapping.Prefix.PrefixString + case configv1.NoPrefix: + out.Prefix = ptr.To("") + case configv1.NoOpinion: + prefix := "" + if usernameClaimMapping.Claim != "email" { + prefix = issuerURL + "#" + } + out.Prefix = &prefix + default: + return out, nil, fmt.Errorf("invalid username prefix policy: %s", usernameClaimMapping.PrefixPolicy) + } + + return out, nil, nil +} + +func generateGroupsClaimMapping(groupsMapping configv1.PrefixedClaimMapping, featureGates featuregates.FeatureGate) (apiserverv1beta1.PrefixedClaimOrExpression, error) { + out := apiserverv1beta1.PrefixedClaimOrExpression{} + if featureGates.Enabled(features.FeatureGateExternalOIDCWithUpstreamParity) { + if len(groupsMapping.Expression) > 0 && len(groupsMapping.Claim) > 0 { + return out, fmt.Errorf("groups claim mapping must not set both claim and expression") + } + if len(groupsMapping.Expression) > 0 && len(groupsMapping.Prefix) > 0 { + return apiserverv1beta1.PrefixedClaimOrExpression{}, fmt.Errorf("groups claim mapping must not set prefix when expression is set") + } + + if len(groupsMapping.Expression) > 0 { + if _, err := validateClaimsCELExpression(&authenticationcel.ClaimMappingExpression{ + Expression: groupsMapping.Expression, + }); err != nil { + return apiserverv1beta1.PrefixedClaimOrExpression{}, fmt.Errorf("invalid CEL expression: %v", err) + } + out.Expression = groupsMapping.Expression + return out, nil + } + } + + out.Claim = groupsMapping.Claim + out.Prefix = &groupsMapping.Prefix + + return out, nil +} + +func generateUIDClaimMapping(uid *configv1.TokenClaimOrExpressionMapping) (apiserverv1beta1.ClaimOrExpression, error) { + out := apiserverv1beta1.ClaimOrExpression{} + + // UID mapping can only specify either claim or expression, not both. + // This should be rejected at admission time of the authentications.config.openshift.io CRD. + // Even though this is the case, we still perform a runtime validation to ensure we never + // attempt to create an invalid configuration. + // If neither claim or expression is specified, default the claim to "sub" + switch { + case uid == nil: + out.Claim = "sub" + case len(uid.Claim) > 0 && len(uid.Expression) == 0: + out.Claim = uid.Claim + case len(uid.Expression) > 0 && len(uid.Claim) == 0: + if _, err := validateClaimsCELExpression(&authenticationcel.ClaimMappingExpression{ + Expression: uid.Expression, + }); err != nil { + return apiserverv1beta1.ClaimOrExpression{}, fmt.Errorf("validating expression: %v", err) + } + out.Expression = uid.Expression + case len(uid.Claim) > 0 && len(uid.Expression) > 0: + return apiserverv1beta1.ClaimOrExpression{}, fmt.Errorf("uid mapping must set either claim or expression, not both: %v", uid) + default: + return apiserverv1beta1.ClaimOrExpression{}, fmt.Errorf("unable to handle uid mapping: %v", uid) + } + + return out, nil +} + +func generateExtraClaimMapping(extraMappings ...configv1.ExtraMapping) ([]apiserverv1beta1.ExtraMapping, []authenticationcel.CompilationResult, error) { + out := []apiserverv1beta1.ExtraMapping{} + var compilationResults []authenticationcel.CompilationResult + errs := []error{} + for _, extraMapping := range extraMappings { + extra, result, err := generateExtraMapping(extraMapping) + if err != nil { + errs = append(errs, err) + continue + } + out = append(out, extra) + if result != nil { + compilationResults = append(compilationResults, *result) + } + } + if len(errs) > 0 { + return nil, nil, errors.Join(errs...) + } + return out, compilationResults, nil +} + +func generateExtraMapping(extraMapping configv1.ExtraMapping) (apiserverv1beta1.ExtraMapping, *authenticationcel.CompilationResult, error) { + out := apiserverv1beta1.ExtraMapping{} + + if len(extraMapping.Key) == 0 { + return apiserverv1beta1.ExtraMapping{}, nil, fmt.Errorf("extra mapping must set a key, but none was provided: %v", extraMapping) + } + + if len(extraMapping.ValueExpression) == 0 { + return apiserverv1beta1.ExtraMapping{}, nil, fmt.Errorf("extra mapping must set a valueExpression, but none was provided: %v", extraMapping) + } + + result, err := validateClaimsCELExpression(&authenticationcel.ExtraMappingExpression{ + Key: extraMapping.Key, + Expression: extraMapping.ValueExpression, + }) + if err != nil { + return apiserverv1beta1.ExtraMapping{}, nil, fmt.Errorf("validating expression: %v", err) + } + + out.Key = extraMapping.Key + out.ValueExpression = extraMapping.ValueExpression + + return out, &result, nil +} + +func generateClaimValidationRules(state *oidcGenerationState, claimValidationRules ...configv1.TokenClaimValidationRule) ([]apiserverv1beta1.ClaimValidationRule, error) { + out := []apiserverv1beta1.ClaimValidationRule{} + errs := []error{} + for _, claimValidationRule := range claimValidationRules { + rule, result, err := generateClaimValidationRule(claimValidationRule) + if err != nil { + errs = append(errs, fmt.Errorf("generating claimValidationRule: %v", err)) + continue + } + out = append(out, rule) + if result != nil { + state.ClaimValidationResults = append(state.ClaimValidationResults, *result) + } + } + if len(errs) > 0 { + return nil, errors.Join(errs...) + } + return out, nil +} + +func generateClaimValidationRule(claimValidationRule configv1.TokenClaimValidationRule) (apiserverv1beta1.ClaimValidationRule, *authenticationcel.CompilationResult, error) { + out := apiserverv1beta1.ClaimValidationRule{} + switch claimValidationRule.Type { + case configv1.TokenValidationRuleTypeRequiredClaim: + if claimValidationRule.RequiredClaim == nil { + return apiserverv1beta1.ClaimValidationRule{}, nil, fmt.Errorf("claimValidationRule.type is %s and requiredClaim is not set", configv1.TokenValidationRuleTypeRequiredClaim) + } + out.Claim = claimValidationRule.RequiredClaim.Claim + out.RequiredValue = claimValidationRule.RequiredClaim.RequiredValue + case configv1.TokenValidationRuleTypeCEL: + if len(claimValidationRule.CEL.Expression) == 0 { + return apiserverv1beta1.ClaimValidationRule{}, nil, fmt.Errorf("claimValidationRule.type is %s and expression is not set", configv1.TokenValidationRuleTypeCEL) + } + result, err := validateClaimsCELExpression(&authenticationcel.ClaimValidationCondition{ + Expression: claimValidationRule.CEL.Expression, + }) + if err != nil { + return apiserverv1beta1.ClaimValidationRule{}, nil, fmt.Errorf("invalid CEL expression: %v", err) + } + out.Expression = claimValidationRule.CEL.Expression + out.Message = claimValidationRule.CEL.Message + return out, &result, nil + default: + return apiserverv1beta1.ClaimValidationRule{}, nil, fmt.Errorf("unknown claimValidationRule type %q", claimValidationRule.Type) + } + return out, nil, nil +} + +func generateUserValidationRule(rule configv1.TokenUserValidationRule) (apiserverv1beta1.UserValidationRule, error) { + if len(rule.Expression) == 0 { + return apiserverv1beta1.UserValidationRule{}, fmt.Errorf("userValidationRule expression must be non-empty") + } + + // validate CEL expression + if _, err := validateUserCELExpression(&authenticationcel.UserValidationCondition{ + Expression: rule.Expression, + }); err != nil { + return apiserverv1beta1.UserValidationRule{}, fmt.Errorf("invalid CEL expression: %v", err) + } + + return apiserverv1beta1.UserValidationRule{ + Expression: rule.Expression, + Message: rule.Message, + }, nil +} + +func generateUserValidationRules(rules []configv1.TokenUserValidationRule) ([]apiserverv1beta1.UserValidationRule, error) { + out := []apiserverv1beta1.UserValidationRule{} + errs := []error{} + + for _, r := range rules { + uvr, err := generateUserValidationRule(r) + if err != nil { + errs = append(errs, fmt.Errorf("generating userValidationRule: %v", err)) + continue + } + out = append(out, uvr) + } + + if len(errs) > 0 { + return nil, errors.Join(errs...) + } + + return out, nil +} + +func validateApiserverAuthenticationConfiguration(auth *apiserverv1beta1.AuthenticationConfiguration) error { + if auth == nil { + return nil + } + + for _, jwt := range auth.JWT { + var caCertPool *x509.CertPool + var err error + if len(jwt.Issuer.CertificateAuthority) > 0 { + caCertPool, err = cert.NewPoolFromBytes([]byte(jwt.Issuer.CertificateAuthority)) + if err != nil { + return fmt.Errorf("issuer CA is invalid: %v", err) + } + } + + // make sure we can access the issuer with the given cert pool (system CAs used if pool is empty) + url := strings.TrimSuffix(jwt.Issuer.URL, "/") + oidcDiscoveryEndpointPath + if jwt.Issuer.DiscoveryURL != nil { + url = *jwt.Issuer.DiscoveryURL + } + + if err := validateCACert(url, caCertPool); err != nil { + certMessage := "using the specified CA cert" + if caCertPool == nil { + certMessage = "using the system CAs" + } + return fmt.Errorf("could not validate IDP URL %s: %v", certMessage, err) + } + } + + return nil +} + +// validateCACert makes a request to the provider's well-known endpoint using the +// specified CA cert pool to validate that the certs in the pool match the host. +func validateCACert(hostURL string, caCertPool *x509.CertPool) error { + client := &http.Client{ + Timeout: 5 * time.Second, + } + + transport := http.DefaultTransport.(*http.Transport).Clone() + if transport == nil { + transport = &http.Transport{} + } + + if transport.TLSClientConfig == nil { + transport.TLSClientConfig = &tls.Config{ + MinVersion: tls.VersionTLS12, + } + } + + transport.TLSClientConfig.RootCAs = caCertPool + client.Transport = transport + + req, err := http.NewRequest(http.MethodGet, hostURL, nil) + if err != nil { + return fmt.Errorf("could not create well-known HTTP request: %v", err) + } + + var resp *http.Response + var connErr error + retryCtx, cancel := context.WithTimeout(req.Context(), 10*time.Second) + defer cancel() + if err := retry.RetryOnConnectionErrors(retryCtx, func(ctx context.Context) (done bool, err error) { + resp, connErr = client.Do(req.WithContext(ctx)) + return connErr == nil, connErr + }); err != nil { + return fmt.Errorf("persistent well-known GET error: %v", err) + } + if connErr != nil { + return fmt.Errorf("GET well-known error: %v", connErr) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + body, err := io.ReadAll(resp.Body) + if err != nil { + return fmt.Errorf("unable to read response body; HTTP status: %s; error: %v", resp.Status, err) + } + + return fmt.Errorf("unexpected well-known status code %s: %s", resp.Status, body) + } + + return nil +} + +// validateClaimsCELExpression validates a CEL expression using the provided expression accessor. +// It uses the default authentication CEL compiler that the KAS uses and thus defaults to +// validating CEL expressions based on the version of the k8s dependencies used by the +// cluster-authentication-operator. +// Compiles the expression with the `claims` environment variable available. +func validateClaimsCELExpression(expressionAccessor authenticationcel.ExpressionAccessor) (authenticationcel.CompilationResult, error) { + return authenticationcel.NewDefaultCompiler().CompileClaimsExpression(expressionAccessor) +} + +// validateUserCELExpression validates a user CEL expression using the user.* scope. +func validateUserCELExpression(expressionAccessor authenticationcel.ExpressionAccessor) (authenticationcel.CompilationResult, error) { + return authenticationcel.NewDefaultCompiler().CompileUserExpression(expressionAccessor) +} + +// validateEmailVerifiedUsage enforces that when claims.email is used in the +// username expression, claims.email_verified must be referenced in at least +// one of: username.expression, extra[*].valueExpression, or +// claimValidationRules[*].cel.expression. +// This mirrors the upstream KAS validation logic. +func validateEmailVerifiedUsage(state *oidcGenerationState) error { + if state == nil { + return nil + } + + if state.UsernameResult == nil { + return nil + } + + if !usesEmailClaim(state.UsernameResult.AST) { + return nil + } + + if usesEmailVerifiedClaim(state.UsernameResult.AST) || anyUsesEmailVerifiedClaim(state.ExtraResults) || anyUsesEmailVerifiedClaim(state.ClaimValidationResults) { + return nil + } + + return fmt.Errorf("claims.email_verified must be used in claimMappings.username.expression or claimMappings.extra[*].valueExpression or claimValidationRules[*].expression when claims.email is used in claimMappings.username.expression") +} + +// usesEmailClaim, usesEmailVerifiedClaim, anyUsesEmailVerifiedClaim, hasSelectExp, +// isIdentOperand, and isConstField are copied from the upstream Kubernetes apiserver +// CEL validation logic introduced in https://github.com/kubernetes/kubernetes/pull/123737 (commit 121607e): +// https://github.com/kubernetes/kubernetes/blob/bfb362c57578518bed8e08a56a7318bab9b57429/staging/src/k8s.io/apiserver/pkg/apis/apiserver/validation/validation.go#L443 +func usesEmailClaim(ast *celgo.Ast) bool { + if ast == nil { + return false + } + return hasSelectExp(ast.Expr(), "claims", "email") +} + +func usesEmailVerifiedClaim(ast *celgo.Ast) bool { + if ast == nil { + return false + } + return hasSelectExp(ast.Expr(), "claims", "email_verified") +} + +func anyUsesEmailVerifiedClaim(results []authenticationcel.CompilationResult) bool { + for _, result := range results { + if usesEmailVerifiedClaim(result.AST) { + return true + } + } + return false +} + +func hasSelectExp(exp *exprpb.Expr, operand, field string) bool { + if exp == nil { + return false + } + switch e := exp.ExprKind.(type) { + case *exprpb.Expr_ConstExpr, + *exprpb.Expr_IdentExpr: + return false + case *exprpb.Expr_SelectExpr: + s := e.SelectExpr + if s == nil { + return false + } + if isIdentOperand(s.Operand, operand) && s.Field == field { + return true + } + return hasSelectExp(s.Operand, operand, field) + case *exprpb.Expr_CallExpr: + c := e.CallExpr + if c == nil { + return false + } + if c.Target == nil && c.Function == operators.OptSelect && len(c.Args) == 2 && + isIdentOperand(c.Args[0], operand) && isConstField(c.Args[1], field) { + return true + } + for _, arg := range c.Args { + if hasSelectExp(arg, operand, field) { + return true + } + } + return hasSelectExp(c.Target, operand, field) + case *exprpb.Expr_ListExpr: + l := e.ListExpr + if l == nil { + return false + } + for _, element := range l.Elements { + if hasSelectExp(element, operand, field) { + return true + } + } + return false + case *exprpb.Expr_StructExpr: + s := e.StructExpr + if s == nil { + return false + } + for _, entry := range s.Entries { + if hasSelectExp(entry.GetMapKey(), operand, field) { + return true + } + if hasSelectExp(entry.Value, operand, field) { + return true + } + } + return false + case *exprpb.Expr_ComprehensionExpr: + c := e.ComprehensionExpr + if c == nil { + return false + } + return hasSelectExp(c.IterRange, operand, field) || + hasSelectExp(c.AccuInit, operand, field) || + hasSelectExp(c.LoopCondition, operand, field) || + hasSelectExp(c.LoopStep, operand, field) || + hasSelectExp(c.Result, operand, field) + default: + return false + } +} + +func isIdentOperand(exp *exprpb.Expr, operand string) bool { + if len(operand) == 0 { + return false + } + id := exp.GetIdentExpr() + return id != nil && id.Name == operand +} + +func isConstField(exp *exprpb.Expr, field string) bool { + if len(field) == 0 { + return false + } + c := exp.GetConstExpr() + return c != nil && c.GetStringValue() == field +} diff --git a/pkg/controllers/externaloidc/generation/kubeapiserver/generate_test.go b/pkg/controllers/externaloidc/generation/kubeapiserver/generate_test.go new file mode 100644 index 0000000000..f94ab3a699 --- /dev/null +++ b/pkg/controllers/externaloidc/generation/kubeapiserver/generate_test.go @@ -0,0 +1,1518 @@ +package kubeapiserver + +import ( + "bytes" + "crypto" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/rsa" + "crypto/tls" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "errors" + "fmt" + "math/big" + "net" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + configv1 "github.com/openshift/api/config/v1" + "github.com/openshift/api/features" + "github.com/openshift/library-go/pkg/operator/configobserver/featuregates" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + apiserverv1beta1 "k8s.io/apiserver/pkg/apis/apiserver/v1beta1" + corev1listers "k8s.io/client-go/listers/core/v1" + "k8s.io/client-go/tools/cache" + certutil "k8s.io/client-go/util/cert" + "k8s.io/utils/ptr" +) + +var ( + testCertData = "fake-ca-cert" + + baseAuthResource = *newAuthWithSpec(configv1.AuthenticationSpec{ + Type: configv1.AuthenticationTypeOIDC, + OIDCProviders: []configv1.OIDCProvider{ + { + Name: "test-oidc-provider", + Issuer: configv1.TokenIssuer{ + CertificateAuthority: configv1.ConfigMapNameReference{Name: "oidc-ca-bundle"}, + Audiences: []configv1.TokenAudience{"my-test-aud", "another-aud"}, + }, + OIDCClients: []configv1.OIDCClientConfig{ + { + ComponentName: "console", + ComponentNamespace: "openshift-console", + ClientID: "console-oidc-client", + }, + { + ComponentName: "kube-apiserver", + ComponentNamespace: "openshift-kube-apiserver", + ClientID: "test-oidc-client", + }, + }, + ClaimMappings: configv1.TokenClaimMappings{ + Username: configv1.UsernameClaimMapping{ + Claim: "username", + PrefixPolicy: configv1.Prefix, + Prefix: &configv1.UsernamePrefix{ + PrefixString: "oidc-user:", + }, + }, + Groups: configv1.PrefixedClaimMapping{ + TokenClaimMapping: configv1.TokenClaimMapping{ + Claim: "groups", + }, + Prefix: "oidc-group:", + }, + }, + ClaimValidationRules: []configv1.TokenClaimValidationRule{ + { + Type: configv1.TokenValidationRuleTypeRequiredClaim, + RequiredClaim: &configv1.TokenRequiredClaim{ + Claim: "username", + RequiredValue: "test-username", + }, + }, + { + Type: configv1.TokenValidationRuleTypeRequiredClaim, + RequiredClaim: &configv1.TokenRequiredClaim{ + Claim: "email", + RequiredValue: "test-email", + }, + }, + }, + }, + }, + }) + + baseAuthConfig = apiserverv1beta1.AuthenticationConfiguration{ + TypeMeta: metav1.TypeMeta{ + Kind: kindAuthenticationConfiguration, + APIVersion: apiserverv1beta1.ConfigSchemeGroupVersion.String(), + }, + JWT: []apiserverv1beta1.JWTAuthenticator{ + { + Issuer: apiserverv1beta1.Issuer{ + Audiences: []string{"my-test-aud", "another-aud"}, + CertificateAuthority: testCertData, + AudienceMatchPolicy: apiserverv1beta1.AudienceMatchPolicyMatchAny, + }, + ClaimMappings: apiserverv1beta1.ClaimMappings{ + Username: apiserverv1beta1.PrefixedClaimOrExpression{ + Claim: "username", + Prefix: ptr.To("oidc-user:"), + }, + Groups: apiserverv1beta1.PrefixedClaimOrExpression{ + Claim: "groups", + Prefix: ptr.To("oidc-group:"), + }, + }, + ClaimValidationRules: []apiserverv1beta1.ClaimValidationRule{ + { + Claim: "username", + RequiredValue: "test-username", + }, + { + Claim: "email", + RequiredValue: "test-email", + }, + }, + }, + }, + } + + baseCABundleConfigMap = corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "oidc-ca-bundle", + Namespace: configNamespace, + }, + Data: map[string]string{ + "ca-bundle.crt": testCertData, + }, + } + + caBundleConfigMapInvalidKey = corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "oidc-ca-bundle", + Namespace: configNamespace, + }, + Data: map[string]string{ + "invalid": testCertData, + }, + } + + caBundleConfigMapNoData = corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "oidc-ca-bundle", + Namespace: configNamespace, + }, + Data: map[string]string{ + "ca-bundle.crt": "", + }, + } +) + +func TestAuthenticationConfigurationGeneratorGenerateAuthenticationConfiguration(t *testing.T) { + for _, tt := range []struct { + name string + + auth configv1.Authentication + caBundleConfigMap *corev1.ConfigMap + configMapIndexer cache.Indexer + + expectedAuthConfig *apiserverv1beta1.AuthenticationConfiguration + expectError bool + featureGates featuregates.FeatureGate + configValidator validationFunc + }{ + { + name: "ca bundle configmap lister error", + auth: baseAuthResource, + configMapIndexer: cache.Indexer(&everFailingIndexer{}), + expectError: true, + featureGates: featuregates.NewFeatureGate( + []configv1.FeatureGateName{}, + []configv1.FeatureGateName{ + features.FeatureGateExternalOIDCWithAdditionalClaimMappings, + features.FeatureGateExternalOIDCWithUpstreamParity, + }, + ), + }, + { + name: "ca bundle configmap without required key", + auth: baseAuthResource, + caBundleConfigMap: &caBundleConfigMapInvalidKey, + expectError: true, + featureGates: featuregates.NewFeatureGate( + []configv1.FeatureGateName{}, + []configv1.FeatureGateName{ + features.FeatureGateExternalOIDCWithAdditionalClaimMappings, + features.FeatureGateExternalOIDCWithUpstreamParity, + }, + ), + }, + { + name: "ca bundle configmap with no data", + auth: baseAuthResource, + caBundleConfigMap: &caBundleConfigMapNoData, + expectError: true, + featureGates: featuregates.NewFeatureGate( + []configv1.FeatureGateName{}, + []configv1.FeatureGateName{ + features.FeatureGateExternalOIDCWithAdditionalClaimMappings, + features.FeatureGateExternalOIDCWithUpstreamParity, + }, + ), + }, + { + name: "auth config nil prefix when required", + caBundleConfigMap: &baseCABundleConfigMap, + auth: *authWithUpdates(baseAuthResource, []func(auth *configv1.Authentication){ + func(auth *configv1.Authentication) { + for i := range auth.Spec.OIDCProviders { + auth.Spec.OIDCProviders[i].ClaimMappings.Username = configv1.UsernameClaimMapping{ + Claim: "username", + PrefixPolicy: configv1.Prefix, + Prefix: nil, + } + } + }, + }), + expectError: true, + featureGates: featuregates.NewFeatureGate( + []configv1.FeatureGateName{}, + []configv1.FeatureGateName{ + features.FeatureGateExternalOIDCWithAdditionalClaimMappings, + features.FeatureGateExternalOIDCWithUpstreamParity, + }, + ), + }, + { + name: "auth config invalid prefix policy", + caBundleConfigMap: &baseCABundleConfigMap, + auth: *authWithUpdates(baseAuthResource, []func(auth *configv1.Authentication){ + func(auth *configv1.Authentication) { + for i := range auth.Spec.OIDCProviders { + auth.Spec.OIDCProviders[i].ClaimMappings.Username = configv1.UsernameClaimMapping{ + Claim: "username", + PrefixPolicy: configv1.UsernamePrefixPolicy("invalid-policy"), + } + } + }, + }), + expectError: true, + featureGates: featuregates.NewFeatureGate( + []configv1.FeatureGateName{}, + []configv1.FeatureGateName{ + features.FeatureGateExternalOIDCWithAdditionalClaimMappings, + features.FeatureGateExternalOIDCWithUpstreamParity, + }, + ), + }, + { + name: "auth config with nil claim in validation rule", + caBundleConfigMap: &baseCABundleConfigMap, + auth: *authWithUpdates(baseAuthResource, []func(auth *configv1.Authentication){ + func(copy *configv1.Authentication) { + for i := range copy.Spec.OIDCProviders { + if len(copy.Spec.OIDCProviders[i].ClaimValidationRules) == 0 { + copy.Spec.OIDCProviders[i].ClaimValidationRules = make([]configv1.TokenClaimValidationRule, 0) + } + copy.Spec.OIDCProviders[i].ClaimValidationRules = append( + copy.Spec.OIDCProviders[i].ClaimValidationRules, + configv1.TokenClaimValidationRule{ + Type: configv1.TokenValidationRuleTypeRequiredClaim, + RequiredClaim: nil, + }, + ) + } + }, + }), + expectError: true, + featureGates: featuregates.NewFeatureGate( + []configv1.FeatureGateName{}, + []configv1.FeatureGateName{ + features.FeatureGateExternalOIDCWithAdditionalClaimMappings, + features.FeatureGateExternalOIDCWithUpstreamParity, + }, + ), + }, + { + name: "valid auth config", + caBundleConfigMap: &baseCABundleConfigMap, + auth: *authWithUpdates(baseAuthResource, []func(auth *configv1.Authentication){ + func(auth *configv1.Authentication) { + for i := range auth.Spec.OIDCProviders { + auth.Spec.OIDCProviders[i].Issuer.URL = "https://example.com" + } + }, + }), + expectedAuthConfig: authConfigWithUpdates(baseAuthConfig, []func(authConfig *apiserverv1beta1.AuthenticationConfiguration){ + func(authConfig *apiserverv1beta1.AuthenticationConfiguration) { + authConfig.JWT[0].Issuer.URL = "https://example.com" + }, + }), + expectError: false, + featureGates: featuregates.NewFeatureGate( + []configv1.FeatureGateName{}, + []configv1.FeatureGateName{ + features.FeatureGateExternalOIDCWithAdditionalClaimMappings, + features.FeatureGateExternalOIDCWithUpstreamParity, + }, + ), + }, + { + name: "valid auth config during generation, validator fails", + caBundleConfigMap: &baseCABundleConfigMap, + auth: *authWithUpdates(baseAuthResource, []func(auth *configv1.Authentication){ + func(auth *configv1.Authentication) { + for i := range auth.Spec.OIDCProviders { + auth.Spec.OIDCProviders[i].Issuer.URL = "https://example.com" + } + }, + }), + expectError: true, + featureGates: featuregates.NewFeatureGate( + []configv1.FeatureGateName{}, + []configv1.FeatureGateName{ + features.FeatureGateExternalOIDCWithAdditionalClaimMappings, + features.FeatureGateExternalOIDCWithUpstreamParity, + }, + ), + configValidator: func(_ *apiserverv1beta1.AuthenticationConfiguration) error { + return errors.New("boom") + }, + }, + { + name: "valid auth config with empty CA name", + auth: *authWithUpdates(baseAuthResource, []func(auth *configv1.Authentication){ + func(auth *configv1.Authentication) { + for i := range auth.Spec.OIDCProviders { + auth.Spec.OIDCProviders[i].Issuer.CertificateAuthority.Name = "" + } + }, + }), + expectedAuthConfig: authConfigWithUpdates(baseAuthConfig, []func(authConfig *apiserverv1beta1.AuthenticationConfiguration){ + func(authConfig *apiserverv1beta1.AuthenticationConfiguration) { + for i := range authConfig.JWT { + authConfig.JWT[i].Issuer.CertificateAuthority = "" + } + }, + }), + expectError: false, + featureGates: featuregates.NewFeatureGate( + []configv1.FeatureGateName{}, + []configv1.FeatureGateName{ + features.FeatureGateExternalOIDCWithAdditionalClaimMappings, + features.FeatureGateExternalOIDCWithUpstreamParity, + }, + ), + }, + { + name: "auth config with default prefix policy", + caBundleConfigMap: &baseCABundleConfigMap, + auth: *authWithUpdates(baseAuthResource, []func(auth *configv1.Authentication){ + func(auth *configv1.Authentication) { + for i := range auth.Spec.OIDCProviders { + auth.Spec.OIDCProviders[i].Issuer.URL = "https://example.com" + auth.Spec.OIDCProviders[i].ClaimMappings.Username = configv1.UsernameClaimMapping{ + Claim: "email", + PrefixPolicy: configv1.NoOpinion, + } + } + }, + }), + expectedAuthConfig: authConfigWithUpdates(baseAuthConfig, []func(authConfig *apiserverv1beta1.AuthenticationConfiguration){ + func(authConfig *apiserverv1beta1.AuthenticationConfiguration) { + for i := range authConfig.JWT { + authConfig.JWT[i].Issuer.URL = "https://example.com" + authConfig.JWT[i].ClaimMappings.Username = apiserverv1beta1.PrefixedClaimOrExpression{ + Claim: "email", + Prefix: ptr.To(""), + } + } + }, + }), + expectError: false, + featureGates: featuregates.NewFeatureGate( + []configv1.FeatureGateName{}, + []configv1.FeatureGateName{ + features.FeatureGateExternalOIDCWithAdditionalClaimMappings, + features.FeatureGateExternalOIDCWithUpstreamParity, + }, + ), + }, + { + name: "auth config with default prefix policy and username claim email", + caBundleConfigMap: &baseCABundleConfigMap, + auth: *authWithUpdates(baseAuthResource, []func(auth *configv1.Authentication){ + func(auth *configv1.Authentication) { + for i := range auth.Spec.OIDCProviders { + auth.Spec.OIDCProviders[i].Issuer.URL = "https://example.com" + auth.Spec.OIDCProviders[i].ClaimMappings.Username = configv1.UsernameClaimMapping{ + Claim: "username", + PrefixPolicy: configv1.NoOpinion, + } + } + }, + }), + expectedAuthConfig: authConfigWithUpdates(baseAuthConfig, []func(authConfig *apiserverv1beta1.AuthenticationConfiguration){ + func(authConfig *apiserverv1beta1.AuthenticationConfiguration) { + for i := range authConfig.JWT { + authConfig.JWT[i].Issuer.URL = "https://example.com" + authConfig.JWT[i].ClaimMappings.Username = apiserverv1beta1.PrefixedClaimOrExpression{ + Claim: "username", + Prefix: ptr.To("https://example.com#"), + } + } + }, + }), + expectError: false, + featureGates: featuregates.NewFeatureGate( + []configv1.FeatureGateName{}, + []configv1.FeatureGateName{ + features.FeatureGateExternalOIDCWithAdditionalClaimMappings, + features.FeatureGateExternalOIDCWithUpstreamParity, + }, + ), + }, + { + name: "auth config with no prefix policy", + caBundleConfigMap: &baseCABundleConfigMap, + auth: *authWithUpdates(baseAuthResource, []func(auth *configv1.Authentication){ + func(auth *configv1.Authentication) { + for i := range auth.Spec.OIDCProviders { + auth.Spec.OIDCProviders[i].Issuer.URL = "https://example.com" + auth.Spec.OIDCProviders[i].ClaimMappings.Username = configv1.UsernameClaimMapping{ + Claim: "username", + PrefixPolicy: configv1.NoPrefix, + } + } + }, + }), + expectedAuthConfig: authConfigWithUpdates(baseAuthConfig, []func(authConfig *apiserverv1beta1.AuthenticationConfiguration){ + func(authConfig *apiserverv1beta1.AuthenticationConfiguration) { + for i := range authConfig.JWT { + authConfig.JWT[i].Issuer.URL = "https://example.com" + authConfig.JWT[i].ClaimMappings.Username = apiserverv1beta1.PrefixedClaimOrExpression{ + Claim: "username", + Prefix: ptr.To(""), + } + } + }, + }), + expectError: false, + featureGates: featuregates.NewFeatureGate( + []configv1.FeatureGateName{}, + []configv1.FeatureGateName{ + features.FeatureGateExternalOIDCWithAdditionalClaimMappings, + features.FeatureGateExternalOIDCWithUpstreamParity, + }, + ), + }, + { + name: "auth config with username claim prefix", + caBundleConfigMap: &baseCABundleConfigMap, + auth: *authWithUpdates(baseAuthResource, []func(auth *configv1.Authentication){ + func(auth *configv1.Authentication) { + for i := range auth.Spec.OIDCProviders { + auth.Spec.OIDCProviders[i].Issuer.URL = "https://example.com" + auth.Spec.OIDCProviders[i].ClaimMappings.Username = configv1.UsernameClaimMapping{ + Claim: "username", + PrefixPolicy: configv1.Prefix, + Prefix: &configv1.UsernamePrefix{ + PrefixString: "oidc-user:", + }, + } + } + }, + }), + expectedAuthConfig: authConfigWithUpdates(baseAuthConfig, []func(authConfig *apiserverv1beta1.AuthenticationConfiguration){ + func(authConfig *apiserverv1beta1.AuthenticationConfiguration) { + for i := range authConfig.JWT { + authConfig.JWT[i].Issuer.URL = "https://example.com" + authConfig.JWT[i].ClaimMappings.Username = apiserverv1beta1.PrefixedClaimOrExpression{ + Claim: "username", + Prefix: ptr.To("oidc-user:"), + } + } + }, + }), + expectError: false, + featureGates: featuregates.NewFeatureGate( + []configv1.FeatureGateName{}, + []configv1.FeatureGateName{ + features.FeatureGateExternalOIDCWithAdditionalClaimMappings, + features.FeatureGateExternalOIDCWithUpstreamParity, + }, + ), + }, + { + name: "auth config with empty string for username claim", + caBundleConfigMap: &baseCABundleConfigMap, + auth: *authWithUpdates(baseAuthResource, []func(auth *configv1.Authentication){ + func(auth *configv1.Authentication) { + for i := range auth.Spec.OIDCProviders { + auth.Spec.OIDCProviders[i].ClaimMappings.Username = configv1.UsernameClaimMapping{ + Claim: "", + } + } + }, + }), + expectError: true, + featureGates: featuregates.NewFeatureGate( + []configv1.FeatureGateName{}, + []configv1.FeatureGateName{ + features.FeatureGateExternalOIDCWithAdditionalClaimMappings, + features.FeatureGateExternalOIDCWithUpstreamParity, + }, + ), + }, + { + name: "auth config with no uid claim or expression", + caBundleConfigMap: &baseCABundleConfigMap, + auth: baseAuthResource, + expectedAuthConfig: authConfigWithUpdates(baseAuthConfig, []func(authConfig *apiserverv1beta1.AuthenticationConfiguration){ + func(authConfig *apiserverv1beta1.AuthenticationConfiguration) { + for i := range authConfig.JWT { + authConfig.JWT[i].ClaimMappings.UID.Claim = "sub" + } + }, + }), + expectError: false, + featureGates: featuregates.NewFeatureGate( + []configv1.FeatureGateName{ + features.FeatureGateExternalOIDCWithAdditionalClaimMappings, + features.FeatureGateExternalOIDCWithUpstreamParity, + }, + []configv1.FeatureGateName{}, + ), + }, + { + name: "auth config with uid claim and expression", + caBundleConfigMap: &baseCABundleConfigMap, + auth: *authWithUpdates(baseAuthResource, []func(auth *configv1.Authentication){ + func(auth *configv1.Authentication) { + for i := range auth.Spec.OIDCProviders { + auth.Spec.OIDCProviders[i].ClaimMappings.UID = &configv1.TokenClaimOrExpressionMapping{ + Claim: "sub", + Expression: "claims.sub", + } + } + }, + }), + expectError: true, + featureGates: featuregates.NewFeatureGate( + []configv1.FeatureGateName{ + features.FeatureGateExternalOIDCWithAdditionalClaimMappings, + features.FeatureGateExternalOIDCWithUpstreamParity, + }, + []configv1.FeatureGateName{}, + ), + }, + { + name: "auth config with uid expression", + caBundleConfigMap: &baseCABundleConfigMap, + auth: *authWithUpdates(baseAuthResource, []func(auth *configv1.Authentication){ + func(auth *configv1.Authentication) { + for i := range auth.Spec.OIDCProviders { + auth.Spec.OIDCProviders[i].ClaimMappings.UID = &configv1.TokenClaimOrExpressionMapping{ + Claim: "", + Expression: "claims.sub", + } + } + }, + }), + expectedAuthConfig: authConfigWithUpdates(baseAuthConfig, []func(authConfig *apiserverv1beta1.AuthenticationConfiguration){ + func(authConfig *apiserverv1beta1.AuthenticationConfiguration) { + for i := range authConfig.JWT { + authConfig.JWT[i].ClaimMappings.UID.Claim = "" + authConfig.JWT[i].ClaimMappings.UID.Expression = "claims.sub" + } + }, + }), + expectError: false, + featureGates: featuregates.NewFeatureGate( + []configv1.FeatureGateName{ + features.FeatureGateExternalOIDCWithAdditionalClaimMappings, + features.FeatureGateExternalOIDCWithUpstreamParity, + }, + []configv1.FeatureGateName{}, + ), + }, + { + name: "auth config with extra missing key", + caBundleConfigMap: &baseCABundleConfigMap, + auth: *authWithUpdates(baseAuthResource, []func(auth *configv1.Authentication){ + func(auth *configv1.Authentication) { + for i := range auth.Spec.OIDCProviders { + auth.Spec.OIDCProviders[i].ClaimMappings.Extra = []configv1.ExtraMapping{ + { + ValueExpression: "claims.foo", + }, + } + } + }, + }), + expectError: true, + featureGates: featuregates.NewFeatureGate( + []configv1.FeatureGateName{ + features.FeatureGateExternalOIDCWithAdditionalClaimMappings, + features.FeatureGateExternalOIDCWithUpstreamParity, + }, + []configv1.FeatureGateName{}, + ), + }, + { + name: "auth config with extra missing valueExpression", + caBundleConfigMap: &baseCABundleConfigMap, + auth: *authWithUpdates(baseAuthResource, []func(auth *configv1.Authentication){ + func(auth *configv1.Authentication) { + for i := range auth.Spec.OIDCProviders { + auth.Spec.OIDCProviders[i].ClaimMappings.Extra = []configv1.ExtraMapping{ + { + Key: "foo.example.com/bar", + }, + } + } + }, + }), + expectError: true, + featureGates: featuregates.NewFeatureGate( + []configv1.FeatureGateName{ + features.FeatureGateExternalOIDCWithAdditionalClaimMappings, + features.FeatureGateExternalOIDCWithUpstreamParity, + }, + []configv1.FeatureGateName{}, + ), + }, + { + name: "auth config with valid extra mappings", + caBundleConfigMap: &baseCABundleConfigMap, + auth: *authWithUpdates(baseAuthResource, []func(auth *configv1.Authentication){ + func(auth *configv1.Authentication) { + for i := range auth.Spec.OIDCProviders { + auth.Spec.OIDCProviders[i].ClaimMappings.Extra = []configv1.ExtraMapping{ + { + Key: "foo.example.com/bar", + ValueExpression: "claims.bar", + }, + } + } + }, + }), + expectedAuthConfig: authConfigWithUpdates(baseAuthConfig, []func(authConfig *apiserverv1beta1.AuthenticationConfiguration){ + func(authConfig *apiserverv1beta1.AuthenticationConfiguration) { + for i := range authConfig.JWT { + authConfig.JWT[i].ClaimMappings.UID.Claim = "sub" + authConfig.JWT[i].ClaimMappings.Extra = []apiserverv1beta1.ExtraMapping{ + { + Key: "foo.example.com/bar", + ValueExpression: "claims.bar", + }, + } + } + }, + }), + expectError: false, + featureGates: featuregates.NewFeatureGate( + []configv1.FeatureGateName{ + features.FeatureGateExternalOIDCWithAdditionalClaimMappings, + features.FeatureGateExternalOIDCWithUpstreamParity, + }, + []configv1.FeatureGateName{}, + ), + }, + { + name: "invalid discovery URL (http instead of https)", + auth: *authWithUpdates(baseAuthResource, []func(auth *configv1.Authentication){ + func(auth *configv1.Authentication) { + auth.Spec.OIDCProviders[0].Issuer.DiscoveryURL = "http://insecure-url.com" + }, + }), + caBundleConfigMap: &baseCABundleConfigMap, // ensure CA bundle exists + expectError: true, + featureGates: featuregates.NewFeatureGate( + []configv1.FeatureGateName{features.FeatureGateExternalOIDCWithUpstreamParity}, + []configv1.FeatureGateName{}, + ), + }, + { + name: "invalid discovery URL (identical to issuer URL)", + auth: *authWithUpdates(baseAuthResource, []func(auth *configv1.Authentication){ + func(auth *configv1.Authentication) { + auth.Spec.OIDCProviders[0].Issuer.URL = "https://issuer.example.com" + auth.Spec.OIDCProviders[0].Issuer.DiscoveryURL = auth.Spec.OIDCProviders[0].Issuer.URL + }, + }), + caBundleConfigMap: &baseCABundleConfigMap, + expectError: true, + featureGates: featuregates.NewFeatureGate( + []configv1.FeatureGateName{features.FeatureGateExternalOIDCWithUpstreamParity}, + []configv1.FeatureGateName{}, + ), + }, + { + name: "invalid discovery URL (identical to issuer URL except trailing slash)", + auth: *authWithUpdates(baseAuthResource, []func(auth *configv1.Authentication){ + func(auth *configv1.Authentication) { + auth.Spec.OIDCProviders[0].Issuer.URL = "https://issuer.example.com" + auth.Spec.OIDCProviders[0].Issuer.DiscoveryURL = "https://issuer.example.com/" + }, + }), + caBundleConfigMap: &baseCABundleConfigMap, + expectError: true, + featureGates: featuregates.NewFeatureGate( + []configv1.FeatureGateName{features.FeatureGateExternalOIDCWithUpstreamParity}, + []configv1.FeatureGateName{}, + ), + }, + { + name: "invalid discovery URL (missing host)", + auth: *authWithUpdates(baseAuthResource, []func(auth *configv1.Authentication){ + func(auth *configv1.Authentication) { + auth.Spec.OIDCProviders[0].Issuer.URL = "https://issuer.example.com" + auth.Spec.OIDCProviders[0].Issuer.DiscoveryURL = "https:///path" + }, + }), + caBundleConfigMap: &baseCABundleConfigMap, + expectError: true, + featureGates: featuregates.NewFeatureGate( + []configv1.FeatureGateName{features.FeatureGateExternalOIDCWithUpstreamParity}, + []configv1.FeatureGateName{}, + ), + }, + { + name: "invalid discovery URL (contains user info)", + auth: *authWithUpdates(baseAuthResource, []func(auth *configv1.Authentication){ + func(auth *configv1.Authentication) { + auth.Spec.OIDCProviders[0].Issuer.URL = "https://issuer.example.com" + auth.Spec.OIDCProviders[0].Issuer.DiscoveryURL = "https://user@discovery.example.com/path" + }, + }), + caBundleConfigMap: &baseCABundleConfigMap, + expectError: true, + featureGates: featuregates.NewFeatureGate( + []configv1.FeatureGateName{features.FeatureGateExternalOIDCWithUpstreamParity}, + []configv1.FeatureGateName{}, + ), + }, + { + name: "invalid discovery URL (contains query string)", + auth: *authWithUpdates(baseAuthResource, []func(auth *configv1.Authentication){ + func(auth *configv1.Authentication) { + auth.Spec.OIDCProviders[0].Issuer.URL = "https://issuer.example.com" + auth.Spec.OIDCProviders[0].Issuer.DiscoveryURL = "https://discovery.example.com/path?q=1" + }, + }), + caBundleConfigMap: &baseCABundleConfigMap, + expectError: true, + featureGates: featuregates.NewFeatureGate( + []configv1.FeatureGateName{features.FeatureGateExternalOIDCWithUpstreamParity}, + []configv1.FeatureGateName{}, + ), + }, + { + name: "invalid discovery URL (contains fragment)", + auth: *authWithUpdates(baseAuthResource, []func(auth *configv1.Authentication){ + func(auth *configv1.Authentication) { + auth.Spec.OIDCProviders[0].Issuer.URL = "https://issuer.example.com" + auth.Spec.OIDCProviders[0].Issuer.DiscoveryURL = "https://discovery.example.com/path#fragment" + }, + }), + caBundleConfigMap: &baseCABundleConfigMap, + expectError: true, + featureGates: featuregates.NewFeatureGate( + []configv1.FeatureGateName{features.FeatureGateExternalOIDCWithUpstreamParity}, + []configv1.FeatureGateName{}, + ), + }, + { + name: "invalid discovery URL (parse error)", + auth: *authWithUpdates(baseAuthResource, []func(auth *configv1.Authentication){ + func(auth *configv1.Authentication) { + auth.Spec.OIDCProviders[0].Issuer.URL = "https://issuer.example.com" + auth.Spec.OIDCProviders[0].Issuer.DiscoveryURL = "https://%zz" + }, + }), + caBundleConfigMap: &baseCABundleConfigMap, + expectError: true, + featureGates: featuregates.NewFeatureGate( + []configv1.FeatureGateName{features.FeatureGateExternalOIDCWithUpstreamParity}, + []configv1.FeatureGateName{}, + ), + }, + { + name: "user validation rule invalid expression", + auth: *authWithUpdates(baseAuthResource, []func(auth *configv1.Authentication){ + func(auth *configv1.Authentication) { + auth.Spec.OIDCProviders[0].UserValidationRules = []configv1.TokenUserValidationRule{ + { + Expression: "", // invalid: empty expression + Message: "must have a valid expression", + }, + } + }, + }), + expectError: true, + featureGates: featuregates.NewFeatureGate( + []configv1.FeatureGateName{features.FeatureGateExternalOIDCWithUpstreamParity}, + []configv1.FeatureGateName{}, + ), + }, + { + name: "auth config with invalid username expression, error", + caBundleConfigMap: &baseCABundleConfigMap, + auth: *authWithUpdates(baseAuthResource, []func(auth *configv1.Authentication){ + func(auth *configv1.Authentication) { + for i := range auth.Spec.OIDCProviders { + auth.Spec.OIDCProviders[i].ClaimMappings.Username = configv1.UsernameClaimMapping{ + Expression: "#@!$&*(^)", + } + } + }, + }), + expectError: true, + featureGates: featuregates.NewFeatureGate( + []configv1.FeatureGateName{ + features.FeatureGateExternalOIDCWithAdditionalClaimMappings, + features.FeatureGateExternalOIDCWithUpstreamParity, + }, + []configv1.FeatureGateName{}, + ), + }, + { + name: "auth config with invalid groups expression, error", + caBundleConfigMap: &baseCABundleConfigMap, + auth: *authWithUpdates(baseAuthResource, []func(auth *configv1.Authentication){ + func(auth *configv1.Authentication) { + for i := range auth.Spec.OIDCProviders { + auth.Spec.OIDCProviders[i].ClaimMappings.Groups = configv1.PrefixedClaimMapping{ + TokenClaimMapping: configv1.TokenClaimMapping{ + Expression: "#@!$&*(^)", + }, + } + } + }, + }), + expectError: true, + featureGates: featuregates.NewFeatureGate( + []configv1.FeatureGateName{ + features.FeatureGateExternalOIDCWithAdditionalClaimMappings, + features.FeatureGateExternalOIDCWithUpstreamParity, + }, + []configv1.FeatureGateName{}, + ), + }, + { + name: "auth config with username expression mapping", + caBundleConfigMap: &baseCABundleConfigMap, + auth: *authWithUpdates(baseAuthResource, []func(auth *configv1.Authentication){ + func(auth *configv1.Authentication) { + for i := range auth.Spec.OIDCProviders { + auth.Spec.OIDCProviders[i].ClaimMappings.Username = configv1.UsernameClaimMapping{ + Expression: "claims.sub", + } + } + }, + }), + expectedAuthConfig: authConfigWithUpdates(baseAuthConfig, []func(authConfig *apiserverv1beta1.AuthenticationConfiguration){ + func(authConfig *apiserverv1beta1.AuthenticationConfiguration) { + for i := range authConfig.JWT { + authConfig.JWT[i].ClaimMappings.Username = apiserverv1beta1.PrefixedClaimOrExpression{ + Expression: "claims.sub", + } + authConfig.JWT[i].ClaimMappings.UID = apiserverv1beta1.ClaimOrExpression{ + Claim: "sub", + } + } + }, + }), + expectError: false, + featureGates: featuregates.NewFeatureGate( + []configv1.FeatureGateName{ + features.FeatureGateExternalOIDCWithAdditionalClaimMappings, + features.FeatureGateExternalOIDCWithUpstreamParity, + }, + []configv1.FeatureGateName{}, + ), + }, + { + name: "auth config with groups expression mapping", + caBundleConfigMap: &baseCABundleConfigMap, + auth: *authWithUpdates(baseAuthResource, []func(auth *configv1.Authentication){ + func(auth *configv1.Authentication) { + for i := range auth.Spec.OIDCProviders { + auth.Spec.OIDCProviders[i].ClaimMappings.Groups = configv1.PrefixedClaimMapping{ + TokenClaimMapping: configv1.TokenClaimMapping{ + Expression: "claims.groups", + }, + } + } + }, + }), + expectedAuthConfig: authConfigWithUpdates(baseAuthConfig, []func(authConfig *apiserverv1beta1.AuthenticationConfiguration){ + func(authConfig *apiserverv1beta1.AuthenticationConfiguration) { + for i := range authConfig.JWT { + authConfig.JWT[i].ClaimMappings.Groups = apiserverv1beta1.PrefixedClaimOrExpression{ + Expression: "claims.groups", + } + authConfig.JWT[i].ClaimMappings.UID = apiserverv1beta1.ClaimOrExpression{ + Claim: "sub", + } + } + }, + }), + expectError: false, + featureGates: featuregates.NewFeatureGate( + []configv1.FeatureGateName{ + features.FeatureGateExternalOIDCWithAdditionalClaimMappings, + features.FeatureGateExternalOIDCWithUpstreamParity, + }, + []configv1.FeatureGateName{}, + ), + }, + { + name: "auth config with username claim and expression both set, error", + caBundleConfigMap: &baseCABundleConfigMap, + auth: *authWithUpdates(baseAuthResource, []func(auth *configv1.Authentication){ + func(auth *configv1.Authentication) { + for i := range auth.Spec.OIDCProviders { + auth.Spec.OIDCProviders[i].ClaimMappings.Username = configv1.UsernameClaimMapping{ + Claim: "username", + Expression: "claims.email", + } + } + }, + }), + expectError: true, + featureGates: featuregates.NewFeatureGate( + []configv1.FeatureGateName{ + features.FeatureGateExternalOIDCWithAdditionalClaimMappings, + features.FeatureGateExternalOIDCWithUpstreamParity, + }, + []configv1.FeatureGateName{}, + ), + }, + { + name: "auth config with groups claim and expression both set, error", + caBundleConfigMap: &baseCABundleConfigMap, + auth: *authWithUpdates(baseAuthResource, []func(auth *configv1.Authentication){ + func(auth *configv1.Authentication) { + for i := range auth.Spec.OIDCProviders { + auth.Spec.OIDCProviders[i].ClaimMappings.Groups = configv1.PrefixedClaimMapping{ + TokenClaimMapping: configv1.TokenClaimMapping{ + Claim: "groups", + Expression: "claims.groups", + }, + } + } + }, + }), + expectError: true, + featureGates: featuregates.NewFeatureGate( + []configv1.FeatureGateName{ + features.FeatureGateExternalOIDCWithAdditionalClaimMappings, + features.FeatureGateExternalOIDCWithUpstreamParity, + }, + []configv1.FeatureGateName{}, + ), + }, + { + name: "auth config with username expression and prefix set, error", + caBundleConfigMap: &baseCABundleConfigMap, + auth: *authWithUpdates(baseAuthResource, []func(auth *configv1.Authentication){ + func(auth *configv1.Authentication) { + for i := range auth.Spec.OIDCProviders { + auth.Spec.OIDCProviders[i].ClaimMappings.Username = configv1.UsernameClaimMapping{ + Expression: "claims.email", + PrefixPolicy: configv1.Prefix, + Prefix: &configv1.UsernamePrefix{ + PrefixString: "oidc-user:", + }, + } + } + }, + }), + expectError: true, + featureGates: featuregates.NewFeatureGate( + []configv1.FeatureGateName{ + features.FeatureGateExternalOIDCWithAdditionalClaimMappings, + features.FeatureGateExternalOIDCWithUpstreamParity, + }, + []configv1.FeatureGateName{}, + ), + }, + { + name: "auth config with groups expression and prefix set, error", + caBundleConfigMap: &baseCABundleConfigMap, + auth: *authWithUpdates(baseAuthResource, []func(auth *configv1.Authentication){ + func(auth *configv1.Authentication) { + for i := range auth.Spec.OIDCProviders { + auth.Spec.OIDCProviders[i].ClaimMappings.Groups = configv1.PrefixedClaimMapping{ + TokenClaimMapping: configv1.TokenClaimMapping{ + Expression: "claims.groups", + }, + Prefix: "oidc-group:", + } + } + }, + }), + expectError: true, + featureGates: featuregates.NewFeatureGate( + []configv1.FeatureGateName{ + features.FeatureGateExternalOIDCWithAdditionalClaimMappings, + features.FeatureGateExternalOIDCWithUpstreamParity, + }, + []configv1.FeatureGateName{}, + ), + }, + { + name: "auth config with username expression using claims.email without claims.email_verified, error", + caBundleConfigMap: &baseCABundleConfigMap, + auth: *authWithUpdates(baseAuthResource, []func(auth *configv1.Authentication){ + func(auth *configv1.Authentication) { + for i := range auth.Spec.OIDCProviders { + auth.Spec.OIDCProviders[i].ClaimMappings.Username = configv1.UsernameClaimMapping{ + Expression: "claims.email", + } + } + }, + }), + expectError: true, + featureGates: featuregates.NewFeatureGate( + []configv1.FeatureGateName{ + features.FeatureGateExternalOIDCWithAdditionalClaimMappings, + features.FeatureGateExternalOIDCWithUpstreamParity, + }, + []configv1.FeatureGateName{}, + ), + }, + { + name: "auth config with username expression using claims.email with claims.email_verified in claimValidationRule, success", + caBundleConfigMap: &baseCABundleConfigMap, + auth: *authWithUpdates(baseAuthResource, []func(auth *configv1.Authentication){ + func(auth *configv1.Authentication) { + for i := range auth.Spec.OIDCProviders { + auth.Spec.OIDCProviders[i].ClaimMappings.Username = configv1.UsernameClaimMapping{ + Expression: "claims.email", + } + auth.Spec.OIDCProviders[i].ClaimValidationRules = []configv1.TokenClaimValidationRule{ + { + Type: configv1.TokenValidationRuleTypeCEL, + CEL: configv1.TokenClaimValidationCELRule{ + Expression: "claims.email_verified == true", + Message: "email must be verified", + }, + }, + } + } + }, + }), + expectedAuthConfig: authConfigWithUpdates(baseAuthConfig, []func(authConfig *apiserverv1beta1.AuthenticationConfiguration){ + func(authConfig *apiserverv1beta1.AuthenticationConfiguration) { + for i := range authConfig.JWT { + authConfig.JWT[i].ClaimMappings.Username = apiserverv1beta1.PrefixedClaimOrExpression{ + Expression: "claims.email", + } + authConfig.JWT[i].ClaimMappings.UID = apiserverv1beta1.ClaimOrExpression{ + Claim: "sub", + } + authConfig.JWT[i].ClaimValidationRules = []apiserverv1beta1.ClaimValidationRule{ + { + Expression: "claims.email_verified == true", + Message: "email must be verified", + }, + } + } + }, + }), + expectError: false, + featureGates: featuregates.NewFeatureGate( + []configv1.FeatureGateName{ + features.FeatureGateExternalOIDCWithAdditionalClaimMappings, + features.FeatureGateExternalOIDCWithUpstreamParity, + }, + []configv1.FeatureGateName{}, + ), + }, + { + name: "auth config with username expression using both claims.email and claims.email_verified, success", + caBundleConfigMap: &baseCABundleConfigMap, + auth: *authWithUpdates(baseAuthResource, []func(auth *configv1.Authentication){ + func(auth *configv1.Authentication) { + for i := range auth.Spec.OIDCProviders { + auth.Spec.OIDCProviders[i].ClaimMappings.Username = configv1.UsernameClaimMapping{ + Expression: "claims.email_verified ? claims.email : 'unverified'", + } + } + }, + }), + expectedAuthConfig: authConfigWithUpdates(baseAuthConfig, []func(authConfig *apiserverv1beta1.AuthenticationConfiguration){ + func(authConfig *apiserverv1beta1.AuthenticationConfiguration) { + for i := range authConfig.JWT { + authConfig.JWT[i].ClaimMappings.Username = apiserverv1beta1.PrefixedClaimOrExpression{ + Expression: "claims.email_verified ? claims.email : 'unverified'", + } + authConfig.JWT[i].ClaimMappings.UID = apiserverv1beta1.ClaimOrExpression{ + Claim: "sub", + } + } + }, + }), + expectError: false, + featureGates: featuregates.NewFeatureGate( + []configv1.FeatureGateName{ + features.FeatureGateExternalOIDCWithAdditionalClaimMappings, + features.FeatureGateExternalOIDCWithUpstreamParity, + }, + []configv1.FeatureGateName{}, + ), + }, + { + name: "auth config with username expression using claims.email with claims.email_verified in extra, success", + caBundleConfigMap: &baseCABundleConfigMap, + auth: *authWithUpdates(baseAuthResource, []func(auth *configv1.Authentication){ + func(auth *configv1.Authentication) { + for i := range auth.Spec.OIDCProviders { + auth.Spec.OIDCProviders[i].ClaimMappings.Username = configv1.UsernameClaimMapping{ + Expression: "claims.email", + } + auth.Spec.OIDCProviders[i].ClaimMappings.Extra = []configv1.ExtraMapping{ + { + Key: "example.com/email-verified", + ValueExpression: "claims.email_verified ? 'true' : 'false'", + }, + } + auth.Spec.OIDCProviders[i].ClaimValidationRules = []configv1.TokenClaimValidationRule{} + } + }, + }), + expectedAuthConfig: authConfigWithUpdates(baseAuthConfig, []func(authConfig *apiserverv1beta1.AuthenticationConfiguration){ + func(authConfig *apiserverv1beta1.AuthenticationConfiguration) { + for i := range authConfig.JWT { + authConfig.JWT[i].ClaimMappings.Username = apiserverv1beta1.PrefixedClaimOrExpression{ + Expression: "claims.email", + } + authConfig.JWT[i].ClaimMappings.UID = apiserverv1beta1.ClaimOrExpression{ + Claim: "sub", + } + authConfig.JWT[i].ClaimMappings.Extra = []apiserverv1beta1.ExtraMapping{ + { + Key: "example.com/email-verified", + ValueExpression: "claims.email_verified ? 'true' : 'false'", + }, + } + authConfig.JWT[i].ClaimValidationRules = []apiserverv1beta1.ClaimValidationRule{} + } + }, + }), + expectError: false, + featureGates: featuregates.NewFeatureGate( + []configv1.FeatureGateName{ + features.FeatureGateExternalOIDCWithAdditionalClaimMappings, + features.FeatureGateExternalOIDCWithUpstreamParity, + }, + []configv1.FeatureGateName{}, + ), + }, + } { + t.Run(tt.name, func(t *testing.T) { + if tt.configMapIndexer == nil { + tt.configMapIndexer = cache.NewIndexer(cache.MetaNamespaceKeyFunc, cache.Indexers{}) + } + + if tt.caBundleConfigMap != nil { + tt.configMapIndexer.Add(tt.caBundleConfigMap) + } + + c := NewAuthenticationConfigurationGenerator(corev1listers.NewConfigMapLister(tt.configMapIndexer), tt.featureGates) + c.validationFn = tt.configValidator + + gotConfig, err := c.GenerateAuthenticationConfiguration(&tt.auth) + if tt.expectError && err == nil { + t.Fatalf("expected error but didn't get any") + } + + if !tt.expectError && err != nil { + t.Fatalf("did not expect any error but got: %v", err) + } + + if gotConfig == nil && tt.expectedAuthConfig == nil { + return + } + + if diff := cmp.Diff(tt.expectedAuthConfig, gotConfig, cmpopts.EquateEmpty()); diff != "" { + t.Fatalf("unexpected config diff: %s", diff) + } + }) + } +} + +func newAuthWithSpec(spec configv1.AuthenticationSpec) *configv1.Authentication { + return &configv1.Authentication{ + ObjectMeta: metav1.ObjectMeta{ + Name: "cluster", + }, + Spec: spec, + } +} + +func authWithUpdates(auth configv1.Authentication, updateFuncs []func(auth *configv1.Authentication)) *configv1.Authentication { + copy := auth.DeepCopy() + for _, updateFunc := range updateFuncs { + updateFunc(copy) + } + return copy +} + +func authConfigWithUpdates(authConfig apiserverv1beta1.AuthenticationConfiguration, updateFuncs []func(authConfig *apiserverv1beta1.AuthenticationConfiguration)) *apiserverv1beta1.AuthenticationConfiguration { + copy := authConfig.DeepCopy() + for _, updateFunc := range updateFuncs { + updateFunc(copy) + } + return copy +} + +type everFailingIndexer struct{} + +// Index always returns an error +func (i *everFailingIndexer) Index(indexName string, obj interface{}) ([]interface{}, error) { + return nil, fmt.Errorf("Index method not implemented") +} + +// IndexKeys always returns an error +func (i *everFailingIndexer) IndexKeys(indexName, indexedValue string) ([]string, error) { + return nil, fmt.Errorf("IndexKeys method not implemented") +} + +// ListIndexFuncValues always returns an error +func (i *everFailingIndexer) ListIndexFuncValues(indexName string) []string { + return nil +} + +// ByIndex always returns an error +func (i *everFailingIndexer) ByIndex(indexName, indexedValue string) ([]interface{}, error) { + return nil, fmt.Errorf("ByIndex method not implemented") +} + +// GetIndexers always returns an error +func (i *everFailingIndexer) GetIndexers() cache.Indexers { + return nil +} + +// AddIndexers always returns an error +func (i *everFailingIndexer) AddIndexers(newIndexers cache.Indexers) error { + return fmt.Errorf("AddIndexers method not implemented") +} + +// Add always returns an error +func (s *everFailingIndexer) Add(obj interface{}) error { + return fmt.Errorf("Add method not implemented") +} + +// Update always returns an error +func (s *everFailingIndexer) Update(obj interface{}) error { + return fmt.Errorf("Update method not implemented") +} + +// Delete always returns an error +func (s *everFailingIndexer) Delete(obj interface{}) error { + return fmt.Errorf("Delete method not implemented") +} + +// List always returns nil +func (s *everFailingIndexer) List() []interface{} { + return nil +} + +// ListKeys always returns nil +func (s *everFailingIndexer) ListKeys() []string { + return nil +} + +// Get always returns an error +func (s *everFailingIndexer) Get(obj interface{}) (item interface{}, exists bool, err error) { + return nil, false, fmt.Errorf("Get method not implemented") +} + +// GetByKey always returns an error +func (s *everFailingIndexer) GetByKey(key string) (item interface{}, exists bool, err error) { + return nil, false, fmt.Errorf("GetByKey method not implemented") +} + +// Replace always returns an error +func (s *everFailingIndexer) Replace(objects []interface{}, sKey string) error { + return fmt.Errorf("Replace method not implemented") +} + +// Resync always returns an error +func (s *everFailingIndexer) Resync() error { + return fmt.Errorf("Resync method not implemented") +} + +var ( + baseCACert, baseCAPrivateKey, validateTestCertData = func() (*x509.Certificate, crypto.Signer, string) { + cert, key, err := generateCAKeyPair() + if err != nil { + panic(err) + } + return cert, key, string(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw})) + }() + + validateBaseAuthConfig = apiserverv1beta1.AuthenticationConfiguration{ + TypeMeta: metav1.TypeMeta{ + Kind: kindAuthenticationConfiguration, + APIVersion: apiserverv1beta1.ConfigSchemeGroupVersion.String(), + }, + JWT: []apiserverv1beta1.JWTAuthenticator{ + { + Issuer: apiserverv1beta1.Issuer{ + Audiences: []string{"my-test-aud", "another-aud"}, + CertificateAuthority: validateTestCertData, + AudienceMatchPolicy: apiserverv1beta1.AudienceMatchPolicyMatchAny, + }, + ClaimMappings: apiserverv1beta1.ClaimMappings{ + Username: apiserverv1beta1.PrefixedClaimOrExpression{ + Claim: "username", + Prefix: ptr.To("oidc-user:"), + }, + Groups: apiserverv1beta1.PrefixedClaimOrExpression{ + Claim: "groups", + Prefix: ptr.To("oidc-group:"), + }, + }, + ClaimValidationRules: []apiserverv1beta1.ClaimValidationRule{ + { + Claim: "username", + RequiredValue: "test-username", + }, + { + Claim: "email", + RequiredValue: "test-email", + }, + }, + }, + }, + } +) + +func TestValidateApiserverAuthenticationConfiguration(t *testing.T) { + testServer, err := createTestServer(baseCACert, baseCAPrivateKey, nil) + if err != nil { + t.Fatalf("could not create test server: %v", err) + } + defer testServer.Close() + testServer.StartTLS() + + for _, tt := range []struct { + name string + authConfig *apiserverv1beta1.AuthenticationConfiguration + expectError bool + }{ + { + name: "empty config", + authConfig: &apiserverv1beta1.AuthenticationConfiguration{}, + expectError: false, + }, + { + name: "nil config", + authConfig: nil, + expectError: false, + }, + { + name: "issuer with empty URL", + authConfig: authConfigWithUpdates(validateBaseAuthConfig, []func(authConfig *apiserverv1beta1.AuthenticationConfiguration){ + func(authConfig *apiserverv1beta1.AuthenticationConfiguration) { + authConfig.JWT[0].Issuer.URL = "" + }, + }), + expectError: true, + }, + { + name: "issuer with http URL", + authConfig: authConfigWithUpdates(validateBaseAuthConfig, []func(authConfig *apiserverv1beta1.AuthenticationConfiguration){ + func(authConfig *apiserverv1beta1.AuthenticationConfiguration) { + authConfig.JWT[0].Issuer.URL = "http://insecure.com" + }, + }), + expectError: true, + }, + { + name: "issuer with invalid CA", + authConfig: authConfigWithUpdates(validateBaseAuthConfig, []func(authConfig *apiserverv1beta1.AuthenticationConfiguration){ + func(authConfig *apiserverv1beta1.AuthenticationConfiguration) { + authConfig.JWT[0].Issuer.CertificateAuthority = "invalid CA" + }, + }), + expectError: true, + }, + { + name: "valid auth config", + authConfig: authConfigWithUpdates(validateBaseAuthConfig, []func(authConfig *apiserverv1beta1.AuthenticationConfiguration){ + func(authConfig *apiserverv1beta1.AuthenticationConfiguration) { + authConfig.JWT[0].Issuer.URL = testServer.URL + }, + }), + expectError: false, + }, + } { + t.Run(tt.name, func(t *testing.T) { + err := validateApiserverAuthenticationConfiguration(tt.authConfig) + if tt.expectError && err == nil { + t.Errorf("expected error but didn't get any") + } + + if !tt.expectError && err != nil { + t.Errorf("did not expect any error but got: %v", err) + } + }) + } +} + +func createTestServer(caCert *x509.Certificate, caPrivateKey crypto.Signer, handlerFunc http.HandlerFunc) (*httptest.Server, error) { + cert := caCert + key := caPrivateKey + var err error + if caCert == nil { + cert, key, err = generateCAKeyPair() + if err != nil { + return nil, err + } + } + + servingCertPair, err := generateServingCert(cert, key) + if err != nil { + return nil, err + } + + if handlerFunc == nil { + handlerFunc = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + }) + } + + testServer := httptest.NewUnstartedServer(handlerFunc) + testServer.TLS = &tls.Config{ + Certificates: []tls.Certificate{*servingCertPair}, + } + + return testServer, nil +} + +func generateCAKeyPair() (*x509.Certificate, crypto.Signer, error) { + caPrivateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + return nil, nil, err + } + + caCert, err := certutil.NewSelfSignedCACert(certutil.Config{CommonName: "test-ca"}, caPrivateKey) + if err != nil { + return nil, nil, err + } + + return caCert, caPrivateKey, err +} + +func generateServingCert(caCert *x509.Certificate, caPrivateKey crypto.Signer) (*tls.Certificate, error) { + cert := &x509.Certificate{ + SerialNumber: big.NewInt(2024), + Subject: pkix.Name{ + Organization: []string{"Company, INC."}, + Country: []string{"US"}, + Province: []string{""}, + Locality: []string{"Springfield"}, + StreetAddress: []string{"742 Evergreen Terrace"}, + }, + IPAddresses: []net.IP{net.IPv4(127, 0, 0, 1), net.IPv6loopback}, + NotBefore: time.Now(), + NotAfter: time.Now().AddDate(10, 0, 0), + SubjectKeyId: []byte{1, 2, 3, 4, 6}, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth}, + KeyUsage: x509.KeyUsageDigitalSignature, + } + + certPrivKey, err := rsa.GenerateKey(rand.Reader, 4096) + if err != nil { + return nil, err + } + + certBytes, err := x509.CreateCertificate(rand.Reader, cert, caCert, &certPrivKey.PublicKey, caPrivateKey) + if err != nil { + return nil, err + } + + certPEM := new(bytes.Buffer) + err = pem.Encode(certPEM, &pem.Block{ + Type: "CERTIFICATE", + Bytes: certBytes, + }) + if err != nil { + return nil, fmt.Errorf("PEM encoding certificate: %w", err) + } + + certPrivKeyPEM := new(bytes.Buffer) + err = pem.Encode(certPrivKeyPEM, &pem.Block{ + Type: "RSA PRIVATE KEY", + Bytes: x509.MarshalPKCS1PrivateKey(certPrivKey), + }) + if err != nil { + return nil, fmt.Errorf("PEM encoding private key: %w", err) + } + + serverCert, err := tls.X509KeyPair(certPEM.Bytes(), certPrivKeyPEM.Bytes()) + if err != nil { + return nil, err + } + + return &serverCert, nil +} diff --git a/pkg/controllers/externaloidc/generation/oauthapiserver/generate.go b/pkg/controllers/externaloidc/generation/oauthapiserver/generate.go new file mode 100644 index 0000000000..00b1a2d2d1 --- /dev/null +++ b/pkg/controllers/externaloidc/generation/oauthapiserver/generate.go @@ -0,0 +1,1032 @@ +package oauthapiserver + +import ( + "context" + "crypto/tls" + "crypto/x509" + "errors" + "fmt" + "io" + "net/http" + "net/url" + "regexp" + "strings" + "time" + + configv1 "github.com/openshift/api/config/v1" + "github.com/openshift/api/features" + "github.com/openshift/library-go/pkg/operator/configobserver/featuregates" + "github.com/openshift/library-go/pkg/operator/resource/retry" + authenticationv1alpha1 "github.com/openshift/oauth-apiserver/pkg/externaloidc/apis/authentication/v1alpha1" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/sets" + authenticationcel "k8s.io/apiserver/pkg/authentication/cel" + corev1listers "k8s.io/client-go/listers/core/v1" + "k8s.io/client-go/util/cert" + "k8s.io/utils/ptr" + + celgo "github.com/google/cel-go/cel" + "github.com/google/cel-go/common/operators" + exprpb "google.golang.org/genproto/googleapis/api/expr/v1alpha1" +) + +// oidcGenerationState holds compilation results gathered during JWT generation +// that are needed for cross-field validation (e.g. email_verified enforcement). +type oidcGenerationState struct { + UsernameResult *authenticationcel.CompilationResult + ExtraResults []authenticationcel.CompilationResult + ClaimValidationResults []authenticationcel.CompilationResult +} + +const ( + configNamespace = "openshift-config" + kindAuthenticationConfiguration = "AuthenticationConfiguration" + oidcDiscoveryEndpointPath = "/.well-known/openid-configuration" +) + +type validationFunc func(*authenticationv1alpha1.AuthenticationConfiguration) error + +type AuthenticationConfigurationGenerator struct { + configMapLister corev1listers.ConfigMapLister + secretLister corev1listers.SecretLister + featureGates featuregates.FeatureGate + validationFn validationFunc +} + +func NewAuthenticationConfigurationGenerator(cmlister corev1listers.ConfigMapLister, secretLister corev1listers.SecretLister, gates featuregates.FeatureGate) *AuthenticationConfigurationGenerator { + return &AuthenticationConfigurationGenerator{ + configMapLister: cmlister, + secretLister: secretLister, + featureGates: gates, + validationFn: validateOAuthApiserverAuthenticationConfiguration, + } +} + +// GenerateAuthenticationConfiguration creates a structured JWT AuthenticationConfiguration for OIDC +// in the oauth-apiserver from the configuration found in the authentication/cluster resource. +func (acg *AuthenticationConfigurationGenerator) GenerateAuthenticationConfiguration(auth *configv1.Authentication) (runtime.Object, error) { + authConfig := &authenticationv1alpha1.AuthenticationConfiguration{ + TypeMeta: metav1.TypeMeta{ + Kind: kindAuthenticationConfiguration, + APIVersion: authenticationv1alpha1.SchemeGroupVersion.String(), + }, + } + + errs := []error{} + for _, provider := range auth.Spec.OIDCProviders { + jwt, err := generateJWTForProvider(provider, acg.configMapLister, acg.secretLister, acg.featureGates, auth.Spec.ServiceAccountIssuer) + if err != nil { + errs = append(errs, err) + continue + } + + authConfig.JWT = append(authConfig.JWT, jwt) + } + + if len(errs) > 0 { + return nil, errors.Join(errs...) + } + + if acg.validationFn != nil { + if err := acg.validationFn(authConfig); err != nil { + return nil, err + } + } + + return authConfig, nil +} + +func generateJWTForProvider(provider configv1.OIDCProvider, configMapLister corev1listers.ConfigMapLister, secretLister corev1listers.SecretLister, featureGates featuregates.FeatureGate, serviceAccountIssuer string) (authenticationv1alpha1.JWTAuthenticator, error) { + out := authenticationv1alpha1.JWTAuthenticator{} + + issuer, err := generateIssuer(provider.Issuer, configMapLister, serviceAccountIssuer) + if err != nil { + return authenticationv1alpha1.JWTAuthenticator{}, fmt.Errorf("generating issuer for provider %q: %v", provider.Name, err) + } + + state := &oidcGenerationState{} + + claimMappings, err := generateClaimMappings(provider.ClaimMappings, issuer.URL, featureGates, state) + if err != nil { + return authenticationv1alpha1.JWTAuthenticator{}, fmt.Errorf("generating claimMappings for provider %q: %v", provider.Name, err) + } + + claimValidationRules, err := generateClaimValidationRules(state, provider.ClaimValidationRules...) + if err != nil { + return authenticationv1alpha1.JWTAuthenticator{}, fmt.Errorf("generating claimValidationRules for provider %q: %v", provider.Name, err) + } + + if featureGates.Enabled(features.FeatureGateExternalOIDCWithUpstreamParity) { + if err := validateEmailVerifiedUsage(state); err != nil { + return authenticationv1alpha1.JWTAuthenticator{}, fmt.Errorf("validating email claim usage for provider %q: %v", provider.Name, err) + } + var userValidationRules []authenticationv1alpha1.UserValidationRule + userValidationRules, err = generateUserValidationRules(provider.UserValidationRules) + if err != nil { + return authenticationv1alpha1.JWTAuthenticator{}, fmt.Errorf("generating userValidationRules for provider %q: %v", provider.Name, err) + } + out.UserValidationRules = userValidationRules + } + + if featureGates.Enabled(features.FeatureGateExternalOIDCExternalClaimsSourcing) { + externalClaimsSources, err := generateExternalClaimsSources(configMapLister, secretLister, provider.ExternalClaimsSources...) + if err != nil { + return authenticationv1alpha1.JWTAuthenticator{}, fmt.Errorf("generating externalClaimsSources for provider %q: %v", provider.Name, err) + } + + out.ExternalClaimsSources = externalClaimsSources + } + + out.Issuer = &issuer + out.ClaimMappings = &claimMappings + out.ClaimValidationRules = claimValidationRules + + return out, nil +} + +func generateIssuer(issuer configv1.TokenIssuer, configMapLister corev1listers.ConfigMapLister, serviceAccountIssuer string) (authenticationv1alpha1.Issuer, error) { + out := authenticationv1alpha1.Issuer{} + + if len(serviceAccountIssuer) > 0 { + if issuer.URL == serviceAccountIssuer { + return authenticationv1alpha1.Issuer{}, errors.New("issuer url cannot overlap with the ServiceAccount issuer url") + } + } + + out.URL = issuer.URL + out.AudienceMatchPolicy = authenticationv1alpha1.AudienceMatchPolicyMatchAny + + for _, audience := range issuer.Audiences { + out.Audiences = append(out.Audiences, string(audience)) + } + if len(issuer.DiscoveryURL) > 0 { + // Validate the URL scheme + u, err := url.Parse(issuer.DiscoveryURL) + if err != nil { + return authenticationv1alpha1.Issuer{}, fmt.Errorf("invalid discovery URL: %v", err) + } + if strings.TrimRight(issuer.DiscoveryURL, "/") == strings.TrimRight(issuer.URL, "/") { + return authenticationv1alpha1.Issuer{}, fmt.Errorf("discovery URL must not be identical to issuer URL") + } + if u.Scheme != "https" { + return authenticationv1alpha1.Issuer{}, fmt.Errorf("discovery URL must use https, got %q", u.Scheme) + } + if u.Host == "" { + return authenticationv1alpha1.Issuer{}, fmt.Errorf("discovery URL must include a host") + } + if u.User != nil { + return authenticationv1alpha1.Issuer{}, fmt.Errorf("discovery URL must not contain user info") + } + if len(u.RawQuery) > 0 { + return authenticationv1alpha1.Issuer{}, fmt.Errorf("discovery URL must not contain a query string") + } + if len(u.Fragment) > 0 { + return authenticationv1alpha1.Issuer{}, fmt.Errorf("discovery URL must not contain a fragment") + } + out.DiscoveryURL = issuer.DiscoveryURL + } + if len(issuer.CertificateAuthority.Name) > 0 { + ca, err := getCertificateAuthorityFromConfigMap(issuer.CertificateAuthority.Name, configMapLister) + if err != nil { + return authenticationv1alpha1.Issuer{}, fmt.Errorf("getting CertificateAuthority for issuer: %v", err) + } + out.CertificateAuthority = ca + } + + return out, nil +} + +func getCertificateAuthorityFromConfigMap(name string, configMapLister corev1listers.ConfigMapLister) (string, error) { + caConfigMap, err := configMapLister.ConfigMaps(configNamespace).Get(name) + if err != nil { + return "", fmt.Errorf("could not retrieve auth configmap %s/%s to check CA bundle: %v", configNamespace, name, err) + } + + caData, ok := caConfigMap.Data["ca-bundle.crt"] + if !ok || len(caData) == 0 { + return "", fmt.Errorf("configmap %s/%s key \"ca-bundle.crt\" missing or empty", configNamespace, name) + } + + return caData, nil +} + +func generateClaimMappings(claimMappings configv1.TokenClaimMappings, issuerURL string, featureGates featuregates.FeatureGate, state *oidcGenerationState) (authenticationv1alpha1.ClaimMappings, error) { + out := authenticationv1alpha1.ClaimMappings{} + + username, usernameResult, err := generateUsernameClaimMapping(claimMappings.Username, issuerURL, featureGates) + if err != nil { + return authenticationv1alpha1.ClaimMappings{}, fmt.Errorf("generating username claim mapping: %v", err) + } + state.UsernameResult = usernameResult + + groups, err := generateGroupsClaimMapping(claimMappings.Groups, featureGates) + if err != nil { + return authenticationv1alpha1.ClaimMappings{}, fmt.Errorf("generating group claim mapping: %v", err) + } + out.Username = username + out.Groups = groups + + if featureGates.Enabled(features.FeatureGateExternalOIDCWithAdditionalClaimMappings) { + uid, err := generateUIDClaimMapping(claimMappings.UID) + if err != nil { + return authenticationv1alpha1.ClaimMappings{}, fmt.Errorf("generating uid claim mapping: %v", err) + } + + extras, extraResults, err := generateExtraClaimMapping(claimMappings.Extra...) + if err != nil { + return authenticationv1alpha1.ClaimMappings{}, fmt.Errorf("generating extra claim mapping: %v", err) + } + + out.UID = uid + out.Extra = extras + state.ExtraResults = extraResults + } + + return out, nil +} + +func generateUsernameClaimMapping(usernameClaimMapping configv1.UsernameClaimMapping, issuerURL string, featureGates featuregates.FeatureGate) (authenticationv1alpha1.PrefixedClaimOrExpression, *authenticationcel.CompilationResult, error) { + if featureGates.Enabled(features.FeatureGateExternalOIDCWithUpstreamParity) { + return generateUsernameClaimMappingWithParity(usernameClaimMapping, issuerURL) + } + return generateUsernameClaimMappingLegacy(usernameClaimMapping, issuerURL) +} + +func generateUsernameClaimMappingWithParity(usernameClaimMapping configv1.UsernameClaimMapping, issuerURL string) (authenticationv1alpha1.PrefixedClaimOrExpression, *authenticationcel.CompilationResult, error) { + out := authenticationv1alpha1.PrefixedClaimOrExpression{} + + if len(usernameClaimMapping.Expression) == 0 && len(usernameClaimMapping.Claim) == 0 { + return out, nil, fmt.Errorf("username claim mapping is required and either claim or expression must be set") + } + + if len(usernameClaimMapping.Expression) > 0 && len(usernameClaimMapping.Claim) > 0 { + return out, nil, fmt.Errorf("username claim mapping must not set both claim and expression") + } + + if len(usernameClaimMapping.Expression) > 0 && usernameClaimMapping.PrefixPolicy == configv1.Prefix { + return out, nil, fmt.Errorf("username claim mappings cannot have a prefix set when using an expression based mapping. If you want to set a prefix while using an expression mapping, set the prefix in the expression") + } + + if len(usernameClaimMapping.Expression) > 0 { + result, err := validateClaimsCELExpression(&authenticationcel.ClaimMappingExpression{ + Expression: usernameClaimMapping.Expression, + }) + if err != nil { + return out, nil, fmt.Errorf("invalid CEL expression: %v", err) + } + out.Expression = usernameClaimMapping.Expression + return out, &result, nil + } + + if len(usernameClaimMapping.Claim) > 0 { + out.Claim = usernameClaimMapping.Claim + + // prefix can only be set when using a direct claim name, so only attempt to set it + // if we are certain we are using a direct claim reference and not an expression + switch usernameClaimMapping.PrefixPolicy { + case configv1.Prefix: + if usernameClaimMapping.Prefix == nil { + return out, nil, fmt.Errorf("nil username prefix while policy expects one") + } + out.Prefix = &usernameClaimMapping.Prefix.PrefixString + case configv1.NoPrefix: + out.Prefix = ptr.To("") + case configv1.NoOpinion: + prefix := "" + if usernameClaimMapping.Claim != "email" { + prefix = issuerURL + "#" + } + out.Prefix = &prefix + default: + return out, nil, fmt.Errorf("invalid username prefix policy: %s", usernameClaimMapping.PrefixPolicy) + } + } + + return out, nil, nil +} + +func generateUsernameClaimMappingLegacy(usernameClaimMapping configv1.UsernameClaimMapping, issuerURL string) (authenticationv1alpha1.PrefixedClaimOrExpression, *authenticationcel.CompilationResult, error) { + out := authenticationv1alpha1.PrefixedClaimOrExpression{} + + if len(usernameClaimMapping.Claim) == 0 { + return out, nil, fmt.Errorf("username claim is required but an empty value was provided") + } + out.Claim = usernameClaimMapping.Claim + + switch usernameClaimMapping.PrefixPolicy { + case configv1.Prefix: + if usernameClaimMapping.Prefix == nil { + return out, nil, fmt.Errorf("nil username prefix while policy expects one") + } + out.Prefix = &usernameClaimMapping.Prefix.PrefixString + case configv1.NoPrefix: + out.Prefix = ptr.To("") + case configv1.NoOpinion: + prefix := "" + if usernameClaimMapping.Claim != "email" { + prefix = issuerURL + "#" + } + out.Prefix = &prefix + default: + return out, nil, fmt.Errorf("invalid username prefix policy: %s", usernameClaimMapping.PrefixPolicy) + } + + return out, nil, nil +} + +func generateGroupsClaimMapping(groupsMapping configv1.PrefixedClaimMapping, featureGates featuregates.FeatureGate) (authenticationv1alpha1.PrefixedClaimOrExpression, error) { + out := authenticationv1alpha1.PrefixedClaimOrExpression{} + if featureGates.Enabled(features.FeatureGateExternalOIDCWithUpstreamParity) { + if len(groupsMapping.Expression) > 0 && len(groupsMapping.Claim) > 0 { + return out, fmt.Errorf("groups claim mapping must not set both claim and expression") + } + if len(groupsMapping.Expression) > 0 && len(groupsMapping.Prefix) > 0 { + return authenticationv1alpha1.PrefixedClaimOrExpression{}, fmt.Errorf("groups claim mapping must not set prefix when expression is set") + } + + if len(groupsMapping.Expression) > 0 { + if _, err := validateClaimsCELExpression(&authenticationcel.ClaimMappingExpression{ + Expression: groupsMapping.Expression, + }); err != nil { + return authenticationv1alpha1.PrefixedClaimOrExpression{}, fmt.Errorf("invalid CEL expression: %v", err) + } + out.Expression = groupsMapping.Expression + return out, nil + } + } + + out.Claim = groupsMapping.Claim + out.Prefix = &groupsMapping.Prefix + + return out, nil +} + +func generateUIDClaimMapping(uid *configv1.TokenClaimOrExpressionMapping) (authenticationv1alpha1.ClaimOrExpression, error) { + out := authenticationv1alpha1.ClaimOrExpression{} + + // UID mapping can only specify either claim or expression, not both. + // This should be rejected at admission time of the authentications.config.openshift.io CRD. + // Even though this is the case, we still perform a runtime validation to ensure we never + // attempt to create an invalid configuration. + // If neither claim or expression is specified, default the claim to "sub" + switch { + case uid == nil: + out.Claim = "sub" + case len(uid.Claim) > 0 && len(uid.Expression) == 0: + out.Claim = uid.Claim + case len(uid.Expression) > 0 && len(uid.Claim) == 0: + if _, err := validateClaimsCELExpression(&authenticationcel.ClaimMappingExpression{ + Expression: uid.Expression, + }); err != nil { + return authenticationv1alpha1.ClaimOrExpression{}, fmt.Errorf("validating expression: %v", err) + } + out.Expression = uid.Expression + case len(uid.Claim) > 0 && len(uid.Expression) > 0: + return authenticationv1alpha1.ClaimOrExpression{}, fmt.Errorf("uid mapping must set either claim or expression, not both: %v", uid) + default: + return authenticationv1alpha1.ClaimOrExpression{}, fmt.Errorf("unable to handle uid mapping: %v", uid) + } + + return out, nil +} + +func generateExtraClaimMapping(extraMappings ...configv1.ExtraMapping) ([]authenticationv1alpha1.ExtraMapping, []authenticationcel.CompilationResult, error) { + out := []authenticationv1alpha1.ExtraMapping{} + var compilationResults []authenticationcel.CompilationResult + errs := []error{} + for _, extraMapping := range extraMappings { + extra, result, err := generateExtraMapping(extraMapping) + if err != nil { + errs = append(errs, err) + continue + } + out = append(out, extra) + if result != nil { + compilationResults = append(compilationResults, *result) + } + } + if len(errs) > 0 { + return nil, nil, errors.Join(errs...) + } + return out, compilationResults, nil +} + +func generateExtraMapping(extraMapping configv1.ExtraMapping) (authenticationv1alpha1.ExtraMapping, *authenticationcel.CompilationResult, error) { + out := authenticationv1alpha1.ExtraMapping{} + + if len(extraMapping.Key) == 0 { + return authenticationv1alpha1.ExtraMapping{}, nil, fmt.Errorf("extra mapping must set a key, but none was provided: %v", extraMapping) + } + + if len(extraMapping.ValueExpression) == 0 { + return authenticationv1alpha1.ExtraMapping{}, nil, fmt.Errorf("extra mapping must set a valueExpression, but none was provided: %v", extraMapping) + } + + result, err := validateClaimsCELExpression(&authenticationcel.ExtraMappingExpression{ + Key: extraMapping.Key, + Expression: extraMapping.ValueExpression, + }) + if err != nil { + return authenticationv1alpha1.ExtraMapping{}, nil, fmt.Errorf("validating expression: %v", err) + } + + out.Key = extraMapping.Key + out.ValueExpression = extraMapping.ValueExpression + + return out, &result, nil +} + +func generateClaimValidationRules(state *oidcGenerationState, claimValidationRules ...configv1.TokenClaimValidationRule) ([]authenticationv1alpha1.ClaimValidationRule, error) { + out := []authenticationv1alpha1.ClaimValidationRule{} + errs := []error{} + for _, claimValidationRule := range claimValidationRules { + rule, result, err := generateClaimValidationRule(claimValidationRule) + if err != nil { + errs = append(errs, fmt.Errorf("generating claimValidationRule: %v", err)) + continue + } + out = append(out, rule) + if result != nil { + state.ClaimValidationResults = append(state.ClaimValidationResults, *result) + } + } + if len(errs) > 0 { + return nil, errors.Join(errs...) + } + return out, nil +} + +func generateClaimValidationRule(claimValidationRule configv1.TokenClaimValidationRule) (authenticationv1alpha1.ClaimValidationRule, *authenticationcel.CompilationResult, error) { + out := authenticationv1alpha1.ClaimValidationRule{} + switch claimValidationRule.Type { + case configv1.TokenValidationRuleTypeRequiredClaim: + if claimValidationRule.RequiredClaim == nil { + return authenticationv1alpha1.ClaimValidationRule{}, nil, fmt.Errorf("claimValidationRule.type is %s and requiredClaim is not set", configv1.TokenValidationRuleTypeRequiredClaim) + } + out.Claim = claimValidationRule.RequiredClaim.Claim + out.RequiredValue = claimValidationRule.RequiredClaim.RequiredValue + case configv1.TokenValidationRuleTypeCEL: + if len(claimValidationRule.CEL.Expression) == 0 { + return authenticationv1alpha1.ClaimValidationRule{}, nil, fmt.Errorf("claimValidationRule.type is %s and expression is not set", configv1.TokenValidationRuleTypeCEL) + } + result, err := validateClaimsCELExpression(&authenticationcel.ClaimValidationCondition{ + Expression: claimValidationRule.CEL.Expression, + }) + if err != nil { + return authenticationv1alpha1.ClaimValidationRule{}, nil, fmt.Errorf("invalid CEL expression: %v", err) + } + out.Expression = claimValidationRule.CEL.Expression + out.Message = claimValidationRule.CEL.Message + return out, &result, nil + default: + return authenticationv1alpha1.ClaimValidationRule{}, nil, fmt.Errorf("unknown claimValidationRule type %q", claimValidationRule.Type) + } + return out, nil, nil +} + +func generateUserValidationRule(rule configv1.TokenUserValidationRule) (authenticationv1alpha1.UserValidationRule, error) { + if len(rule.Expression) == 0 { + return authenticationv1alpha1.UserValidationRule{}, fmt.Errorf("userValidationRule expression must be non-empty") + } + + // validate CEL expression + if _, err := validateUserCELExpression(&authenticationcel.UserValidationCondition{ + Expression: rule.Expression, + }); err != nil { + return authenticationv1alpha1.UserValidationRule{}, fmt.Errorf("invalid CEL expression: %v", err) + } + + return authenticationv1alpha1.UserValidationRule{ + Expression: rule.Expression, + Message: rule.Message, + }, nil +} + +func generateUserValidationRules(rules []configv1.TokenUserValidationRule) ([]authenticationv1alpha1.UserValidationRule, error) { + out := []authenticationv1alpha1.UserValidationRule{} + errs := []error{} + + for _, r := range rules { + uvr, err := generateUserValidationRule(r) + if err != nil { + errs = append(errs, fmt.Errorf("generating userValidationRule: %v", err)) + continue + } + out = append(out, uvr) + } + + if len(errs) > 0 { + return nil, errors.Join(errs...) + } + + return out, nil +} + +func validateOAuthApiserverAuthenticationConfiguration(auth *authenticationv1alpha1.AuthenticationConfiguration) error { + if auth == nil { + return nil + } + + for _, jwt := range auth.JWT { + var caCertPool *x509.CertPool + var err error + if len(jwt.Issuer.CertificateAuthority) > 0 { + caCertPool, err = cert.NewPoolFromBytes([]byte(jwt.Issuer.CertificateAuthority)) + if err != nil { + return fmt.Errorf("issuer CA is invalid: %v", err) + } + } + + // make sure we can access the issuer with the given cert pool (system CAs used if pool is empty) + url := strings.TrimSuffix(jwt.Issuer.URL, "/") + oidcDiscoveryEndpointPath + if len(jwt.Issuer.DiscoveryURL) > 0 { + url = jwt.Issuer.DiscoveryURL + } + + if err := validateCACert(url, caCertPool); err != nil { + certMessage := "using the specified CA cert" + if caCertPool == nil { + certMessage = "using the system CAs" + } + return fmt.Errorf("could not validate IDP URL %s: %v", certMessage, err) + } + } + + return nil +} + +// validateCACert makes a request to the provider's well-known endpoint using the +// specified CA cert pool to validate that the certs in the pool match the host. +func validateCACert(hostURL string, caCertPool *x509.CertPool) error { + client := &http.Client{ + Timeout: 5 * time.Second, + } + + transport := http.DefaultTransport.(*http.Transport).Clone() + if transport == nil { + transport = &http.Transport{} + } + + if transport.TLSClientConfig == nil { + transport.TLSClientConfig = &tls.Config{ + MinVersion: tls.VersionTLS12, + } + } + + transport.TLSClientConfig.RootCAs = caCertPool + client.Transport = transport + + req, err := http.NewRequest(http.MethodGet, hostURL, nil) + if err != nil { + return fmt.Errorf("could not create well-known HTTP request: %v", err) + } + + var resp *http.Response + var connErr error + retryCtx, cancel := context.WithTimeout(req.Context(), 10*time.Second) + defer cancel() + if err := retry.RetryOnConnectionErrors(retryCtx, func(ctx context.Context) (done bool, err error) { + resp, connErr = client.Do(req.WithContext(ctx)) + return connErr == nil, connErr + }); err != nil { + return fmt.Errorf("persistent well-known GET error: %v", err) + } + if connErr != nil { + return fmt.Errorf("GET well-known error: %v", connErr) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + body, err := io.ReadAll(resp.Body) + if err != nil { + return fmt.Errorf("unable to read response body; HTTP status: %s; error: %v", resp.Status, err) + } + + return fmt.Errorf("unexpected well-known status code %s: %s", resp.Status, body) + } + + return nil +} + +// validateClaimsCELExpression validates a CEL expression using the provided expression accessor. +// It uses the default authentication CEL compiler that the KAS uses and thus defaults to +// validating CEL expressions based on the version of the k8s dependencies used by the +// cluster-authentication-operator. +// Compiles the expression with the `claims` environment variable available. +func validateClaimsCELExpression(expressionAccessor authenticationcel.ExpressionAccessor) (authenticationcel.CompilationResult, error) { + return authenticationcel.NewDefaultCompiler().CompileClaimsExpression(expressionAccessor) +} + +// validateUserCELExpression validates a user CEL expression using the user.* scope. +func validateUserCELExpression(expressionAccessor authenticationcel.ExpressionAccessor) (authenticationcel.CompilationResult, error) { + return authenticationcel.NewDefaultCompiler().CompileUserExpression(expressionAccessor) +} + +// validateEmailVerifiedUsage enforces that when claims.email is used in the +// username expression, claims.email_verified must be referenced in at least +// one of: username.expression, extra[*].valueExpression, or +// claimValidationRules[*].cel.expression. +// This mirrors the upstream KAS validation logic. +func validateEmailVerifiedUsage(state *oidcGenerationState) error { + if state == nil { + return nil + } + + if state.UsernameResult == nil { + return nil + } + + if !usesEmailClaim(state.UsernameResult.AST) { + return nil + } + + if usesEmailVerifiedClaim(state.UsernameResult.AST) || anyUsesEmailVerifiedClaim(state.ExtraResults) || anyUsesEmailVerifiedClaim(state.ClaimValidationResults) { + return nil + } + + return fmt.Errorf("claims.email_verified must be used in claimMappings.username.expression or claimMappings.extra[*].valueExpression or claimValidationRules[*].expression when claims.email is used in claimMappings.username.expression") +} + +// usesEmailClaim, usesEmailVerifiedClaim, anyUsesEmailVerifiedClaim, hasSelectExp, +// isIdentOperand, and isConstField are copied from the upstream Kubernetes apiserver +// CEL validation logic introduced in https://github.com/kubernetes/kubernetes/pull/123737 (commit 121607e): +// https://github.com/kubernetes/kubernetes/blob/bfb362c57578518bed8e08a56a7318bab9b57429/staging/src/k8s.io/apiserver/pkg/apis/apiserver/validation/validation.go#L443 +func usesEmailClaim(ast *celgo.Ast) bool { + if ast == nil { + return false + } + return hasSelectExp(ast.Expr(), "claims", "email") +} + +func usesEmailVerifiedClaim(ast *celgo.Ast) bool { + if ast == nil { + return false + } + return hasSelectExp(ast.Expr(), "claims", "email_verified") +} + +func anyUsesEmailVerifiedClaim(results []authenticationcel.CompilationResult) bool { + for _, result := range results { + if usesEmailVerifiedClaim(result.AST) { + return true + } + } + return false +} + +func hasSelectExp(exp *exprpb.Expr, operand, field string) bool { + if exp == nil { + return false + } + switch e := exp.ExprKind.(type) { + case *exprpb.Expr_ConstExpr, + *exprpb.Expr_IdentExpr: + return false + case *exprpb.Expr_SelectExpr: + s := e.SelectExpr + if s == nil { + return false + } + if isIdentOperand(s.Operand, operand) && s.Field == field { + return true + } + return hasSelectExp(s.Operand, operand, field) + case *exprpb.Expr_CallExpr: + c := e.CallExpr + if c == nil { + return false + } + if c.Target == nil && c.Function == operators.OptSelect && len(c.Args) == 2 && + isIdentOperand(c.Args[0], operand) && isConstField(c.Args[1], field) { + return true + } + for _, arg := range c.Args { + if hasSelectExp(arg, operand, field) { + return true + } + } + return hasSelectExp(c.Target, operand, field) + case *exprpb.Expr_ListExpr: + l := e.ListExpr + if l == nil { + return false + } + for _, element := range l.Elements { + if hasSelectExp(element, operand, field) { + return true + } + } + return false + case *exprpb.Expr_StructExpr: + s := e.StructExpr + if s == nil { + return false + } + for _, entry := range s.Entries { + if hasSelectExp(entry.GetMapKey(), operand, field) { + return true + } + if hasSelectExp(entry.Value, operand, field) { + return true + } + } + return false + case *exprpb.Expr_ComprehensionExpr: + c := e.ComprehensionExpr + if c == nil { + return false + } + return hasSelectExp(c.IterRange, operand, field) || + hasSelectExp(c.AccuInit, operand, field) || + hasSelectExp(c.LoopCondition, operand, field) || + hasSelectExp(c.LoopStep, operand, field) || + hasSelectExp(c.Result, operand, field) + default: + return false + } +} + +func isIdentOperand(exp *exprpb.Expr, operand string) bool { + if len(operand) == 0 { + return false + } + id := exp.GetIdentExpr() + return id != nil && id.Name == operand +} + +func isConstField(exp *exprpb.Expr, field string) bool { + if len(field) == 0 { + return false + } + c := exp.GetConstExpr() + return c != nil && c.GetStringValue() == field +} + +func generateExternalClaimsSources(cmLister corev1listers.ConfigMapLister, secretLister corev1listers.SecretLister, sources ...configv1.ExternalClaimsSource) ([]authenticationv1alpha1.ExternalClaimsSource, error) { + out := []authenticationv1alpha1.ExternalClaimsSource{} + seenClaimNames := sets.New[string]() + for _, source := range sources { + source, err := generateExternalClaimsSource(source, cmLister, secretLister, seenClaimNames) + if err != nil { + return nil, err + } + + if source != nil { + out = append(out, *source) + } + } + + return out, nil +} + +func generateExternalClaimsSource(source configv1.ExternalClaimsSource, cmLister corev1listers.ConfigMapLister, secretLister corev1listers.SecretLister, seenClaimNames sets.Set[string]) (*authenticationv1alpha1.ExternalClaimsSource, error) { + authentication, err := generateExternalClaimsSourceAuthentication(source.Authentication, secretLister, cmLister) + if err != nil { + return nil, err + } + + tls, err := generateExternalClaimsSourceTLS(source.TLS, cmLister) + if err != nil { + return nil, err + } + + url, err := generateExternalClaimsSourceURL(source.URL) + if err != nil { + return nil, err + } + + mappings, err := generateExternalClaimsSourceMappings(seenClaimNames, source.Mappings...) + if err != nil { + return nil, err + } + + conditions, err := generateExternalClaimsSourceConditions(source.Predicates...) + if err != nil { + return nil, err + } + + return &authenticationv1alpha1.ExternalClaimsSource{ + Authentication: authentication, + TLS: tls, + URL: url, + Mappings: mappings, + Conditions: conditions, + }, nil +} + +func generateExternalClaimsSourceAuthentication(externalSourceAuthentication configv1.ExternalSourceAuthentication, secretLister corev1listers.SecretLister, cmLister corev1listers.ConfigMapLister) (*authenticationv1alpha1.Authentication, error) { + switch externalSourceAuthentication.Type { + case "": // signals the omitted case which is valid and means to use anonymous auth. This means we should omit it as well so anonymous auth takes place. + return nil, nil + case configv1.ExternalSourceAuthenticationTypeRequestProvidedToken: + return &authenticationv1alpha1.Authentication{ + Type: ptr.To(authenticationv1alpha1.AuthenticationTypeRequestProvidedToken), + }, nil + case configv1.ExternalSourceAuthenticationTypeClientCredential: + cc, err := generateExternalClaimsSourceAuthenticationClientCredential(externalSourceAuthentication.ClientCredential, secretLister, cmLister) + if err != nil { + return nil, fmt.Errorf("generating client credentials configuration: %w", err) + } + + return &authenticationv1alpha1.Authentication{ + Type: ptr.To(authenticationv1alpha1.AuthenticationTypeClientCredential), + ClientCredential: cc, + }, nil + default: + return nil, fmt.Errorf("unknown external source authentication type %q", externalSourceAuthentication.Type) + } +} + +var printableASCIIRegexp = regexp.MustCompile(`^[[:print:]]+$`) + +func generateExternalClaimsSourceAuthenticationClientCredential(clientCredentialConfig configv1.ClientCredentialConfig, secretLister corev1listers.SecretLister, cmLister corev1listers.ConfigMapLister) (*authenticationv1alpha1.ClientCredentialConfig, error) { + // TODO: enable validation when it is possible to do so. Currently blocked + // due to oauth-apiserver not being rebased on 1.35 and the KAS library changes + // not existing in the 1.35 branch. + /* + if err := validation.ValidateClientCredentialConfigClientID(clientCredentialConfig.ClientID, field.NewPath("")); err != nil { + return nil, fmt.Errorf("validating client id: %w", kubeErrorListToGoError(err)) + } + + if err := validation.ValidateTokenEndpoint(clientCredentialConfig.TokenEndpoint, field.NewPath("")); err != nil { + return nil, fmt.Errorf("validating token endpoint: %w", kubeErrorListToGoError(err)) + } + */ + + clientSecret, err := getClientSecretFromSecret(clientCredentialConfig.ClientSecret.Name, secretLister) + if err != nil { + return nil, fmt.Errorf("getting client secret: %w", err) + } + + // TODO: enable validation when it is possible to do so. Currently blocked + // due to oauth-apiserver not being rebased on 1.35 and the KAS library changes + // not existing in the 1.35 branch. + /* + if err := validation.ValidateClientCredentialConfigClientSecret(clientSecret, field.NewPath("")); err != nil { + return nil, fmt.Errorf("validating client secret: %w", kubeErrorListToGoError(err)) + } + */ + + scopes, err := generateClientCredentialScopes(clientCredentialConfig.Scopes...) + if err != nil { + return nil, fmt.Errorf("generating scopes: %w", err) + } + + var certificateAuthority *string = nil + if len(clientCredentialConfig.TLS.CertificateAuthority.Name) > 0 { + ca, err := getCertificateAuthorityFromConfigMap(clientCredentialConfig.TLS.CertificateAuthority.Name, cmLister) + if err != nil { + return nil, fmt.Errorf("getting certificate authority: %w", err) + } + + certificateAuthority = &ca + } + + return &authenticationv1alpha1.ClientCredentialConfig{ + ClientID: clientCredentialConfig.ClientID, + ClientSecret: clientSecret, + TokenEndpoint: clientCredentialConfig.TokenEndpoint, + Scopes: scopes, + TLS: &authenticationv1alpha1.TLS{ + CertificateAuthority: certificateAuthority, + }, + }, nil +} + +func generateClientCredentialScopes(scopes ...configv1.OAuth2Scope) ([]string, error) { + out := make([]string, 0, len(scopes)) + errs := []error{} + for _, scope := range scopes { + // TODO: enable validation when it is possible to do so. Currently blocked + // due to oauth-apiserver not being rebased on 1.35 and the KAS library changes + // not existing in the 1.35 branch. + /* + err := validation.ValidateClientCredentialConfigScope(string(scope), field.NewPath("")) + if err != nil { + errs = append(errs, fmt.Errorf("validating scopes[%s]: %w", i, kubeErrorListToGoError(err))) + continue + } + */ + + out = append(out, string(scope)) + } + + return out, errors.Join(errs...) +} + +func getClientSecretFromSecret(name string, secretLister corev1listers.SecretLister) (string, error) { + secret, err := secretLister.Secrets(configNamespace).Get(name) + if err != nil { + return "", fmt.Errorf("could not retrieve auth secret %s/%s to get client secret: %v", configNamespace, name, err) + } + + clientSecret, ok := secret.Data["client-secret"] + if !ok || len(clientSecret) == 0 { + return "", fmt.Errorf("secret %s/%s key \"client-secret\" missing or empty", configNamespace, name) + } + + return string(clientSecret), nil +} + +func generateExternalClaimsSourceTLS(externalSourceTLS configv1.ExternalSourceTLS, cmLister corev1listers.ConfigMapLister) (*authenticationv1alpha1.TLS, error) { + caData, err := getCertificateAuthorityFromConfigMap(externalSourceTLS.CertificateAuthority.Name, cmLister) + if err != nil { + return nil, fmt.Errorf("getting certificate authority for external source: %w", err) + } + + return &authenticationv1alpha1.TLS{ + CertificateAuthority: &caData, + }, nil +} + +func generateExternalClaimsSourceURL(sourceURL configv1.SourceURL) (*authenticationv1alpha1.SourceURL, error) { + // TODO: enable validation when it is possible to do so. Currently blocked + // due to oauth-apiserver not being rebased on 1.35 and the KAS library changes + // not existing in the 1.35 branch. + /* + if err := validation.ValidateExternalClaimsSourceURLHostname(&sourceURL.Hostname, field.NewPath("")); err != nil { + return nil, fmt.Errorf("validating hostname: %w", kubeErrorListToGoError(err)) + } + + if err := validation.ValidateExternalClaimsSourceURLPathExpression(externaloidccel.NewCompiler(), &sourceURL.PathExpression, field.NewPath("")); err != nil { + return nil, fmt.Errorf("validating path expression: %w", kubeErrorListToGoError(err)) + } + */ + + return &authenticationv1alpha1.SourceURL{ + Hostname: &sourceURL.Hostname, + PathExpression: &sourceURL.PathExpression, + }, nil +} + +func generateExternalClaimsSourceMappings(seenClaimNames sets.Set[string], sourcedClaimMappings ...configv1.SourcedClaimMapping) ([]authenticationv1alpha1.SourcedClaimMapping, error) { + out := make([]authenticationv1alpha1.SourcedClaimMapping, 0, len(sourcedClaimMappings)) + + errs := []error{} + for _, sourcedClaimMapping := range sourcedClaimMappings { + // TODO: enable validation when it is possible to do so. Currently blocked + // due to oauth-apiserver not being rebased on 1.35 and the KAS library changes + // not existing in the 1.35 branch. + /* + if err := validation.ValidateExternalClaimsSourceMappingName(&sourcedClaimMapping.Name, seenClaimNames, field.NewPath("")); err != nil { + errs = append(errs, fmt.Errorf("validating mappings[%d]: validating name %q: %w", i, sourcedClaimMapping.Name, kubeErrorListToGoError(err))) + continue + } + + if err := validation.ValidateExternalClaimsSourceMappingExpression(externaloidccel.NewCompiler(), &sourcedClaimMapping.Expression, field.NewPath("")); err != nil { + errs = append(errs, fmt.Errorf("validating mappings[%d]: validating expression %q: %w", i, sourcedClaimMapping.Expression, kubeErrorListToGoError(err))) + continue + } + */ + + out = append(out, authenticationv1alpha1.SourcedClaimMapping{ + Name: &sourcedClaimMapping.Name, + Expression: &sourcedClaimMapping.Expression, + }) + } + + return out, errors.Join(errs...) +} + +func generateExternalClaimsSourceConditions(externalSourcePredicates ...configv1.ExternalSourcePredicate) ([]authenticationv1alpha1.ExternalSourceCondition, error) { + out := make([]authenticationv1alpha1.ExternalSourceCondition, 0, len(externalSourcePredicates)) + + errs := []error{} + // seenConditions := sets.New[string]() + for _, predicate := range externalSourcePredicates { + // TODO: enable validation when it is possible to do so. Currently blocked + // due to oauth-apiserver not being rebased on 1.35 and the KAS library changes + // not existing in the 1.35 branch. + /* + cond := authentication.ExternalSourceCondition{ + Expression: &predicate.Expression, + } + + if err := validation.ValidateExternalSourceCondition(externaloidccel.NewCompiler(), cond, seenConditions, field.NewPath("")); err != nil { + errs = append(errs, fmt.Errorf("validating predicates[%d]: validating expression %q: %w", i, predicate.Expression, kubeErrorListToGoError(err))) + } + */ + + out = append(out, authenticationv1alpha1.ExternalSourceCondition{ + Expression: &predicate.Expression, + }) + } + + return out, errors.Join(errs...) +} + +// TODO: enable validation when it is possible to do so. Currently blocked +// due to oauth-apiserver not being rebased on 1.35 and the KAS library changes +// not existing in the 1.35 branch. +/* +func kubeErrorListToGoError(list field.ErrorList) error { + errs := make([]error, 0, len(list)) + for _, err := range list { + errs = append(errs, errors.New(fmt.Sprintf("%s: %s", err.Type.String(), err.Detail))) + } + + return errors.Join(errs...) +} +*/ diff --git a/pkg/controllers/externaloidc/generation/oauthapiserver/generate_test.go b/pkg/controllers/externaloidc/generation/oauthapiserver/generate_test.go new file mode 100644 index 0000000000..54490fb3ce --- /dev/null +++ b/pkg/controllers/externaloidc/generation/oauthapiserver/generate_test.go @@ -0,0 +1,2146 @@ +package oauthapiserver + +import ( + "bytes" + "crypto" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/rsa" + "crypto/tls" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "errors" + "fmt" + "math/big" + "net" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + configv1 "github.com/openshift/api/config/v1" + "github.com/openshift/api/features" + "github.com/openshift/library-go/pkg/operator/configobserver/featuregates" + authenticationv1alpha1 "github.com/openshift/oauth-apiserver/pkg/externaloidc/apis/authentication/v1alpha1" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + corev1listers "k8s.io/client-go/listers/core/v1" + "k8s.io/client-go/tools/cache" + certutil "k8s.io/client-go/util/cert" + "k8s.io/utils/ptr" +) + +var ( + testCertData = "fake-ca-cert" + + baseAuthResource = *newAuthWithSpec(configv1.AuthenticationSpec{ + Type: configv1.AuthenticationTypeOIDC, + OIDCProviders: []configv1.OIDCProvider{ + { + Name: "test-oidc-provider", + Issuer: configv1.TokenIssuer{ + CertificateAuthority: configv1.ConfigMapNameReference{Name: "oidc-ca-bundle"}, + Audiences: []configv1.TokenAudience{"my-test-aud", "another-aud"}, + }, + OIDCClients: []configv1.OIDCClientConfig{ + { + ComponentName: "console", + ComponentNamespace: "openshift-console", + ClientID: "console-oidc-client", + }, + { + ComponentName: "kube-apiserver", + ComponentNamespace: "openshift-kube-apiserver", + ClientID: "test-oidc-client", + }, + }, + ClaimMappings: configv1.TokenClaimMappings{ + Username: configv1.UsernameClaimMapping{ + Claim: "username", + PrefixPolicy: configv1.Prefix, + Prefix: &configv1.UsernamePrefix{ + PrefixString: "oidc-user:", + }, + }, + Groups: configv1.PrefixedClaimMapping{ + TokenClaimMapping: configv1.TokenClaimMapping{ + Claim: "groups", + }, + Prefix: "oidc-group:", + }, + }, + ClaimValidationRules: []configv1.TokenClaimValidationRule{ + { + Type: configv1.TokenValidationRuleTypeRequiredClaim, + RequiredClaim: &configv1.TokenRequiredClaim{ + Claim: "username", + RequiredValue: "test-username", + }, + }, + { + Type: configv1.TokenValidationRuleTypeRequiredClaim, + RequiredClaim: &configv1.TokenRequiredClaim{ + Claim: "email", + RequiredValue: "test-email", + }, + }, + }, + }, + }, + }) + + baseAuthConfig = authenticationv1alpha1.AuthenticationConfiguration{ + TypeMeta: metav1.TypeMeta{ + Kind: kindAuthenticationConfiguration, + APIVersion: authenticationv1alpha1.SchemeGroupVersion.String(), + }, + JWT: []authenticationv1alpha1.JWTAuthenticator{ + { + Issuer: &authenticationv1alpha1.Issuer{ + Audiences: []string{"my-test-aud", "another-aud"}, + CertificateAuthority: testCertData, + AudienceMatchPolicy: authenticationv1alpha1.AudienceMatchPolicyMatchAny, + }, + ClaimMappings: &authenticationv1alpha1.ClaimMappings{ + Username: authenticationv1alpha1.PrefixedClaimOrExpression{ + Claim: "username", + Prefix: ptr.To("oidc-user:"), + }, + Groups: authenticationv1alpha1.PrefixedClaimOrExpression{ + Claim: "groups", + Prefix: ptr.To("oidc-group:"), + }, + }, + ClaimValidationRules: []authenticationv1alpha1.ClaimValidationRule{ + { + Claim: "username", + RequiredValue: "test-username", + }, + { + Claim: "email", + RequiredValue: "test-email", + }, + }, + }, + }, + } + + baseCABundleConfigMap = corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "oidc-ca-bundle", + Namespace: configNamespace, + }, + Data: map[string]string{ + "ca-bundle.crt": testCertData, + }, + } + + caBundleConfigMapInvalidKey = corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "oidc-ca-bundle", + Namespace: configNamespace, + }, + Data: map[string]string{ + "invalid": testCertData, + }, + } + + caBundleConfigMapNoData = corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "oidc-ca-bundle", + Namespace: configNamespace, + }, + Data: map[string]string{ + "ca-bundle.crt": "", + }, + } +) + +func TestAuthenticationConfigurationGeneratorGenerateAuthenticationConfiguration(t *testing.T) { + for _, tt := range []struct { + name string + + auth configv1.Authentication + caBundleConfigMap *corev1.ConfigMap + configMapIndexer cache.Indexer + secretIndexer cache.Indexer + configValidator validationFunc + + expectedAuthConfig *authenticationv1alpha1.AuthenticationConfiguration + expectError bool + featureGates featuregates.FeatureGate + }{ + { + name: "ca bundle configmap lister error", + auth: baseAuthResource, + configMapIndexer: cache.Indexer(&everFailingIndexer{}), + expectError: true, + featureGates: featuregates.NewFeatureGate( + []configv1.FeatureGateName{}, + []configv1.FeatureGateName{ + features.FeatureGateExternalOIDCWithAdditionalClaimMappings, + features.FeatureGateExternalOIDCWithUpstreamParity, + features.FeatureGateExternalOIDCExternalClaimsSourcing, + }, + ), + }, + { + name: "ca bundle configmap without required key", + auth: baseAuthResource, + caBundleConfigMap: &caBundleConfigMapInvalidKey, + expectError: true, + featureGates: featuregates.NewFeatureGate( + []configv1.FeatureGateName{}, + []configv1.FeatureGateName{ + features.FeatureGateExternalOIDCWithAdditionalClaimMappings, + features.FeatureGateExternalOIDCWithUpstreamParity, + features.FeatureGateExternalOIDCExternalClaimsSourcing, + }, + ), + }, + { + name: "ca bundle configmap with no data", + auth: baseAuthResource, + caBundleConfigMap: &caBundleConfigMapNoData, + expectError: true, + featureGates: featuregates.NewFeatureGate( + []configv1.FeatureGateName{}, + []configv1.FeatureGateName{ + features.FeatureGateExternalOIDCWithAdditionalClaimMappings, + features.FeatureGateExternalOIDCWithUpstreamParity, + features.FeatureGateExternalOIDCExternalClaimsSourcing, + }, + ), + }, + { + name: "auth config nil prefix when required", + caBundleConfigMap: &baseCABundleConfigMap, + auth: *authWithUpdates(baseAuthResource, []func(auth *configv1.Authentication){ + func(auth *configv1.Authentication) { + for i := range auth.Spec.OIDCProviders { + auth.Spec.OIDCProviders[i].ClaimMappings.Username = configv1.UsernameClaimMapping{ + Claim: "username", + PrefixPolicy: configv1.Prefix, + Prefix: nil, + } + } + }, + }), + expectError: true, + featureGates: featuregates.NewFeatureGate( + []configv1.FeatureGateName{}, + []configv1.FeatureGateName{ + features.FeatureGateExternalOIDCWithAdditionalClaimMappings, + features.FeatureGateExternalOIDCWithUpstreamParity, + features.FeatureGateExternalOIDCExternalClaimsSourcing, + }, + ), + }, + { + name: "auth config invalid prefix policy", + caBundleConfigMap: &baseCABundleConfigMap, + auth: *authWithUpdates(baseAuthResource, []func(auth *configv1.Authentication){ + func(auth *configv1.Authentication) { + for i := range auth.Spec.OIDCProviders { + auth.Spec.OIDCProviders[i].ClaimMappings.Username = configv1.UsernameClaimMapping{ + Claim: "username", + PrefixPolicy: configv1.UsernamePrefixPolicy("invalid-policy"), + } + } + }, + }), + expectError: true, + featureGates: featuregates.NewFeatureGate( + []configv1.FeatureGateName{}, + []configv1.FeatureGateName{ + features.FeatureGateExternalOIDCWithAdditionalClaimMappings, + features.FeatureGateExternalOIDCWithUpstreamParity, + features.FeatureGateExternalOIDCExternalClaimsSourcing, + }, + ), + }, + { + name: "auth config with nil claim in validation rule", + caBundleConfigMap: &baseCABundleConfigMap, + auth: *authWithUpdates(baseAuthResource, []func(auth *configv1.Authentication){ + func(copy *configv1.Authentication) { + for i := range copy.Spec.OIDCProviders { + if len(copy.Spec.OIDCProviders[i].ClaimValidationRules) == 0 { + copy.Spec.OIDCProviders[i].ClaimValidationRules = make([]configv1.TokenClaimValidationRule, 0) + } + copy.Spec.OIDCProviders[i].ClaimValidationRules = append( + copy.Spec.OIDCProviders[i].ClaimValidationRules, + configv1.TokenClaimValidationRule{ + Type: configv1.TokenValidationRuleTypeRequiredClaim, + RequiredClaim: nil, + }, + ) + } + }, + }), + expectError: true, + featureGates: featuregates.NewFeatureGate( + []configv1.FeatureGateName{}, + []configv1.FeatureGateName{ + features.FeatureGateExternalOIDCWithAdditionalClaimMappings, + features.FeatureGateExternalOIDCWithUpstreamParity, + features.FeatureGateExternalOIDCExternalClaimsSourcing, + }, + ), + }, + { + name: "valid auth config", + caBundleConfigMap: &baseCABundleConfigMap, + auth: *authWithUpdates(baseAuthResource, []func(auth *configv1.Authentication){ + func(auth *configv1.Authentication) { + for i := range auth.Spec.OIDCProviders { + auth.Spec.OIDCProviders[i].Issuer.URL = "https://example.com" + } + }, + }), + expectedAuthConfig: authConfigWithUpdates(baseAuthConfig, []func(authConfig *authenticationv1alpha1.AuthenticationConfiguration){ + func(authConfig *authenticationv1alpha1.AuthenticationConfiguration) { + authConfig.JWT[0].Issuer.URL = "https://example.com" + }, + }), + expectError: false, + featureGates: featuregates.NewFeatureGate( + []configv1.FeatureGateName{}, + []configv1.FeatureGateName{ + features.FeatureGateExternalOIDCWithAdditionalClaimMappings, + features.FeatureGateExternalOIDCWithUpstreamParity, + features.FeatureGateExternalOIDCExternalClaimsSourcing, + }, + ), + }, + { + name: "valid auth config during generation, validator fails", + caBundleConfigMap: &baseCABundleConfigMap, + auth: *authWithUpdates(baseAuthResource, []func(auth *configv1.Authentication){ + func(auth *configv1.Authentication) { + for i := range auth.Spec.OIDCProviders { + auth.Spec.OIDCProviders[i].Issuer.URL = "https://example.com" + } + }, + }), + expectError: true, + featureGates: featuregates.NewFeatureGate( + []configv1.FeatureGateName{}, + []configv1.FeatureGateName{ + features.FeatureGateExternalOIDCWithAdditionalClaimMappings, + features.FeatureGateExternalOIDCWithUpstreamParity, + features.FeatureGateExternalOIDCExternalClaimsSourcing, + }, + ), + configValidator: func(_ *authenticationv1alpha1.AuthenticationConfiguration) error { + return errors.New("boom") + }, + }, + { + name: "valid auth config with empty CA name", + auth: *authWithUpdates(baseAuthResource, []func(auth *configv1.Authentication){ + func(auth *configv1.Authentication) { + for i := range auth.Spec.OIDCProviders { + auth.Spec.OIDCProviders[i].Issuer.CertificateAuthority.Name = "" + } + }, + }), + expectedAuthConfig: authConfigWithUpdates(baseAuthConfig, []func(authConfig *authenticationv1alpha1.AuthenticationConfiguration){ + func(authConfig *authenticationv1alpha1.AuthenticationConfiguration) { + for i := range authConfig.JWT { + authConfig.JWT[i].Issuer.CertificateAuthority = "" + } + }, + }), + expectError: false, + featureGates: featuregates.NewFeatureGate( + []configv1.FeatureGateName{}, + []configv1.FeatureGateName{ + features.FeatureGateExternalOIDCWithAdditionalClaimMappings, + features.FeatureGateExternalOIDCWithUpstreamParity, + features.FeatureGateExternalOIDCExternalClaimsSourcing, + }, + ), + }, + { + name: "auth config with default prefix policy", + caBundleConfigMap: &baseCABundleConfigMap, + auth: *authWithUpdates(baseAuthResource, []func(auth *configv1.Authentication){ + func(auth *configv1.Authentication) { + for i := range auth.Spec.OIDCProviders { + auth.Spec.OIDCProviders[i].Issuer.URL = "https://example.com" + auth.Spec.OIDCProviders[i].ClaimMappings.Username = configv1.UsernameClaimMapping{ + Claim: "email", + PrefixPolicy: configv1.NoOpinion, + } + } + }, + }), + expectedAuthConfig: authConfigWithUpdates(baseAuthConfig, []func(authConfig *authenticationv1alpha1.AuthenticationConfiguration){ + func(authConfig *authenticationv1alpha1.AuthenticationConfiguration) { + for i := range authConfig.JWT { + authConfig.JWT[i].Issuer.URL = "https://example.com" + authConfig.JWT[i].ClaimMappings.Username = authenticationv1alpha1.PrefixedClaimOrExpression{ + Claim: "email", + Prefix: ptr.To(""), + } + } + }, + }), + expectError: false, + featureGates: featuregates.NewFeatureGate( + []configv1.FeatureGateName{}, + []configv1.FeatureGateName{ + features.FeatureGateExternalOIDCWithAdditionalClaimMappings, + features.FeatureGateExternalOIDCWithUpstreamParity, + features.FeatureGateExternalOIDCExternalClaimsSourcing, + }, + ), + }, + { + name: "auth config with default prefix policy and username claim email", + caBundleConfigMap: &baseCABundleConfigMap, + auth: *authWithUpdates(baseAuthResource, []func(auth *configv1.Authentication){ + func(auth *configv1.Authentication) { + for i := range auth.Spec.OIDCProviders { + auth.Spec.OIDCProviders[i].Issuer.URL = "https://example.com" + auth.Spec.OIDCProviders[i].ClaimMappings.Username = configv1.UsernameClaimMapping{ + Claim: "username", + PrefixPolicy: configv1.NoOpinion, + } + } + }, + }), + expectedAuthConfig: authConfigWithUpdates(baseAuthConfig, []func(authConfig *authenticationv1alpha1.AuthenticationConfiguration){ + func(authConfig *authenticationv1alpha1.AuthenticationConfiguration) { + for i := range authConfig.JWT { + authConfig.JWT[i].Issuer.URL = "https://example.com" + authConfig.JWT[i].ClaimMappings.Username = authenticationv1alpha1.PrefixedClaimOrExpression{ + Claim: "username", + Prefix: ptr.To("https://example.com#"), + } + } + }, + }), + expectError: false, + featureGates: featuregates.NewFeatureGate( + []configv1.FeatureGateName{}, + []configv1.FeatureGateName{ + features.FeatureGateExternalOIDCWithAdditionalClaimMappings, + features.FeatureGateExternalOIDCWithUpstreamParity, + features.FeatureGateExternalOIDCExternalClaimsSourcing, + }, + ), + }, + { + name: "auth config with no prefix policy", + caBundleConfigMap: &baseCABundleConfigMap, + auth: *authWithUpdates(baseAuthResource, []func(auth *configv1.Authentication){ + func(auth *configv1.Authentication) { + for i := range auth.Spec.OIDCProviders { + auth.Spec.OIDCProviders[i].Issuer.URL = "https://example.com" + auth.Spec.OIDCProviders[i].ClaimMappings.Username = configv1.UsernameClaimMapping{ + Claim: "username", + PrefixPolicy: configv1.NoPrefix, + } + } + }, + }), + expectedAuthConfig: authConfigWithUpdates(baseAuthConfig, []func(authConfig *authenticationv1alpha1.AuthenticationConfiguration){ + func(authConfig *authenticationv1alpha1.AuthenticationConfiguration) { + for i := range authConfig.JWT { + authConfig.JWT[i].Issuer.URL = "https://example.com" + authConfig.JWT[i].ClaimMappings.Username = authenticationv1alpha1.PrefixedClaimOrExpression{ + Claim: "username", + Prefix: ptr.To(""), + } + } + }, + }), + expectError: false, + featureGates: featuregates.NewFeatureGate( + []configv1.FeatureGateName{}, + []configv1.FeatureGateName{ + features.FeatureGateExternalOIDCWithAdditionalClaimMappings, + features.FeatureGateExternalOIDCWithUpstreamParity, + features.FeatureGateExternalOIDCExternalClaimsSourcing, + }, + ), + }, + { + name: "auth config with username claim prefix", + caBundleConfigMap: &baseCABundleConfigMap, + auth: *authWithUpdates(baseAuthResource, []func(auth *configv1.Authentication){ + func(auth *configv1.Authentication) { + for i := range auth.Spec.OIDCProviders { + auth.Spec.OIDCProviders[i].Issuer.URL = "https://example.com" + auth.Spec.OIDCProviders[i].ClaimMappings.Username = configv1.UsernameClaimMapping{ + Claim: "username", + PrefixPolicy: configv1.Prefix, + Prefix: &configv1.UsernamePrefix{ + PrefixString: "oidc-user:", + }, + } + } + }, + }), + expectedAuthConfig: authConfigWithUpdates(baseAuthConfig, []func(authConfig *authenticationv1alpha1.AuthenticationConfiguration){ + func(authConfig *authenticationv1alpha1.AuthenticationConfiguration) { + for i := range authConfig.JWT { + authConfig.JWT[i].Issuer.URL = "https://example.com" + authConfig.JWT[i].ClaimMappings.Username = authenticationv1alpha1.PrefixedClaimOrExpression{ + Claim: "username", + Prefix: ptr.To("oidc-user:"), + } + } + }, + }), + expectError: false, + featureGates: featuregates.NewFeatureGate( + []configv1.FeatureGateName{}, + []configv1.FeatureGateName{ + features.FeatureGateExternalOIDCWithAdditionalClaimMappings, + features.FeatureGateExternalOIDCWithUpstreamParity, + features.FeatureGateExternalOIDCExternalClaimsSourcing, + }, + ), + }, + { + name: "auth config with empty string for username claim", + caBundleConfigMap: &baseCABundleConfigMap, + auth: *authWithUpdates(baseAuthResource, []func(auth *configv1.Authentication){ + func(auth *configv1.Authentication) { + for i := range auth.Spec.OIDCProviders { + auth.Spec.OIDCProviders[i].ClaimMappings.Username = configv1.UsernameClaimMapping{ + Claim: "", + } + } + }, + }), + expectError: true, + featureGates: featuregates.NewFeatureGate( + []configv1.FeatureGateName{}, + []configv1.FeatureGateName{ + features.FeatureGateExternalOIDCWithAdditionalClaimMappings, + features.FeatureGateExternalOIDCWithUpstreamParity, + features.FeatureGateExternalOIDCExternalClaimsSourcing, + }, + ), + }, + { + name: "auth config with no uid claim or expression", + caBundleConfigMap: &baseCABundleConfigMap, + auth: baseAuthResource, + expectedAuthConfig: authConfigWithUpdates(baseAuthConfig, []func(authConfig *authenticationv1alpha1.AuthenticationConfiguration){ + func(authConfig *authenticationv1alpha1.AuthenticationConfiguration) { + for i := range authConfig.JWT { + authConfig.JWT[i].ClaimMappings.UID.Claim = "sub" + } + }, + }), + expectError: false, + featureGates: featuregates.NewFeatureGate( + []configv1.FeatureGateName{ + features.FeatureGateExternalOIDCWithAdditionalClaimMappings, + features.FeatureGateExternalOIDCWithUpstreamParity, + features.FeatureGateExternalOIDCExternalClaimsSourcing, + }, + []configv1.FeatureGateName{}, + ), + }, + { + name: "auth config with uid claim and expression", + caBundleConfigMap: &baseCABundleConfigMap, + auth: *authWithUpdates(baseAuthResource, []func(auth *configv1.Authentication){ + func(auth *configv1.Authentication) { + for i := range auth.Spec.OIDCProviders { + auth.Spec.OIDCProviders[i].ClaimMappings.UID = &configv1.TokenClaimOrExpressionMapping{ + Claim: "sub", + Expression: "claims.sub", + } + } + }, + }), + expectError: true, + featureGates: featuregates.NewFeatureGate( + []configv1.FeatureGateName{ + features.FeatureGateExternalOIDCWithAdditionalClaimMappings, + features.FeatureGateExternalOIDCWithUpstreamParity, + }, + []configv1.FeatureGateName{ + features.FeatureGateExternalOIDCExternalClaimsSourcing, + }, + ), + }, + { + name: "auth config with uid expression", + caBundleConfigMap: &baseCABundleConfigMap, + auth: *authWithUpdates(baseAuthResource, []func(auth *configv1.Authentication){ + func(auth *configv1.Authentication) { + for i := range auth.Spec.OIDCProviders { + auth.Spec.OIDCProviders[i].ClaimMappings.UID = &configv1.TokenClaimOrExpressionMapping{ + Claim: "", + Expression: "claims.sub", + } + } + }, + }), + expectedAuthConfig: authConfigWithUpdates(baseAuthConfig, []func(authConfig *authenticationv1alpha1.AuthenticationConfiguration){ + func(authConfig *authenticationv1alpha1.AuthenticationConfiguration) { + for i := range authConfig.JWT { + authConfig.JWT[i].ClaimMappings.UID.Claim = "" + authConfig.JWT[i].ClaimMappings.UID.Expression = "claims.sub" + } + }, + }), + expectError: false, + featureGates: featuregates.NewFeatureGate( + []configv1.FeatureGateName{ + features.FeatureGateExternalOIDCWithAdditionalClaimMappings, + features.FeatureGateExternalOIDCWithUpstreamParity, + }, + []configv1.FeatureGateName{ + features.FeatureGateExternalOIDCExternalClaimsSourcing, + }, + ), + }, + { + name: "auth config with extra missing key", + caBundleConfigMap: &baseCABundleConfigMap, + auth: *authWithUpdates(baseAuthResource, []func(auth *configv1.Authentication){ + func(auth *configv1.Authentication) { + for i := range auth.Spec.OIDCProviders { + auth.Spec.OIDCProviders[i].ClaimMappings.Extra = []configv1.ExtraMapping{ + { + ValueExpression: "claims.foo", + }, + } + } + }, + }), + expectError: true, + featureGates: featuregates.NewFeatureGate( + []configv1.FeatureGateName{ + features.FeatureGateExternalOIDCWithAdditionalClaimMappings, + features.FeatureGateExternalOIDCWithUpstreamParity, + }, + []configv1.FeatureGateName{ + features.FeatureGateExternalOIDCExternalClaimsSourcing, + }, + ), + }, + { + name: "auth config with extra missing valueExpression", + caBundleConfigMap: &baseCABundleConfigMap, + auth: *authWithUpdates(baseAuthResource, []func(auth *configv1.Authentication){ + func(auth *configv1.Authentication) { + for i := range auth.Spec.OIDCProviders { + auth.Spec.OIDCProviders[i].ClaimMappings.Extra = []configv1.ExtraMapping{ + { + Key: "foo.example.com/bar", + }, + } + } + }, + }), + expectError: true, + featureGates: featuregates.NewFeatureGate( + []configv1.FeatureGateName{ + features.FeatureGateExternalOIDCWithAdditionalClaimMappings, + features.FeatureGateExternalOIDCWithUpstreamParity, + }, + []configv1.FeatureGateName{ + features.FeatureGateExternalOIDCExternalClaimsSourcing, + }, + ), + }, + { + name: "auth config with valid extra mappings", + caBundleConfigMap: &baseCABundleConfigMap, + auth: *authWithUpdates(baseAuthResource, []func(auth *configv1.Authentication){ + func(auth *configv1.Authentication) { + for i := range auth.Spec.OIDCProviders { + auth.Spec.OIDCProviders[i].ClaimMappings.Extra = []configv1.ExtraMapping{ + { + Key: "foo.example.com/bar", + ValueExpression: "claims.bar", + }, + } + } + }, + }), + expectedAuthConfig: authConfigWithUpdates(baseAuthConfig, []func(authConfig *authenticationv1alpha1.AuthenticationConfiguration){ + func(authConfig *authenticationv1alpha1.AuthenticationConfiguration) { + for i := range authConfig.JWT { + authConfig.JWT[i].ClaimMappings.UID.Claim = "sub" + authConfig.JWT[i].ClaimMappings.Extra = []authenticationv1alpha1.ExtraMapping{ + { + Key: "foo.example.com/bar", + ValueExpression: "claims.bar", + }, + } + } + }, + }), + expectError: false, + featureGates: featuregates.NewFeatureGate( + []configv1.FeatureGateName{ + features.FeatureGateExternalOIDCWithAdditionalClaimMappings, + features.FeatureGateExternalOIDCWithUpstreamParity, + }, + []configv1.FeatureGateName{ + features.FeatureGateExternalOIDCExternalClaimsSourcing, + }, + ), + }, + { + name: "invalid discovery URL (http instead of https)", + auth: *authWithUpdates(baseAuthResource, []func(auth *configv1.Authentication){ + func(auth *configv1.Authentication) { + auth.Spec.OIDCProviders[0].Issuer.DiscoveryURL = "http://insecure-url.com" + }, + }), + caBundleConfigMap: &baseCABundleConfigMap, // ensure CA bundle exists + expectError: true, + featureGates: featuregates.NewFeatureGate( + []configv1.FeatureGateName{features.FeatureGateExternalOIDCWithUpstreamParity}, + []configv1.FeatureGateName{ + features.FeatureGateExternalOIDCExternalClaimsSourcing, + }, + ), + }, + { + name: "invalid discovery URL (identical to issuer URL)", + auth: *authWithUpdates(baseAuthResource, []func(auth *configv1.Authentication){ + func(auth *configv1.Authentication) { + auth.Spec.OIDCProviders[0].Issuer.URL = "https://issuer.example.com" + auth.Spec.OIDCProviders[0].Issuer.DiscoveryURL = auth.Spec.OIDCProviders[0].Issuer.URL + }, + }), + caBundleConfigMap: &baseCABundleConfigMap, + expectError: true, + featureGates: featuregates.NewFeatureGate( + []configv1.FeatureGateName{features.FeatureGateExternalOIDCWithUpstreamParity}, + []configv1.FeatureGateName{ + features.FeatureGateExternalOIDCExternalClaimsSourcing, + }, + ), + }, + { + name: "invalid discovery URL (identical to issuer URL except trailing slash)", + auth: *authWithUpdates(baseAuthResource, []func(auth *configv1.Authentication){ + func(auth *configv1.Authentication) { + auth.Spec.OIDCProviders[0].Issuer.URL = "https://issuer.example.com" + auth.Spec.OIDCProviders[0].Issuer.DiscoveryURL = "https://issuer.example.com/" + }, + }), + caBundleConfigMap: &baseCABundleConfigMap, + expectError: true, + featureGates: featuregates.NewFeatureGate( + []configv1.FeatureGateName{features.FeatureGateExternalOIDCWithUpstreamParity}, + []configv1.FeatureGateName{ + features.FeatureGateExternalOIDCExternalClaimsSourcing, + }, + ), + }, + { + name: "invalid discovery URL (missing host)", + auth: *authWithUpdates(baseAuthResource, []func(auth *configv1.Authentication){ + func(auth *configv1.Authentication) { + auth.Spec.OIDCProviders[0].Issuer.URL = "https://issuer.example.com" + auth.Spec.OIDCProviders[0].Issuer.DiscoveryURL = "https:///path" + }, + }), + caBundleConfigMap: &baseCABundleConfigMap, + expectError: true, + featureGates: featuregates.NewFeatureGate( + []configv1.FeatureGateName{features.FeatureGateExternalOIDCWithUpstreamParity}, + []configv1.FeatureGateName{ + features.FeatureGateExternalOIDCExternalClaimsSourcing, + }, + ), + }, + { + name: "invalid discovery URL (contains user info)", + auth: *authWithUpdates(baseAuthResource, []func(auth *configv1.Authentication){ + func(auth *configv1.Authentication) { + auth.Spec.OIDCProviders[0].Issuer.URL = "https://issuer.example.com" + auth.Spec.OIDCProviders[0].Issuer.DiscoveryURL = "https://user@discovery.example.com/path" + }, + }), + caBundleConfigMap: &baseCABundleConfigMap, + expectError: true, + featureGates: featuregates.NewFeatureGate( + []configv1.FeatureGateName{features.FeatureGateExternalOIDCWithUpstreamParity}, + []configv1.FeatureGateName{ + features.FeatureGateExternalOIDCExternalClaimsSourcing, + }, + ), + }, + { + name: "invalid discovery URL (contains query string)", + auth: *authWithUpdates(baseAuthResource, []func(auth *configv1.Authentication){ + func(auth *configv1.Authentication) { + auth.Spec.OIDCProviders[0].Issuer.URL = "https://issuer.example.com" + auth.Spec.OIDCProviders[0].Issuer.DiscoveryURL = "https://discovery.example.com/path?q=1" + }, + }), + caBundleConfigMap: &baseCABundleConfigMap, + expectError: true, + featureGates: featuregates.NewFeatureGate( + []configv1.FeatureGateName{features.FeatureGateExternalOIDCWithUpstreamParity}, + []configv1.FeatureGateName{ + features.FeatureGateExternalOIDCExternalClaimsSourcing, + }, + ), + }, + { + name: "invalid discovery URL (contains fragment)", + auth: *authWithUpdates(baseAuthResource, []func(auth *configv1.Authentication){ + func(auth *configv1.Authentication) { + auth.Spec.OIDCProviders[0].Issuer.URL = "https://issuer.example.com" + auth.Spec.OIDCProviders[0].Issuer.DiscoveryURL = "https://discovery.example.com/path#fragment" + }, + }), + caBundleConfigMap: &baseCABundleConfigMap, + expectError: true, + featureGates: featuregates.NewFeatureGate( + []configv1.FeatureGateName{features.FeatureGateExternalOIDCWithUpstreamParity}, + []configv1.FeatureGateName{ + features.FeatureGateExternalOIDCExternalClaimsSourcing, + }, + ), + }, + { + name: "invalid discovery URL (parse error)", + auth: *authWithUpdates(baseAuthResource, []func(auth *configv1.Authentication){ + func(auth *configv1.Authentication) { + auth.Spec.OIDCProviders[0].Issuer.URL = "https://issuer.example.com" + auth.Spec.OIDCProviders[0].Issuer.DiscoveryURL = "https://%zz" + }, + }), + caBundleConfigMap: &baseCABundleConfigMap, + expectError: true, + featureGates: featuregates.NewFeatureGate( + []configv1.FeatureGateName{features.FeatureGateExternalOIDCWithUpstreamParity}, + []configv1.FeatureGateName{ + features.FeatureGateExternalOIDCExternalClaimsSourcing, + }, + ), + }, + { + name: "user validation rule invalid expression", + auth: *authWithUpdates(baseAuthResource, []func(auth *configv1.Authentication){ + func(auth *configv1.Authentication) { + auth.Spec.OIDCProviders[0].UserValidationRules = []configv1.TokenUserValidationRule{ + { + Expression: "", // invalid: empty expression + Message: "must have a valid expression", + }, + } + }, + }), + expectError: true, + featureGates: featuregates.NewFeatureGate( + []configv1.FeatureGateName{features.FeatureGateExternalOIDCWithUpstreamParity}, + []configv1.FeatureGateName{ + features.FeatureGateExternalOIDCExternalClaimsSourcing, + }, + ), + }, + { + name: "auth config with invalid username expression, error", + caBundleConfigMap: &baseCABundleConfigMap, + auth: *authWithUpdates(baseAuthResource, []func(auth *configv1.Authentication){ + func(auth *configv1.Authentication) { + for i := range auth.Spec.OIDCProviders { + auth.Spec.OIDCProviders[i].ClaimMappings.Username = configv1.UsernameClaimMapping{ + Expression: "#@!$&*(^)", + } + } + }, + }), + expectError: true, + featureGates: featuregates.NewFeatureGate( + []configv1.FeatureGateName{ + features.FeatureGateExternalOIDCWithAdditionalClaimMappings, + features.FeatureGateExternalOIDCWithUpstreamParity, + }, + []configv1.FeatureGateName{ + features.FeatureGateExternalOIDCExternalClaimsSourcing, + }, + ), + }, + { + name: "auth config with invalid groups expression, error", + caBundleConfigMap: &baseCABundleConfigMap, + auth: *authWithUpdates(baseAuthResource, []func(auth *configv1.Authentication){ + func(auth *configv1.Authentication) { + for i := range auth.Spec.OIDCProviders { + auth.Spec.OIDCProviders[i].ClaimMappings.Groups = configv1.PrefixedClaimMapping{ + TokenClaimMapping: configv1.TokenClaimMapping{ + Expression: "#@!$&*(^)", + }, + } + } + }, + }), + expectError: true, + featureGates: featuregates.NewFeatureGate( + []configv1.FeatureGateName{ + features.FeatureGateExternalOIDCWithAdditionalClaimMappings, + features.FeatureGateExternalOIDCWithUpstreamParity, + }, + []configv1.FeatureGateName{ + features.FeatureGateExternalOIDCExternalClaimsSourcing, + }, + ), + }, + { + name: "auth config with username expression mapping", + caBundleConfigMap: &baseCABundleConfigMap, + auth: *authWithUpdates(baseAuthResource, []func(auth *configv1.Authentication){ + func(auth *configv1.Authentication) { + for i := range auth.Spec.OIDCProviders { + auth.Spec.OIDCProviders[i].ClaimMappings.Username = configv1.UsernameClaimMapping{ + Expression: "claims.sub", + } + } + }, + }), + expectedAuthConfig: authConfigWithUpdates(baseAuthConfig, []func(authConfig *authenticationv1alpha1.AuthenticationConfiguration){ + func(authConfig *authenticationv1alpha1.AuthenticationConfiguration) { + for i := range authConfig.JWT { + authConfig.JWT[i].ClaimMappings.Username = authenticationv1alpha1.PrefixedClaimOrExpression{ + Expression: "claims.sub", + } + authConfig.JWT[i].ClaimMappings.UID = authenticationv1alpha1.ClaimOrExpression{ + Claim: "sub", + } + } + }, + }), + expectError: false, + featureGates: featuregates.NewFeatureGate( + []configv1.FeatureGateName{ + features.FeatureGateExternalOIDCWithAdditionalClaimMappings, + features.FeatureGateExternalOIDCWithUpstreamParity, + }, + []configv1.FeatureGateName{ + features.FeatureGateExternalOIDCExternalClaimsSourcing, + }, + ), + }, + { + name: "auth config with groups expression mapping", + caBundleConfigMap: &baseCABundleConfigMap, + auth: *authWithUpdates(baseAuthResource, []func(auth *configv1.Authentication){ + func(auth *configv1.Authentication) { + for i := range auth.Spec.OIDCProviders { + auth.Spec.OIDCProviders[i].ClaimMappings.Groups = configv1.PrefixedClaimMapping{ + TokenClaimMapping: configv1.TokenClaimMapping{ + Expression: "claims.groups", + }, + } + } + }, + }), + expectedAuthConfig: authConfigWithUpdates(baseAuthConfig, []func(authConfig *authenticationv1alpha1.AuthenticationConfiguration){ + func(authConfig *authenticationv1alpha1.AuthenticationConfiguration) { + for i := range authConfig.JWT { + authConfig.JWT[i].ClaimMappings.Groups = authenticationv1alpha1.PrefixedClaimOrExpression{ + Expression: "claims.groups", + } + authConfig.JWT[i].ClaimMappings.UID = authenticationv1alpha1.ClaimOrExpression{ + Claim: "sub", + } + } + }, + }), + expectError: false, + featureGates: featuregates.NewFeatureGate( + []configv1.FeatureGateName{ + features.FeatureGateExternalOIDCWithAdditionalClaimMappings, + features.FeatureGateExternalOIDCWithUpstreamParity, + }, + []configv1.FeatureGateName{ + features.FeatureGateExternalOIDCExternalClaimsSourcing, + }, + ), + }, + { + name: "auth config with username claim and expression both set, error", + caBundleConfigMap: &baseCABundleConfigMap, + auth: *authWithUpdates(baseAuthResource, []func(auth *configv1.Authentication){ + func(auth *configv1.Authentication) { + for i := range auth.Spec.OIDCProviders { + auth.Spec.OIDCProviders[i].ClaimMappings.Username = configv1.UsernameClaimMapping{ + Claim: "username", + Expression: "claims.email", + } + } + }, + }), + expectError: true, + featureGates: featuregates.NewFeatureGate( + []configv1.FeatureGateName{ + features.FeatureGateExternalOIDCWithAdditionalClaimMappings, + features.FeatureGateExternalOIDCWithUpstreamParity, + }, + []configv1.FeatureGateName{ + features.FeatureGateExternalOIDCExternalClaimsSourcing, + }, + ), + }, + { + name: "auth config with groups claim and expression both set, error", + caBundleConfigMap: &baseCABundleConfigMap, + auth: *authWithUpdates(baseAuthResource, []func(auth *configv1.Authentication){ + func(auth *configv1.Authentication) { + for i := range auth.Spec.OIDCProviders { + auth.Spec.OIDCProviders[i].ClaimMappings.Groups = configv1.PrefixedClaimMapping{ + TokenClaimMapping: configv1.TokenClaimMapping{ + Claim: "groups", + Expression: "claims.groups", + }, + } + } + }, + }), + expectError: true, + featureGates: featuregates.NewFeatureGate( + []configv1.FeatureGateName{ + features.FeatureGateExternalOIDCWithAdditionalClaimMappings, + features.FeatureGateExternalOIDCWithUpstreamParity, + }, + []configv1.FeatureGateName{ + features.FeatureGateExternalOIDCExternalClaimsSourcing, + }, + ), + }, + { + name: "auth config with username expression and prefix set, error", + caBundleConfigMap: &baseCABundleConfigMap, + auth: *authWithUpdates(baseAuthResource, []func(auth *configv1.Authentication){ + func(auth *configv1.Authentication) { + for i := range auth.Spec.OIDCProviders { + auth.Spec.OIDCProviders[i].ClaimMappings.Username = configv1.UsernameClaimMapping{ + Expression: "claims.email", + PrefixPolicy: configv1.Prefix, + Prefix: &configv1.UsernamePrefix{ + PrefixString: "oidc-user:", + }, + } + } + }, + }), + expectError: true, + featureGates: featuregates.NewFeatureGate( + []configv1.FeatureGateName{ + features.FeatureGateExternalOIDCWithAdditionalClaimMappings, + features.FeatureGateExternalOIDCWithUpstreamParity, + }, + []configv1.FeatureGateName{ + features.FeatureGateExternalOIDCExternalClaimsSourcing, + }, + ), + }, + { + name: "auth config with groups expression and prefix set, error", + caBundleConfigMap: &baseCABundleConfigMap, + auth: *authWithUpdates(baseAuthResource, []func(auth *configv1.Authentication){ + func(auth *configv1.Authentication) { + for i := range auth.Spec.OIDCProviders { + auth.Spec.OIDCProviders[i].ClaimMappings.Groups = configv1.PrefixedClaimMapping{ + TokenClaimMapping: configv1.TokenClaimMapping{ + Expression: "claims.groups", + }, + Prefix: "oidc-group:", + } + } + }, + }), + expectError: true, + featureGates: featuregates.NewFeatureGate( + []configv1.FeatureGateName{ + features.FeatureGateExternalOIDCWithAdditionalClaimMappings, + features.FeatureGateExternalOIDCWithUpstreamParity, + }, + []configv1.FeatureGateName{ + features.FeatureGateExternalOIDCExternalClaimsSourcing, + }, + ), + }, + { + name: "auth config with username expression using claims.email without claims.email_verified, error", + caBundleConfigMap: &baseCABundleConfigMap, + auth: *authWithUpdates(baseAuthResource, []func(auth *configv1.Authentication){ + func(auth *configv1.Authentication) { + for i := range auth.Spec.OIDCProviders { + auth.Spec.OIDCProviders[i].ClaimMappings.Username = configv1.UsernameClaimMapping{ + Expression: "claims.email", + } + } + }, + }), + expectError: true, + featureGates: featuregates.NewFeatureGate( + []configv1.FeatureGateName{ + features.FeatureGateExternalOIDCWithAdditionalClaimMappings, + features.FeatureGateExternalOIDCWithUpstreamParity, + }, + []configv1.FeatureGateName{ + features.FeatureGateExternalOIDCExternalClaimsSourcing, + }, + ), + }, + { + name: "auth config with username expression using claims.email with claims.email_verified in claimValidationRule, success", + caBundleConfigMap: &baseCABundleConfigMap, + auth: *authWithUpdates(baseAuthResource, []func(auth *configv1.Authentication){ + func(auth *configv1.Authentication) { + for i := range auth.Spec.OIDCProviders { + auth.Spec.OIDCProviders[i].ClaimMappings.Username = configv1.UsernameClaimMapping{ + Expression: "claims.email", + } + auth.Spec.OIDCProviders[i].ClaimValidationRules = []configv1.TokenClaimValidationRule{ + { + Type: configv1.TokenValidationRuleTypeCEL, + CEL: configv1.TokenClaimValidationCELRule{ + Expression: "claims.email_verified == true", + Message: "email must be verified", + }, + }, + } + } + }, + }), + expectedAuthConfig: authConfigWithUpdates(baseAuthConfig, []func(authConfig *authenticationv1alpha1.AuthenticationConfiguration){ + func(authConfig *authenticationv1alpha1.AuthenticationConfiguration) { + for i := range authConfig.JWT { + authConfig.JWT[i].ClaimMappings.Username = authenticationv1alpha1.PrefixedClaimOrExpression{ + Expression: "claims.email", + } + authConfig.JWT[i].ClaimMappings.UID = authenticationv1alpha1.ClaimOrExpression{ + Claim: "sub", + } + authConfig.JWT[i].ClaimValidationRules = []authenticationv1alpha1.ClaimValidationRule{ + { + Expression: "claims.email_verified == true", + Message: "email must be verified", + }, + } + } + }, + }), + expectError: false, + featureGates: featuregates.NewFeatureGate( + []configv1.FeatureGateName{ + features.FeatureGateExternalOIDCWithAdditionalClaimMappings, + features.FeatureGateExternalOIDCWithUpstreamParity, + }, + []configv1.FeatureGateName{ + features.FeatureGateExternalOIDCExternalClaimsSourcing, + }, + ), + }, + { + name: "auth config with username expression using both claims.email and claims.email_verified, success", + caBundleConfigMap: &baseCABundleConfigMap, + auth: *authWithUpdates(baseAuthResource, []func(auth *configv1.Authentication){ + func(auth *configv1.Authentication) { + for i := range auth.Spec.OIDCProviders { + auth.Spec.OIDCProviders[i].ClaimMappings.Username = configv1.UsernameClaimMapping{ + Expression: "claims.email_verified ? claims.email : 'unverified'", + } + } + }, + }), + expectedAuthConfig: authConfigWithUpdates(baseAuthConfig, []func(authConfig *authenticationv1alpha1.AuthenticationConfiguration){ + func(authConfig *authenticationv1alpha1.AuthenticationConfiguration) { + for i := range authConfig.JWT { + authConfig.JWT[i].ClaimMappings.Username = authenticationv1alpha1.PrefixedClaimOrExpression{ + Expression: "claims.email_verified ? claims.email : 'unverified'", + } + authConfig.JWT[i].ClaimMappings.UID = authenticationv1alpha1.ClaimOrExpression{ + Claim: "sub", + } + } + }, + }), + expectError: false, + featureGates: featuregates.NewFeatureGate( + []configv1.FeatureGateName{ + features.FeatureGateExternalOIDCWithAdditionalClaimMappings, + features.FeatureGateExternalOIDCWithUpstreamParity, + }, + []configv1.FeatureGateName{ + features.FeatureGateExternalOIDCExternalClaimsSourcing, + }, + ), + }, + { + name: "auth config with username expression using claims.email with claims.email_verified in extra, success", + caBundleConfigMap: &baseCABundleConfigMap, + auth: *authWithUpdates(baseAuthResource, []func(auth *configv1.Authentication){ + func(auth *configv1.Authentication) { + for i := range auth.Spec.OIDCProviders { + auth.Spec.OIDCProviders[i].ClaimMappings.Username = configv1.UsernameClaimMapping{ + Expression: "claims.email", + } + auth.Spec.OIDCProviders[i].ClaimMappings.Extra = []configv1.ExtraMapping{ + { + Key: "example.com/email-verified", + ValueExpression: "claims.email_verified ? 'true' : 'false'", + }, + } + auth.Spec.OIDCProviders[i].ClaimValidationRules = []configv1.TokenClaimValidationRule{} + } + }, + }), + expectedAuthConfig: authConfigWithUpdates(baseAuthConfig, []func(authConfig *authenticationv1alpha1.AuthenticationConfiguration){ + func(authConfig *authenticationv1alpha1.AuthenticationConfiguration) { + for i := range authConfig.JWT { + authConfig.JWT[i].ClaimMappings.Username = authenticationv1alpha1.PrefixedClaimOrExpression{ + Expression: "claims.email", + } + authConfig.JWT[i].ClaimMappings.UID = authenticationv1alpha1.ClaimOrExpression{ + Claim: "sub", + } + authConfig.JWT[i].ClaimMappings.Extra = []authenticationv1alpha1.ExtraMapping{ + { + Key: "example.com/email-verified", + ValueExpression: "claims.email_verified ? 'true' : 'false'", + }, + } + authConfig.JWT[i].ClaimValidationRules = []authenticationv1alpha1.ClaimValidationRule{} + } + }, + }), + expectError: false, + featureGates: featuregates.NewFeatureGate( + []configv1.FeatureGateName{ + features.FeatureGateExternalOIDCWithAdditionalClaimMappings, + features.FeatureGateExternalOIDCWithUpstreamParity, + }, + []configv1.FeatureGateName{ + features.FeatureGateExternalOIDCExternalClaimsSourcing, + }, + ), + }, + { + name: "valid auth config with external claims source using request provided token auth and conditions, success", + caBundleConfigMap: &baseCABundleConfigMap, + configMapIndexer: func() cache.Indexer { + idx := cache.NewIndexer(cache.MetaNamespaceKeyFunc, cache.Indexers{}) + idx.Add(&corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "ext-source-ca-bundle", + Namespace: configNamespace, + }, + Data: map[string]string{ + "ca-bundle.crt": testCertData, + }, + }) + return idx + }(), + auth: *authWithUpdates(baseAuthResource, []func(auth *configv1.Authentication){ + func(auth *configv1.Authentication) { + for i := range auth.Spec.OIDCProviders { + auth.Spec.OIDCProviders[i].Issuer.URL = "https://example.com" + auth.Spec.OIDCProviders[i].ExternalClaimsSources = []configv1.ExternalClaimsSource{ + { + Authentication: configv1.ExternalSourceAuthentication{ + Type: configv1.ExternalSourceAuthenticationTypeRequestProvidedToken, + }, + TLS: configv1.ExternalSourceTLS{ + CertificateAuthority: configv1.ExternalSourceCertificateAuthorityConfigMapReference{ + Name: "ext-source-ca-bundle", + }, + }, + URL: configv1.SourceURL{ + Hostname: "claims.example.com", + PathExpression: "claims.sub", + }, + Mappings: []configv1.SourcedClaimMapping{ + { + Name: "custom_claim", + Expression: "response.custom_claim", + }, + }, + Predicates: []configv1.ExternalSourcePredicate{ + { + Expression: "has(claims.sub)", + }, + }, + }, + } + } + }, + }), + expectedAuthConfig: authConfigWithUpdates(baseAuthConfig, []func(authConfig *authenticationv1alpha1.AuthenticationConfiguration){ + func(authConfig *authenticationv1alpha1.AuthenticationConfiguration) { + for i := range authConfig.JWT { + authConfig.JWT[i].Issuer.URL = "https://example.com" + authConfig.JWT[i].ExternalClaimsSources = []authenticationv1alpha1.ExternalClaimsSource{ + { + Authentication: &authenticationv1alpha1.Authentication{ + Type: ptr.To(authenticationv1alpha1.AuthenticationTypeRequestProvidedToken), + }, + TLS: &authenticationv1alpha1.TLS{ + CertificateAuthority: ptr.To(testCertData), + }, + URL: &authenticationv1alpha1.SourceURL{ + Hostname: ptr.To("claims.example.com"), + PathExpression: ptr.To("claims.sub"), + }, + Mappings: []authenticationv1alpha1.SourcedClaimMapping{ + { + Name: ptr.To("custom_claim"), + Expression: ptr.To("response.custom_claim"), + }, + }, + Conditions: []authenticationv1alpha1.ExternalSourceCondition{ + { + Expression: ptr.To("has(claims.sub)"), + }, + }, + }, + } + } + }, + }), + expectError: false, + featureGates: featuregates.NewFeatureGate( + []configv1.FeatureGateName{ + features.FeatureGateExternalOIDCExternalClaimsSourcing, + }, + []configv1.FeatureGateName{ + features.FeatureGateExternalOIDCWithAdditionalClaimMappings, + features.FeatureGateExternalOIDCWithUpstreamParity, + }, + ), + }, + { + name: "valid auth config with external claims source using anonymous auth, success", + caBundleConfigMap: &baseCABundleConfigMap, + configMapIndexer: func() cache.Indexer { + idx := cache.NewIndexer(cache.MetaNamespaceKeyFunc, cache.Indexers{}) + idx.Add(&corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "ext-source-ca-bundle", + Namespace: configNamespace, + }, + Data: map[string]string{ + "ca-bundle.crt": testCertData, + }, + }) + return idx + }(), + auth: *authWithUpdates(baseAuthResource, []func(auth *configv1.Authentication){ + func(auth *configv1.Authentication) { + for i := range auth.Spec.OIDCProviders { + auth.Spec.OIDCProviders[i].Issuer.URL = "https://example.com" + auth.Spec.OIDCProviders[i].ExternalClaimsSources = []configv1.ExternalClaimsSource{ + { + TLS: configv1.ExternalSourceTLS{ + CertificateAuthority: configv1.ExternalSourceCertificateAuthorityConfigMapReference{ + Name: "ext-source-ca-bundle", + }, + }, + URL: configv1.SourceURL{ + Hostname: "claims.example.com", + PathExpression: "claims.sub", + }, + Mappings: []configv1.SourcedClaimMapping{ + { + Name: "custom_claim", + Expression: "response.custom_claim", + }, + }, + }, + } + } + }, + }), + expectedAuthConfig: authConfigWithUpdates(baseAuthConfig, []func(authConfig *authenticationv1alpha1.AuthenticationConfiguration){ + func(authConfig *authenticationv1alpha1.AuthenticationConfiguration) { + for i := range authConfig.JWT { + authConfig.JWT[i].Issuer.URL = "https://example.com" + authConfig.JWT[i].ExternalClaimsSources = []authenticationv1alpha1.ExternalClaimsSource{ + { + TLS: &authenticationv1alpha1.TLS{ + CertificateAuthority: ptr.To(testCertData), + }, + URL: &authenticationv1alpha1.SourceURL{ + Hostname: ptr.To("claims.example.com"), + PathExpression: ptr.To("claims.sub"), + }, + Mappings: []authenticationv1alpha1.SourcedClaimMapping{ + { + Name: ptr.To("custom_claim"), + Expression: ptr.To("response.custom_claim"), + }, + }, + }, + } + } + }, + }), + expectError: false, + featureGates: featuregates.NewFeatureGate( + []configv1.FeatureGateName{ + features.FeatureGateExternalOIDCExternalClaimsSourcing, + }, + []configv1.FeatureGateName{ + features.FeatureGateExternalOIDCWithAdditionalClaimMappings, + features.FeatureGateExternalOIDCWithUpstreamParity, + }, + ), + }, + { + name: "valid auth config with external claims source using client credential auth", + caBundleConfigMap: &baseCABundleConfigMap, + configMapIndexer: func() cache.Indexer { + idx := cache.NewIndexer(cache.MetaNamespaceKeyFunc, cache.Indexers{}) + idx.Add(&corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "ext-source-ca-bundle", + Namespace: configNamespace, + }, + Data: map[string]string{ + "ca-bundle.crt": testCertData, + }, + }) + idx.Add(&corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "cc-tls-ca-bundle", + Namespace: configNamespace, + }, + Data: map[string]string{ + "ca-bundle.crt": testCertData, + }, + }) + return idx + }(), + secretIndexer: func() cache.Indexer { + idx := cache.NewIndexer(cache.MetaNamespaceKeyFunc, cache.Indexers{}) + idx.Add(&corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "client-secret-ref", + Namespace: configNamespace, + }, + Data: map[string][]byte{ + "client-secret": []byte("my-secret-value"), + }, + }) + return idx + }(), + auth: *authWithUpdates(baseAuthResource, []func(auth *configv1.Authentication){ + func(auth *configv1.Authentication) { + for i := range auth.Spec.OIDCProviders { + auth.Spec.OIDCProviders[i].Issuer.URL = "https://example.com" + auth.Spec.OIDCProviders[i].ExternalClaimsSources = []configv1.ExternalClaimsSource{ + { + Authentication: configv1.ExternalSourceAuthentication{ + Type: configv1.ExternalSourceAuthenticationTypeClientCredential, + ClientCredential: configv1.ClientCredentialConfig{ + ClientID: "my-client-id", + ClientSecret: configv1.ClientSecretSecretReference{ + Name: "client-secret-ref", + }, + TokenEndpoint: "https://idp.example.com/oauth2/token", + Scopes: []configv1.OAuth2Scope{"openid", "profile"}, + TLS: configv1.ExternalSourceTLS{ + CertificateAuthority: configv1.ExternalSourceCertificateAuthorityConfigMapReference{ + Name: "cc-tls-ca-bundle", + }, + }, + }, + }, + TLS: configv1.ExternalSourceTLS{ + CertificateAuthority: configv1.ExternalSourceCertificateAuthorityConfigMapReference{ + Name: "ext-source-ca-bundle", + }, + }, + URL: configv1.SourceURL{ + Hostname: "claims.example.com", + PathExpression: "claims.sub", + }, + Mappings: []configv1.SourcedClaimMapping{ + { + Name: "custom_claim", + Expression: "response.custom_claim", + }, + }, + }, + } + } + }, + }), + expectedAuthConfig: authConfigWithUpdates(baseAuthConfig, []func(authConfig *authenticationv1alpha1.AuthenticationConfiguration){ + func(authConfig *authenticationv1alpha1.AuthenticationConfiguration) { + for i := range authConfig.JWT { + authConfig.JWT[i].Issuer.URL = "https://example.com" + authConfig.JWT[i].ExternalClaimsSources = []authenticationv1alpha1.ExternalClaimsSource{ + { + Authentication: &authenticationv1alpha1.Authentication{ + Type: ptr.To(authenticationv1alpha1.AuthenticationTypeClientCredential), + ClientCredential: &authenticationv1alpha1.ClientCredentialConfig{ + ClientID: "my-client-id", + ClientSecret: "my-secret-value", + TokenEndpoint: "https://idp.example.com/oauth2/token", + Scopes: []string{"openid", "profile"}, + TLS: &authenticationv1alpha1.TLS{ + CertificateAuthority: ptr.To(testCertData), + }, + }, + }, + TLS: &authenticationv1alpha1.TLS{ + CertificateAuthority: ptr.To(testCertData), + }, + URL: &authenticationv1alpha1.SourceURL{ + Hostname: ptr.To("claims.example.com"), + PathExpression: ptr.To("claims.sub"), + }, + Mappings: []authenticationv1alpha1.SourcedClaimMapping{ + { + Name: ptr.To("custom_claim"), + Expression: ptr.To("response.custom_claim"), + }, + }, + }, + } + } + }, + }), + expectError: false, + featureGates: featuregates.NewFeatureGate( + []configv1.FeatureGateName{ + features.FeatureGateExternalOIDCExternalClaimsSourcing, + }, + []configv1.FeatureGateName{ + features.FeatureGateExternalOIDCWithAdditionalClaimMappings, + features.FeatureGateExternalOIDCWithUpstreamParity, + }, + ), + }, + { + name: "auth config with external claims source with unknown auth type, error", + caBundleConfigMap: &baseCABundleConfigMap, + auth: *authWithUpdates(baseAuthResource, []func(auth *configv1.Authentication){ + func(auth *configv1.Authentication) { + for i := range auth.Spec.OIDCProviders { + auth.Spec.OIDCProviders[i].Issuer.URL = "https://example.com" + auth.Spec.OIDCProviders[i].ExternalClaimsSources = []configv1.ExternalClaimsSource{ + { + Authentication: configv1.ExternalSourceAuthentication{ + Type: configv1.ExternalSourceAuthenticationType("UnknownType"), + }, + TLS: configv1.ExternalSourceTLS{ + CertificateAuthority: configv1.ExternalSourceCertificateAuthorityConfigMapReference{ + Name: "ext-source-ca-bundle", + }, + }, + URL: configv1.SourceURL{ + Hostname: "claims.example.com", + PathExpression: "claims.sub", + }, + Mappings: []configv1.SourcedClaimMapping{ + { + Name: "custom_claim", + Expression: "response.custom_claim", + }, + }, + }, + } + } + }, + }), + expectError: true, + featureGates: featuregates.NewFeatureGate( + []configv1.FeatureGateName{ + features.FeatureGateExternalOIDCExternalClaimsSourcing, + }, + []configv1.FeatureGateName{ + features.FeatureGateExternalOIDCWithAdditionalClaimMappings, + features.FeatureGateExternalOIDCWithUpstreamParity, + }, + ), + }, + { + name: "auth config with external claims source with missing TLS CA configmap, error", + caBundleConfigMap: &baseCABundleConfigMap, + auth: *authWithUpdates(baseAuthResource, []func(auth *configv1.Authentication){ + func(auth *configv1.Authentication) { + for i := range auth.Spec.OIDCProviders { + auth.Spec.OIDCProviders[i].Issuer.URL = "https://example.com" + auth.Spec.OIDCProviders[i].ExternalClaimsSources = []configv1.ExternalClaimsSource{ + { + Authentication: configv1.ExternalSourceAuthentication{ + Type: configv1.ExternalSourceAuthenticationTypeRequestProvidedToken, + }, + TLS: configv1.ExternalSourceTLS{ + CertificateAuthority: configv1.ExternalSourceCertificateAuthorityConfigMapReference{ + Name: "nonexistent-ca-bundle", + }, + }, + URL: configv1.SourceURL{ + Hostname: "claims.example.com", + PathExpression: "claims.sub", + }, + Mappings: []configv1.SourcedClaimMapping{ + { + Name: "custom_claim", + Expression: "response.custom_claim", + }, + }, + }, + } + } + }, + }), + expectError: true, + featureGates: featuregates.NewFeatureGate( + []configv1.FeatureGateName{ + features.FeatureGateExternalOIDCExternalClaimsSourcing, + }, + []configv1.FeatureGateName{ + features.FeatureGateExternalOIDCWithAdditionalClaimMappings, + features.FeatureGateExternalOIDCWithUpstreamParity, + }, + ), + }, + { + name: "auth config with external claims source with client secret key missing in secret, error", + caBundleConfigMap: &baseCABundleConfigMap, + secretIndexer: func() cache.Indexer { + idx := cache.NewIndexer(cache.MetaNamespaceKeyFunc, cache.Indexers{}) + idx.Add(&corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "client-secret-ref", + Namespace: configNamespace, + }, + Data: map[string][]byte{ + "wrong-key": []byte("my-secret-value"), + }, + }) + return idx + }(), + auth: *authWithUpdates(baseAuthResource, []func(auth *configv1.Authentication){ + func(auth *configv1.Authentication) { + for i := range auth.Spec.OIDCProviders { + auth.Spec.OIDCProviders[i].Issuer.URL = "https://example.com" + auth.Spec.OIDCProviders[i].ExternalClaimsSources = []configv1.ExternalClaimsSource{ + { + Authentication: configv1.ExternalSourceAuthentication{ + Type: configv1.ExternalSourceAuthenticationTypeClientCredential, + ClientCredential: configv1.ClientCredentialConfig{ + ClientID: "my-client-id", + ClientSecret: configv1.ClientSecretSecretReference{ + Name: "client-secret-ref", + }, + TokenEndpoint: "https://idp.example.com/oauth2/token", + }, + }, + TLS: configv1.ExternalSourceTLS{ + CertificateAuthority: configv1.ExternalSourceCertificateAuthorityConfigMapReference{ + Name: "ext-source-ca-bundle", + }, + }, + URL: configv1.SourceURL{ + Hostname: "claims.example.com", + PathExpression: "claims.sub", + }, + Mappings: []configv1.SourcedClaimMapping{ + { + Name: "custom_claim", + Expression: "response.custom_claim", + }, + }, + }, + } + } + }, + }), + expectError: true, + featureGates: featuregates.NewFeatureGate( + []configv1.FeatureGateName{ + features.FeatureGateExternalOIDCExternalClaimsSourcing, + }, + []configv1.FeatureGateName{ + features.FeatureGateExternalOIDCWithAdditionalClaimMappings, + features.FeatureGateExternalOIDCWithUpstreamParity, + }, + ), + }, + + { + name: "auth config with external claims source configured but feature gate disabled", + caBundleConfigMap: &baseCABundleConfigMap, + auth: *authWithUpdates(baseAuthResource, []func(auth *configv1.Authentication){ + func(auth *configv1.Authentication) { + for i := range auth.Spec.OIDCProviders { + auth.Spec.OIDCProviders[i].Issuer.URL = "https://example.com" + auth.Spec.OIDCProviders[i].ExternalClaimsSources = []configv1.ExternalClaimsSource{ + { + Authentication: configv1.ExternalSourceAuthentication{ + Type: configv1.ExternalSourceAuthenticationTypeRequestProvidedToken, + }, + }, + } + } + }, + }), + expectedAuthConfig: authConfigWithUpdates(baseAuthConfig, []func(authConfig *authenticationv1alpha1.AuthenticationConfiguration){ + func(authConfig *authenticationv1alpha1.AuthenticationConfiguration) { + for i := range authConfig.JWT { + authConfig.JWT[i].Issuer.URL = "https://example.com" + } + }, + }), + expectError: false, + featureGates: featuregates.NewFeatureGate( + []configv1.FeatureGateName{}, + []configv1.FeatureGateName{ + features.FeatureGateExternalOIDCWithAdditionalClaimMappings, + features.FeatureGateExternalOIDCWithUpstreamParity, + features.FeatureGateExternalOIDCExternalClaimsSourcing, + }, + ), + }, + // TODO: Add tests for validating currently unvalidated fields due to dependency issues (CEL expression validation) + /* + { + name: "auth config with duplicate mapping names across external claims sources", + caBundleConfigMap: &baseCABundleConfigMap, + configMapIndexer: func() cache.Indexer { + idx := cache.NewIndexer(cache.MetaNamespaceKeyFunc, cache.Indexers{}) + idx.Add(&corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "ext-source-ca-bundle", + Namespace: configNamespace, + }, + Data: map[string]string{ + "ca-bundle.crt": testCertData, + }, + }) + return idx + }(), + auth: *authWithUpdates(baseAuthResource, []func(auth *configv1.Authentication){ + func(auth *configv1.Authentication) { + for i := range auth.Spec.OIDCProviders { + auth.Spec.OIDCProviders[i].Issuer.URL = "https://example.com" + auth.Spec.OIDCProviders[i].ExternalClaimsSources = []configv1.ExternalClaimsSource{ + { + Authentication: configv1.ExternalSourceAuthentication{ + Type: configv1.ExternalSourceAuthenticationTypeRequestProvidedToken, + }, + TLS: configv1.ExternalSourceTLS{ + CertificateAuthority: configv1.ExternalSourceCertificateAuthorityConfigMapReference{ + Name: "ext-source-ca-bundle", + }, + }, + URL: configv1.SourceURL{ + Hostname: "source-one.example.com", + PathExpression: "claims.sub", + }, + Mappings: []configv1.SourcedClaimMapping{ + { + Name: "custom_claim", + Expression: "response.custom_claim", + }, + }, + }, + { + Authentication: configv1.ExternalSourceAuthentication{ + Type: configv1.ExternalSourceAuthenticationTypeRequestProvidedToken, + }, + TLS: configv1.ExternalSourceTLS{ + CertificateAuthority: configv1.ExternalSourceCertificateAuthorityConfigMapReference{ + Name: "ext-source-ca-bundle", + }, + }, + URL: configv1.SourceURL{ + Hostname: "source-two.example.com", + PathExpression: "claims.sub", + }, + Mappings: []configv1.SourcedClaimMapping{ + { + Name: "custom_claim", + Expression: "response.other_claim", + }, + }, + }, + } + } + }, + }), + expectError: true, + featureGates: featuregates.NewFeatureGate( + []configv1.FeatureGateName{ + features.FeatureGateExternalOIDCExternalClaimsSourcing, + }, + []configv1.FeatureGateName{ + features.FeatureGateExternalOIDCWithAdditionalClaimMappings, + features.FeatureGateExternalOIDCWithUpstreamParity, + }, + ), + }, + */ + } { + t.Run(tt.name, func(t *testing.T) { + if tt.configMapIndexer == nil { + tt.configMapIndexer = cache.NewIndexer(cache.MetaNamespaceKeyFunc, cache.Indexers{}) + } + + if tt.caBundleConfigMap != nil { + tt.configMapIndexer.Add(tt.caBundleConfigMap) + } + + c := NewAuthenticationConfigurationGenerator(corev1listers.NewConfigMapLister(tt.configMapIndexer), corev1listers.NewSecretLister(tt.secretIndexer), tt.featureGates) + c.validationFn = tt.configValidator + + gotConfig, err := c.GenerateAuthenticationConfiguration(&tt.auth) + if tt.expectError && err == nil { + t.Fatalf("expected error but didn't get any") + } + + if !tt.expectError && err != nil { + t.Fatalf("did not expect any error but got: %v", err) + } + + if gotConfig == nil && tt.expectedAuthConfig == nil { + return + } + + if diff := cmp.Diff(tt.expectedAuthConfig, gotConfig, cmpopts.EquateEmpty()); diff != "" { + t.Fatalf("unexpected config diff: %s", diff) + } + }) + } +} + +func newAuthWithSpec(spec configv1.AuthenticationSpec) *configv1.Authentication { + return &configv1.Authentication{ + ObjectMeta: metav1.ObjectMeta{ + Name: "cluster", + }, + Spec: spec, + } +} + +func authWithUpdates(auth configv1.Authentication, updateFuncs []func(auth *configv1.Authentication)) *configv1.Authentication { + copy := auth.DeepCopy() + for _, updateFunc := range updateFuncs { + updateFunc(copy) + } + return copy +} + +func authConfigWithUpdates(authConfig authenticationv1alpha1.AuthenticationConfiguration, updateFuncs []func(authConfig *authenticationv1alpha1.AuthenticationConfiguration)) *authenticationv1alpha1.AuthenticationConfiguration { + copy := authConfig.DeepCopy() + for _, updateFunc := range updateFuncs { + updateFunc(copy) + } + return copy +} + +type everFailingIndexer struct{} + +// Index always returns an error +func (i *everFailingIndexer) Index(indexName string, obj interface{}) ([]interface{}, error) { + return nil, fmt.Errorf("Index method not implemented") +} + +// IndexKeys always returns an error +func (i *everFailingIndexer) IndexKeys(indexName, indexedValue string) ([]string, error) { + return nil, fmt.Errorf("IndexKeys method not implemented") +} + +// ListIndexFuncValues always returns an error +func (i *everFailingIndexer) ListIndexFuncValues(indexName string) []string { + return nil +} + +// ByIndex always returns an error +func (i *everFailingIndexer) ByIndex(indexName, indexedValue string) ([]interface{}, error) { + return nil, fmt.Errorf("ByIndex method not implemented") +} + +// GetIndexers always returns an error +func (i *everFailingIndexer) GetIndexers() cache.Indexers { + return nil +} + +// AddIndexers always returns an error +func (i *everFailingIndexer) AddIndexers(newIndexers cache.Indexers) error { + return fmt.Errorf("AddIndexers method not implemented") +} + +// Add always returns an error +func (s *everFailingIndexer) Add(obj interface{}) error { + return fmt.Errorf("Add method not implemented") +} + +// Update always returns an error +func (s *everFailingIndexer) Update(obj interface{}) error { + return fmt.Errorf("Update method not implemented") +} + +// Delete always returns an error +func (s *everFailingIndexer) Delete(obj interface{}) error { + return fmt.Errorf("Delete method not implemented") +} + +// List always returns nil +func (s *everFailingIndexer) List() []interface{} { + return nil +} + +// ListKeys always returns nil +func (s *everFailingIndexer) ListKeys() []string { + return nil +} + +// Get always returns an error +func (s *everFailingIndexer) Get(obj interface{}) (item interface{}, exists bool, err error) { + return nil, false, fmt.Errorf("Get method not implemented") +} + +// GetByKey always returns an error +func (s *everFailingIndexer) GetByKey(key string) (item interface{}, exists bool, err error) { + return nil, false, fmt.Errorf("GetByKey method not implemented") +} + +// Replace always returns an error +func (s *everFailingIndexer) Replace(objects []interface{}, sKey string) error { + return fmt.Errorf("Replace method not implemented") +} + +// Resync always returns an error +func (s *everFailingIndexer) Resync() error { + return fmt.Errorf("Resync method not implemented") +} + +var ( + baseCACert, baseCAPrivateKey, validateTestCertData = func() (*x509.Certificate, crypto.Signer, string) { + cert, key, err := generateCAKeyPair() + if err != nil { + panic(err) + } + return cert, key, string(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw})) + }() + + validateBaseAuthConfig = authenticationv1alpha1.AuthenticationConfiguration{ + TypeMeta: metav1.TypeMeta{ + Kind: kindAuthenticationConfiguration, + APIVersion: authenticationv1alpha1.SchemeGroupVersion.String(), + }, + JWT: []authenticationv1alpha1.JWTAuthenticator{ + { + Issuer: &authenticationv1alpha1.Issuer{ + Audiences: []string{"my-test-aud", "another-aud"}, + CertificateAuthority: validateTestCertData, + AudienceMatchPolicy: authenticationv1alpha1.AudienceMatchPolicyMatchAny, + }, + ClaimMappings: &authenticationv1alpha1.ClaimMappings{ + Username: authenticationv1alpha1.PrefixedClaimOrExpression{ + Claim: "username", + Prefix: ptr.To("oidc-user:"), + }, + Groups: authenticationv1alpha1.PrefixedClaimOrExpression{ + Claim: "groups", + Prefix: ptr.To("oidc-group:"), + }, + }, + ClaimValidationRules: []authenticationv1alpha1.ClaimValidationRule{ + { + Claim: "username", + RequiredValue: "test-username", + }, + { + Claim: "email", + RequiredValue: "test-email", + }, + }, + }, + }, + } +) + +func TestValidateOAuthApiserverAuthenticationConfiguration(t *testing.T) { + testServer, err := createTestServer(baseCACert, baseCAPrivateKey, nil) + if err != nil { + t.Fatalf("could not create test server: %v", err) + } + defer testServer.Close() + testServer.StartTLS() + + for _, tt := range []struct { + name string + authConfig *authenticationv1alpha1.AuthenticationConfiguration + expectError bool + }{ + { + name: "empty config", + authConfig: &authenticationv1alpha1.AuthenticationConfiguration{}, + expectError: false, + }, + { + name: "nil config", + authConfig: nil, + expectError: false, + }, + { + name: "issuer with empty URL", + authConfig: authConfigWithUpdates(validateBaseAuthConfig, []func(authConfig *authenticationv1alpha1.AuthenticationConfiguration){ + func(authConfig *authenticationv1alpha1.AuthenticationConfiguration) { + authConfig.JWT[0].Issuer.URL = "" + }, + }), + expectError: true, + }, + { + name: "issuer with http URL", + authConfig: authConfigWithUpdates(validateBaseAuthConfig, []func(authConfig *authenticationv1alpha1.AuthenticationConfiguration){ + func(authConfig *authenticationv1alpha1.AuthenticationConfiguration) { + authConfig.JWT[0].Issuer.URL = "http://insecure.com" + }, + }), + expectError: true, + }, + { + name: "issuer with invalid CA", + authConfig: authConfigWithUpdates(validateBaseAuthConfig, []func(authConfig *authenticationv1alpha1.AuthenticationConfiguration){ + func(authConfig *authenticationv1alpha1.AuthenticationConfiguration) { + authConfig.JWT[0].Issuer.CertificateAuthority = "invalid CA" + }, + }), + expectError: true, + }, + { + name: "valid auth config", + authConfig: authConfigWithUpdates(validateBaseAuthConfig, []func(authConfig *authenticationv1alpha1.AuthenticationConfiguration){ + func(authConfig *authenticationv1alpha1.AuthenticationConfiguration) { + authConfig.JWT[0].Issuer.URL = testServer.URL + }, + }), + expectError: false, + }, + } { + t.Run(tt.name, func(t *testing.T) { + err := validateOAuthApiserverAuthenticationConfiguration(tt.authConfig) + if tt.expectError && err == nil { + t.Errorf("expected error but didn't get any") + } + + if !tt.expectError && err != nil { + t.Errorf("did not expect any error but got: %v", err) + } + }) + } +} + +func createTestServer(caCert *x509.Certificate, caPrivateKey crypto.Signer, handlerFunc http.HandlerFunc) (*httptest.Server, error) { + cert := caCert + key := caPrivateKey + var err error + if caCert == nil { + cert, key, err = generateCAKeyPair() + if err != nil { + return nil, err + } + } + + servingCertPair, err := generateServingCert(cert, key) + if err != nil { + return nil, err + } + + if handlerFunc == nil { + handlerFunc = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + }) + } + + testServer := httptest.NewUnstartedServer(handlerFunc) + testServer.TLS = &tls.Config{ + Certificates: []tls.Certificate{*servingCertPair}, + } + + return testServer, nil +} + +func generateCAKeyPair() (*x509.Certificate, crypto.Signer, error) { + caPrivateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + return nil, nil, err + } + + caCert, err := certutil.NewSelfSignedCACert(certutil.Config{CommonName: "test-ca"}, caPrivateKey) + if err != nil { + return nil, nil, err + } + + return caCert, caPrivateKey, err +} + +func generateServingCert(caCert *x509.Certificate, caPrivateKey crypto.Signer) (*tls.Certificate, error) { + cert := &x509.Certificate{ + SerialNumber: big.NewInt(2024), + Subject: pkix.Name{ + Organization: []string{"Company, INC."}, + Country: []string{"US"}, + Province: []string{""}, + Locality: []string{"Springfield"}, + StreetAddress: []string{"742 Evergreen Terrace"}, + }, + IPAddresses: []net.IP{net.IPv4(127, 0, 0, 1), net.IPv6loopback}, + NotBefore: time.Now(), + NotAfter: time.Now().AddDate(10, 0, 0), + SubjectKeyId: []byte{1, 2, 3, 4, 6}, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth}, + KeyUsage: x509.KeyUsageDigitalSignature, + } + + certPrivKey, err := rsa.GenerateKey(rand.Reader, 4096) + if err != nil { + return nil, err + } + + certBytes, err := x509.CreateCertificate(rand.Reader, cert, caCert, &certPrivKey.PublicKey, caPrivateKey) + if err != nil { + return nil, err + } + + certPEM := new(bytes.Buffer) + err = pem.Encode(certPEM, &pem.Block{ + Type: "CERTIFICATE", + Bytes: certBytes, + }) + if err != nil { + return nil, fmt.Errorf("PEM encoding certificate: %w", err) + } + + certPrivKeyPEM := new(bytes.Buffer) + err = pem.Encode(certPrivKeyPEM, &pem.Block{ + Type: "RSA PRIVATE KEY", + Bytes: x509.MarshalPKCS1PrivateKey(certPrivKey), + }) + if err != nil { + return nil, fmt.Errorf("PEM encoding private key: %w", err) + } + + serverCert, err := tls.X509KeyPair(certPEM.Bytes(), certPrivKeyPEM.Bytes()) + if err != nil { + return nil, err + } + + return &serverCert, nil +} diff --git a/vendor/github.com/google/go-cmp/cmp/cmpopts/equate.go b/vendor/github.com/google/go-cmp/cmp/cmpopts/equate.go new file mode 100644 index 0000000000..3d8d0cd3ae --- /dev/null +++ b/vendor/github.com/google/go-cmp/cmp/cmpopts/equate.go @@ -0,0 +1,185 @@ +// Copyright 2017, The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package cmpopts provides common options for the cmp package. +package cmpopts + +import ( + "errors" + "fmt" + "math" + "reflect" + "time" + + "github.com/google/go-cmp/cmp" +) + +func equateAlways(_, _ interface{}) bool { return true } + +// EquateEmpty returns a [cmp.Comparer] option that determines all maps and slices +// with a length of zero to be equal, regardless of whether they are nil. +// +// EquateEmpty can be used in conjunction with [SortSlices] and [SortMaps]. +func EquateEmpty() cmp.Option { + return cmp.FilterValues(isEmpty, cmp.Comparer(equateAlways)) +} + +func isEmpty(x, y interface{}) bool { + vx, vy := reflect.ValueOf(x), reflect.ValueOf(y) + return (x != nil && y != nil && vx.Type() == vy.Type()) && + (vx.Kind() == reflect.Slice || vx.Kind() == reflect.Map) && + (vx.Len() == 0 && vy.Len() == 0) +} + +// EquateApprox returns a [cmp.Comparer] option that determines float32 or float64 +// values to be equal if they are within a relative fraction or absolute margin. +// This option is not used when either x or y is NaN or infinite. +// +// The fraction determines that the difference of two values must be within the +// smaller fraction of the two values, while the margin determines that the two +// values must be within some absolute margin. +// To express only a fraction or only a margin, use 0 for the other parameter. +// The fraction and margin must be non-negative. +// +// The mathematical expression used is equivalent to: +// +// |x-y| ≤ max(fraction*min(|x|, |y|), margin) +// +// EquateApprox can be used in conjunction with [EquateNaNs]. +func EquateApprox(fraction, margin float64) cmp.Option { + if margin < 0 || fraction < 0 || math.IsNaN(margin) || math.IsNaN(fraction) { + panic("margin or fraction must be a non-negative number") + } + a := approximator{fraction, margin} + return cmp.Options{ + cmp.FilterValues(areRealF64s, cmp.Comparer(a.compareF64)), + cmp.FilterValues(areRealF32s, cmp.Comparer(a.compareF32)), + } +} + +type approximator struct{ frac, marg float64 } + +func areRealF64s(x, y float64) bool { + return !math.IsNaN(x) && !math.IsNaN(y) && !math.IsInf(x, 0) && !math.IsInf(y, 0) +} +func areRealF32s(x, y float32) bool { + return areRealF64s(float64(x), float64(y)) +} +func (a approximator) compareF64(x, y float64) bool { + relMarg := a.frac * math.Min(math.Abs(x), math.Abs(y)) + return math.Abs(x-y) <= math.Max(a.marg, relMarg) +} +func (a approximator) compareF32(x, y float32) bool { + return a.compareF64(float64(x), float64(y)) +} + +// EquateNaNs returns a [cmp.Comparer] option that determines float32 and float64 +// NaN values to be equal. +// +// EquateNaNs can be used in conjunction with [EquateApprox]. +func EquateNaNs() cmp.Option { + return cmp.Options{ + cmp.FilterValues(areNaNsF64s, cmp.Comparer(equateAlways)), + cmp.FilterValues(areNaNsF32s, cmp.Comparer(equateAlways)), + } +} + +func areNaNsF64s(x, y float64) bool { + return math.IsNaN(x) && math.IsNaN(y) +} +func areNaNsF32s(x, y float32) bool { + return areNaNsF64s(float64(x), float64(y)) +} + +// EquateApproxTime returns a [cmp.Comparer] option that determines two non-zero +// [time.Time] values to be equal if they are within some margin of one another. +// If both times have a monotonic clock reading, then the monotonic time +// difference will be used. The margin must be non-negative. +func EquateApproxTime(margin time.Duration) cmp.Option { + if margin < 0 { + panic("margin must be a non-negative number") + } + a := timeApproximator{margin} + return cmp.FilterValues(areNonZeroTimes, cmp.Comparer(a.compare)) +} + +func areNonZeroTimes(x, y time.Time) bool { + return !x.IsZero() && !y.IsZero() +} + +type timeApproximator struct { + margin time.Duration +} + +func (a timeApproximator) compare(x, y time.Time) bool { + // Avoid subtracting times to avoid overflow when the + // difference is larger than the largest representable duration. + if x.After(y) { + // Ensure x is always before y + x, y = y, x + } + // We're within the margin if x+margin >= y. + // Note: time.Time doesn't have AfterOrEqual method hence the negation. + return !x.Add(a.margin).Before(y) +} + +// AnyError is an error that matches any non-nil error. +var AnyError anyError + +type anyError struct{} + +func (anyError) Error() string { return "any error" } +func (anyError) Is(err error) bool { return err != nil } + +// EquateErrors returns a [cmp.Comparer] option that determines errors to be equal +// if [errors.Is] reports them to match. The [AnyError] error can be used to +// match any non-nil error. +func EquateErrors() cmp.Option { + return cmp.FilterValues(areConcreteErrors, cmp.Comparer(compareErrors)) +} + +// areConcreteErrors reports whether x and y are types that implement error. +// The input types are deliberately of the interface{} type rather than the +// error type so that we can handle situations where the current type is an +// interface{}, but the underlying concrete types both happen to implement +// the error interface. +func areConcreteErrors(x, y interface{}) bool { + _, ok1 := x.(error) + _, ok2 := y.(error) + return ok1 && ok2 +} + +func compareErrors(x, y interface{}) bool { + xe := x.(error) + ye := y.(error) + return errors.Is(xe, ye) || errors.Is(ye, xe) +} + +// EquateComparable returns a [cmp.Option] that determines equality +// of comparable types by directly comparing them using the == operator in Go. +// The types to compare are specified by passing a value of that type. +// This option should only be used on types that are documented as being +// safe for direct == comparison. For example, [net/netip.Addr] is documented +// as being semantically safe to use with ==, while [time.Time] is documented +// to discourage the use of == on time values. +func EquateComparable(typs ...interface{}) cmp.Option { + types := make(typesFilter) + for _, typ := range typs { + switch t := reflect.TypeOf(typ); { + case !t.Comparable(): + panic(fmt.Sprintf("%T is not a comparable Go type", typ)) + case types[t]: + panic(fmt.Sprintf("%T is already specified", typ)) + default: + types[t] = true + } + } + return cmp.FilterPath(types.filter, cmp.Comparer(equateAny)) +} + +type typesFilter map[reflect.Type]bool + +func (tf typesFilter) filter(p cmp.Path) bool { return tf[p.Last().Type()] } + +func equateAny(x, y interface{}) bool { return x == y } diff --git a/vendor/github.com/google/go-cmp/cmp/cmpopts/ignore.go b/vendor/github.com/google/go-cmp/cmp/cmpopts/ignore.go new file mode 100644 index 0000000000..fb84d11d70 --- /dev/null +++ b/vendor/github.com/google/go-cmp/cmp/cmpopts/ignore.go @@ -0,0 +1,206 @@ +// Copyright 2017, The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package cmpopts + +import ( + "fmt" + "reflect" + "unicode" + "unicode/utf8" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/internal/function" +) + +// IgnoreFields returns an [cmp.Option] that ignores fields of the +// given names on a single struct type. It respects the names of exported fields +// that are forwarded due to struct embedding. +// The struct type is specified by passing in a value of that type. +// +// The name may be a dot-delimited string (e.g., "Foo.Bar") to ignore a +// specific sub-field that is embedded or nested within the parent struct. +func IgnoreFields(typ interface{}, names ...string) cmp.Option { + sf := newStructFilter(typ, names...) + return cmp.FilterPath(sf.filter, cmp.Ignore()) +} + +// IgnoreTypes returns an [cmp.Option] that ignores all values assignable to +// certain types, which are specified by passing in a value of each type. +func IgnoreTypes(typs ...interface{}) cmp.Option { + tf := newTypeFilter(typs...) + return cmp.FilterPath(tf.filter, cmp.Ignore()) +} + +type typeFilter []reflect.Type + +func newTypeFilter(typs ...interface{}) (tf typeFilter) { + for _, typ := range typs { + t := reflect.TypeOf(typ) + if t == nil { + // This occurs if someone tries to pass in sync.Locker(nil) + panic("cannot determine type; consider using IgnoreInterfaces") + } + tf = append(tf, t) + } + return tf +} +func (tf typeFilter) filter(p cmp.Path) bool { + if len(p) < 1 { + return false + } + t := p.Last().Type() + for _, ti := range tf { + if t.AssignableTo(ti) { + return true + } + } + return false +} + +// IgnoreInterfaces returns an [cmp.Option] that ignores all values or references of +// values assignable to certain interface types. These interfaces are specified +// by passing in an anonymous struct with the interface types embedded in it. +// For example, to ignore [sync.Locker], pass in struct{sync.Locker}{}. +func IgnoreInterfaces(ifaces interface{}) cmp.Option { + tf := newIfaceFilter(ifaces) + return cmp.FilterPath(tf.filter, cmp.Ignore()) +} + +type ifaceFilter []reflect.Type + +func newIfaceFilter(ifaces interface{}) (tf ifaceFilter) { + t := reflect.TypeOf(ifaces) + if ifaces == nil || t.Name() != "" || t.Kind() != reflect.Struct { + panic("input must be an anonymous struct") + } + for i := 0; i < t.NumField(); i++ { + fi := t.Field(i) + switch { + case !fi.Anonymous: + panic("struct cannot have named fields") + case fi.Type.Kind() != reflect.Interface: + panic("embedded field must be an interface type") + case fi.Type.NumMethod() == 0: + // This matches everything; why would you ever want this? + panic("cannot ignore empty interface") + default: + tf = append(tf, fi.Type) + } + } + return tf +} +func (tf ifaceFilter) filter(p cmp.Path) bool { + if len(p) < 1 { + return false + } + t := p.Last().Type() + for _, ti := range tf { + if t.AssignableTo(ti) { + return true + } + if t.Kind() != reflect.Ptr && reflect.PtrTo(t).AssignableTo(ti) { + return true + } + } + return false +} + +// IgnoreUnexported returns an [cmp.Option] that only ignores the immediate unexported +// fields of a struct, including anonymous fields of unexported types. +// In particular, unexported fields within the struct's exported fields +// of struct types, including anonymous fields, will not be ignored unless the +// type of the field itself is also passed to IgnoreUnexported. +// +// Avoid ignoring unexported fields of a type which you do not control (i.e. a +// type from another repository), as changes to the implementation of such types +// may change how the comparison behaves. Prefer a custom [cmp.Comparer] instead. +func IgnoreUnexported(typs ...interface{}) cmp.Option { + ux := newUnexportedFilter(typs...) + return cmp.FilterPath(ux.filter, cmp.Ignore()) +} + +type unexportedFilter struct{ m map[reflect.Type]bool } + +func newUnexportedFilter(typs ...interface{}) unexportedFilter { + ux := unexportedFilter{m: make(map[reflect.Type]bool)} + for _, typ := range typs { + t := reflect.TypeOf(typ) + if t == nil || t.Kind() != reflect.Struct { + panic(fmt.Sprintf("%T must be a non-pointer struct", typ)) + } + ux.m[t] = true + } + return ux +} +func (xf unexportedFilter) filter(p cmp.Path) bool { + sf, ok := p.Index(-1).(cmp.StructField) + if !ok { + return false + } + return xf.m[p.Index(-2).Type()] && !isExported(sf.Name()) +} + +// isExported reports whether the identifier is exported. +func isExported(id string) bool { + r, _ := utf8.DecodeRuneInString(id) + return unicode.IsUpper(r) +} + +// IgnoreSliceElements returns an [cmp.Option] that ignores elements of []V. +// The discard function must be of the form "func(T) bool" which is used to +// ignore slice elements of type V, where V is assignable to T. +// Elements are ignored if the function reports true. +func IgnoreSliceElements(discardFunc interface{}) cmp.Option { + vf := reflect.ValueOf(discardFunc) + if !function.IsType(vf.Type(), function.ValuePredicate) || vf.IsNil() { + panic(fmt.Sprintf("invalid discard function: %T", discardFunc)) + } + return cmp.FilterPath(func(p cmp.Path) bool { + si, ok := p.Index(-1).(cmp.SliceIndex) + if !ok { + return false + } + if !si.Type().AssignableTo(vf.Type().In(0)) { + return false + } + vx, vy := si.Values() + if vx.IsValid() && vf.Call([]reflect.Value{vx})[0].Bool() { + return true + } + if vy.IsValid() && vf.Call([]reflect.Value{vy})[0].Bool() { + return true + } + return false + }, cmp.Ignore()) +} + +// IgnoreMapEntries returns an [cmp.Option] that ignores entries of map[K]V. +// The discard function must be of the form "func(T, R) bool" which is used to +// ignore map entries of type K and V, where K and V are assignable to T and R. +// Entries are ignored if the function reports true. +func IgnoreMapEntries(discardFunc interface{}) cmp.Option { + vf := reflect.ValueOf(discardFunc) + if !function.IsType(vf.Type(), function.KeyValuePredicate) || vf.IsNil() { + panic(fmt.Sprintf("invalid discard function: %T", discardFunc)) + } + return cmp.FilterPath(func(p cmp.Path) bool { + mi, ok := p.Index(-1).(cmp.MapIndex) + if !ok { + return false + } + if !mi.Key().Type().AssignableTo(vf.Type().In(0)) || !mi.Type().AssignableTo(vf.Type().In(1)) { + return false + } + k := mi.Key() + vx, vy := mi.Values() + if vx.IsValid() && vf.Call([]reflect.Value{k, vx})[0].Bool() { + return true + } + if vy.IsValid() && vf.Call([]reflect.Value{k, vy})[0].Bool() { + return true + } + return false + }, cmp.Ignore()) +} diff --git a/vendor/github.com/google/go-cmp/cmp/cmpopts/sort.go b/vendor/github.com/google/go-cmp/cmp/cmpopts/sort.go new file mode 100644 index 0000000000..720f3cdf57 --- /dev/null +++ b/vendor/github.com/google/go-cmp/cmp/cmpopts/sort.go @@ -0,0 +1,171 @@ +// Copyright 2017, The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package cmpopts + +import ( + "fmt" + "reflect" + "sort" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/internal/function" +) + +// SortSlices returns a [cmp.Transformer] option that sorts all []V. +// The lessOrCompareFunc function must be either +// a less function of the form "func(T, T) bool" or +// a compare function of the format "func(T, T) int" +// which is used to sort any slice with element type V that is assignable to T. +// +// A less function must be: +// - Deterministic: less(x, y) == less(x, y) +// - Irreflexive: !less(x, x) +// - Transitive: if !less(x, y) and !less(y, z), then !less(x, z) +// +// A compare function must be: +// - Deterministic: compare(x, y) == compare(x, y) +// - Irreflexive: compare(x, x) == 0 +// - Transitive: if !less(x, y) and !less(y, z), then !less(x, z) +// +// The function does not have to be "total". That is, if x != y, but +// less or compare report inequality, their relative order is maintained. +// +// SortSlices can be used in conjunction with [EquateEmpty]. +func SortSlices(lessOrCompareFunc interface{}) cmp.Option { + vf := reflect.ValueOf(lessOrCompareFunc) + if (!function.IsType(vf.Type(), function.Less) && !function.IsType(vf.Type(), function.Compare)) || vf.IsNil() { + panic(fmt.Sprintf("invalid less or compare function: %T", lessOrCompareFunc)) + } + ss := sliceSorter{vf.Type().In(0), vf} + return cmp.FilterValues(ss.filter, cmp.Transformer("cmpopts.SortSlices", ss.sort)) +} + +type sliceSorter struct { + in reflect.Type // T + fnc reflect.Value // func(T, T) bool +} + +func (ss sliceSorter) filter(x, y interface{}) bool { + vx, vy := reflect.ValueOf(x), reflect.ValueOf(y) + if !(x != nil && y != nil && vx.Type() == vy.Type()) || + !(vx.Kind() == reflect.Slice && vx.Type().Elem().AssignableTo(ss.in)) || + (vx.Len() <= 1 && vy.Len() <= 1) { + return false + } + // Check whether the slices are already sorted to avoid an infinite + // recursion cycle applying the same transform to itself. + ok1 := sort.SliceIsSorted(x, func(i, j int) bool { return ss.less(vx, i, j) }) + ok2 := sort.SliceIsSorted(y, func(i, j int) bool { return ss.less(vy, i, j) }) + return !ok1 || !ok2 +} +func (ss sliceSorter) sort(x interface{}) interface{} { + src := reflect.ValueOf(x) + dst := reflect.MakeSlice(src.Type(), src.Len(), src.Len()) + for i := 0; i < src.Len(); i++ { + dst.Index(i).Set(src.Index(i)) + } + sort.SliceStable(dst.Interface(), func(i, j int) bool { return ss.less(dst, i, j) }) + ss.checkSort(dst) + return dst.Interface() +} +func (ss sliceSorter) checkSort(v reflect.Value) { + start := -1 // Start of a sequence of equal elements. + for i := 1; i < v.Len(); i++ { + if ss.less(v, i-1, i) { + // Check that first and last elements in v[start:i] are equal. + if start >= 0 && (ss.less(v, start, i-1) || ss.less(v, i-1, start)) { + panic(fmt.Sprintf("incomparable values detected: want equal elements: %v", v.Slice(start, i))) + } + start = -1 + } else if start == -1 { + start = i + } + } +} +func (ss sliceSorter) less(v reflect.Value, i, j int) bool { + vx, vy := v.Index(i), v.Index(j) + vo := ss.fnc.Call([]reflect.Value{vx, vy})[0] + if vo.Kind() == reflect.Bool { + return vo.Bool() + } else { + return vo.Int() < 0 + } +} + +// SortMaps returns a [cmp.Transformer] option that flattens map[K]V types to be +// a sorted []struct{K, V}. The lessOrCompareFunc function must be either +// a less function of the form "func(T, T) bool" or +// a compare function of the format "func(T, T) int" +// which is used to sort any map with key K that is assignable to T. +// +// Flattening the map into a slice has the property that [cmp.Equal] is able to +// use [cmp.Comparer] options on K or the K.Equal method if it exists. +// +// A less function must be: +// - Deterministic: less(x, y) == less(x, y) +// - Irreflexive: !less(x, x) +// - Transitive: if !less(x, y) and !less(y, z), then !less(x, z) +// - Total: if x != y, then either less(x, y) or less(y, x) +// +// A compare function must be: +// - Deterministic: compare(x, y) == compare(x, y) +// - Irreflexive: compare(x, x) == 0 +// - Transitive: if compare(x, y) < 0 and compare(y, z) < 0, then compare(x, z) < 0 +// - Total: if x != y, then compare(x, y) != 0 +// +// SortMaps can be used in conjunction with [EquateEmpty]. +func SortMaps(lessOrCompareFunc interface{}) cmp.Option { + vf := reflect.ValueOf(lessOrCompareFunc) + if (!function.IsType(vf.Type(), function.Less) && !function.IsType(vf.Type(), function.Compare)) || vf.IsNil() { + panic(fmt.Sprintf("invalid less or compare function: %T", lessOrCompareFunc)) + } + ms := mapSorter{vf.Type().In(0), vf} + return cmp.FilterValues(ms.filter, cmp.Transformer("cmpopts.SortMaps", ms.sort)) +} + +type mapSorter struct { + in reflect.Type // T + fnc reflect.Value // func(T, T) bool +} + +func (ms mapSorter) filter(x, y interface{}) bool { + vx, vy := reflect.ValueOf(x), reflect.ValueOf(y) + return (x != nil && y != nil && vx.Type() == vy.Type()) && + (vx.Kind() == reflect.Map && vx.Type().Key().AssignableTo(ms.in)) && + (vx.Len() != 0 || vy.Len() != 0) +} +func (ms mapSorter) sort(x interface{}) interface{} { + src := reflect.ValueOf(x) + outType := reflect.StructOf([]reflect.StructField{ + {Name: "K", Type: src.Type().Key()}, + {Name: "V", Type: src.Type().Elem()}, + }) + dst := reflect.MakeSlice(reflect.SliceOf(outType), src.Len(), src.Len()) + for i, k := range src.MapKeys() { + v := reflect.New(outType).Elem() + v.Field(0).Set(k) + v.Field(1).Set(src.MapIndex(k)) + dst.Index(i).Set(v) + } + sort.Slice(dst.Interface(), func(i, j int) bool { return ms.less(dst, i, j) }) + ms.checkSort(dst) + return dst.Interface() +} +func (ms mapSorter) checkSort(v reflect.Value) { + for i := 1; i < v.Len(); i++ { + if !ms.less(v, i-1, i) { + panic(fmt.Sprintf("partial order detected: want %v < %v", v.Index(i-1), v.Index(i))) + } + } +} +func (ms mapSorter) less(v reflect.Value, i, j int) bool { + vx, vy := v.Index(i).Field(0), v.Index(j).Field(0) + vo := ms.fnc.Call([]reflect.Value{vx, vy})[0] + if vo.Kind() == reflect.Bool { + return vo.Bool() + } else { + return vo.Int() < 0 + } +} diff --git a/vendor/github.com/google/go-cmp/cmp/cmpopts/struct_filter.go b/vendor/github.com/google/go-cmp/cmp/cmpopts/struct_filter.go new file mode 100644 index 0000000000..ca11a40249 --- /dev/null +++ b/vendor/github.com/google/go-cmp/cmp/cmpopts/struct_filter.go @@ -0,0 +1,189 @@ +// Copyright 2017, The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package cmpopts + +import ( + "fmt" + "reflect" + "strings" + + "github.com/google/go-cmp/cmp" +) + +// filterField returns a new Option where opt is only evaluated on paths that +// include a specific exported field on a single struct type. +// The struct type is specified by passing in a value of that type. +// +// The name may be a dot-delimited string (e.g., "Foo.Bar") to select a +// specific sub-field that is embedded or nested within the parent struct. +func filterField(typ interface{}, name string, opt cmp.Option) cmp.Option { + // TODO: This is currently unexported over concerns of how helper filters + // can be composed together easily. + // TODO: Add tests for FilterField. + + sf := newStructFilter(typ, name) + return cmp.FilterPath(sf.filter, opt) +} + +type structFilter struct { + t reflect.Type // The root struct type to match on + ft fieldTree // Tree of fields to match on +} + +func newStructFilter(typ interface{}, names ...string) structFilter { + // TODO: Perhaps allow * as a special identifier to allow ignoring any + // number of path steps until the next field match? + // This could be useful when a concrete struct gets transformed into + // an anonymous struct where it is not possible to specify that by type, + // but the transformer happens to provide guarantees about the names of + // the transformed fields. + + t := reflect.TypeOf(typ) + if t == nil || t.Kind() != reflect.Struct { + panic(fmt.Sprintf("%T must be a non-pointer struct", typ)) + } + var ft fieldTree + for _, name := range names { + cname, err := canonicalName(t, name) + if err != nil { + panic(fmt.Sprintf("%s: %v", strings.Join(cname, "."), err)) + } + ft.insert(cname) + } + return structFilter{t, ft} +} + +func (sf structFilter) filter(p cmp.Path) bool { + for i, ps := range p { + if ps.Type().AssignableTo(sf.t) && sf.ft.matchPrefix(p[i+1:]) { + return true + } + } + return false +} + +// fieldTree represents a set of dot-separated identifiers. +// +// For example, inserting the following selectors: +// +// Foo +// Foo.Bar.Baz +// Foo.Buzz +// Nuka.Cola.Quantum +// +// Results in a tree of the form: +// +// {sub: { +// "Foo": {ok: true, sub: { +// "Bar": {sub: { +// "Baz": {ok: true}, +// }}, +// "Buzz": {ok: true}, +// }}, +// "Nuka": {sub: { +// "Cola": {sub: { +// "Quantum": {ok: true}, +// }}, +// }}, +// }} +type fieldTree struct { + ok bool // Whether this is a specified node + sub map[string]fieldTree // The sub-tree of fields under this node +} + +// insert inserts a sequence of field accesses into the tree. +func (ft *fieldTree) insert(cname []string) { + if ft.sub == nil { + ft.sub = make(map[string]fieldTree) + } + if len(cname) == 0 { + ft.ok = true + return + } + sub := ft.sub[cname[0]] + sub.insert(cname[1:]) + ft.sub[cname[0]] = sub +} + +// matchPrefix reports whether any selector in the fieldTree matches +// the start of path p. +func (ft fieldTree) matchPrefix(p cmp.Path) bool { + for _, ps := range p { + switch ps := ps.(type) { + case cmp.StructField: + ft = ft.sub[ps.Name()] + if ft.ok { + return true + } + if len(ft.sub) == 0 { + return false + } + case cmp.Indirect: + default: + return false + } + } + return false +} + +// canonicalName returns a list of identifiers where any struct field access +// through an embedded field is expanded to include the names of the embedded +// types themselves. +// +// For example, suppose field "Foo" is not directly in the parent struct, +// but actually from an embedded struct of type "Bar". Then, the canonical name +// of "Foo" is actually "Bar.Foo". +// +// Suppose field "Foo" is not directly in the parent struct, but actually +// a field in two different embedded structs of types "Bar" and "Baz". +// Then the selector "Foo" causes a panic since it is ambiguous which one it +// refers to. The user must specify either "Bar.Foo" or "Baz.Foo". +func canonicalName(t reflect.Type, sel string) ([]string, error) { + var name string + sel = strings.TrimPrefix(sel, ".") + if sel == "" { + return nil, fmt.Errorf("name must not be empty") + } + if i := strings.IndexByte(sel, '.'); i < 0 { + name, sel = sel, "" + } else { + name, sel = sel[:i], sel[i:] + } + + // Type must be a struct or pointer to struct. + if t.Kind() == reflect.Ptr { + t = t.Elem() + } + if t.Kind() != reflect.Struct { + return nil, fmt.Errorf("%v must be a struct", t) + } + + // Find the canonical name for this current field name. + // If the field exists in an embedded struct, then it will be expanded. + sf, _ := t.FieldByName(name) + if !isExported(name) { + // Avoid using reflect.Type.FieldByName for unexported fields due to + // buggy behavior with regard to embeddeding and unexported fields. + // See https://golang.org/issue/4876 for details. + sf = reflect.StructField{} + for i := 0; i < t.NumField() && sf.Name == ""; i++ { + if t.Field(i).Name == name { + sf = t.Field(i) + } + } + } + if sf.Name == "" { + return []string{name}, fmt.Errorf("does not exist") + } + var ss []string + for i := range sf.Index { + ss = append(ss, t.FieldByIndex(sf.Index[:i+1]).Name) + } + if sel == "" { + return ss, nil + } + ssPost, err := canonicalName(sf.Type, sel) + return append(ss, ssPost...), err +} diff --git a/vendor/github.com/google/go-cmp/cmp/cmpopts/xform.go b/vendor/github.com/google/go-cmp/cmp/cmpopts/xform.go new file mode 100644 index 0000000000..25b4bd05bd --- /dev/null +++ b/vendor/github.com/google/go-cmp/cmp/cmpopts/xform.go @@ -0,0 +1,36 @@ +// Copyright 2018, The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package cmpopts + +import ( + "github.com/google/go-cmp/cmp" +) + +type xformFilter struct{ xform cmp.Option } + +func (xf xformFilter) filter(p cmp.Path) bool { + for _, ps := range p { + if t, ok := ps.(cmp.Transform); ok && t.Option() == xf.xform { + return false + } + } + return true +} + +// AcyclicTransformer returns a [cmp.Transformer] with a filter applied that ensures +// that the transformer cannot be recursively applied upon its own output. +// +// An example use case is a transformer that splits a string by lines: +// +// AcyclicTransformer("SplitLines", func(s string) []string{ +// return strings.Split(s, "\n") +// }) +// +// Had this been an unfiltered [cmp.Transformer] instead, this would result in an +// infinite cycle converting a string to []string to [][]string and so on. +func AcyclicTransformer(name string, xformFunc interface{}) cmp.Option { + xf := xformFilter{cmp.Transformer(name, xformFunc)} + return cmp.FilterPath(xf.filter, xf.xform) +} diff --git a/vendor/github.com/openshift-eng/openshift-tests-extension/pkg/cmd/cmdrun/runsuite.go b/vendor/github.com/openshift-eng/openshift-tests-extension/pkg/cmd/cmdrun/runsuite.go index 7d9453da17..eafa30c061 100644 --- a/vendor/github.com/openshift-eng/openshift-tests-extension/pkg/cmd/cmdrun/runsuite.go +++ b/vendor/github.com/openshift-eng/openshift-tests-extension/pkg/cmd/cmdrun/runsuite.go @@ -24,11 +24,13 @@ func NewRunSuiteCommand(registry *extension.Registry) *cobra.Command { outputFlags *flags.OutputFlags concurrencyFlags *flags.ConcurrencyFlags junitPath string + htmlPath string }{ componentFlags: flags.NewComponentFlags(), outputFlags: flags.NewOutputFlags(), concurrencyFlags: flags.NewConcurrencyFlags(), junitPath: "", + htmlPath: "", } cmd := &cobra.Command{ @@ -90,6 +92,14 @@ func NewRunSuiteCommand(registry *extension.Registry) *cobra.Command { } compositeWriter.AddWriter(junitWriter) } + // HTML writer if needed + if opts.htmlPath != "" { + htmlWriter, err := extensiontests.NewHTMLResultWriter(opts.htmlPath, suite.Name) + if err != nil { + return errors.Wrap(err, "couldn't create html writer") + } + compositeWriter.AddWriter(htmlWriter) + } // JSON writer jsonWriter, err := extensiontests.NewJSONResultWriter(os.Stdout, @@ -104,7 +114,19 @@ func NewRunSuiteCommand(registry *extension.Registry) *cobra.Command { return errors.Wrap(err, "couldn't filter specs") } - results, runErr := specs.Run(ctx, compositeWriter, min(opts.concurrencyFlags.MaxConcurency, suite.Parallelism)) + if suite.TestTimeout != nil { + for _, spec := range specs { + if spec.Timeout == 0 { + spec.Timeout = *suite.TestTimeout + } + } + } + + concurrency := opts.concurrencyFlags.MaxConcurency + if suite.Parallelism > 0 { + concurrency = min(concurrency, suite.Parallelism) + } + results, runErr := specs.Run(ctx, compositeWriter, concurrency) if opts.junitPath != "" { // we want to commit the results to disk regardless of the success or failure of the specs if err := writeResults(opts.junitPath, results); err != nil { @@ -118,6 +140,7 @@ func NewRunSuiteCommand(registry *extension.Registry) *cobra.Command { opts.outputFlags.BindFlags(cmd.Flags()) opts.concurrencyFlags.BindFlags(cmd.Flags()) cmd.Flags().StringVarP(&opts.junitPath, "junit-path", "j", opts.junitPath, "write results to junit XML") + cmd.Flags().StringVar(&opts.htmlPath, "html-path", opts.htmlPath, "write results to summary HTML") return cmd } diff --git a/vendor/github.com/openshift-eng/openshift-tests-extension/pkg/extension/extensiontests/result.go b/vendor/github.com/openshift-eng/openshift-tests-extension/pkg/extension/extensiontests/result.go index 2e36969fe6..9c03a0a84b 100644 --- a/vendor/github.com/openshift-eng/openshift-tests-extension/pkg/extension/extensiontests/result.go +++ b/vendor/github.com/openshift-eng/openshift-tests-extension/pkg/extension/extensiontests/result.go @@ -1,8 +1,12 @@ package extensiontests import ( + "bytes" + _ "embed" + "encoding/json" "fmt" "strings" + "text/template" "github.com/openshift-eng/openshift-tests-extension/pkg/junit" ) @@ -67,3 +71,55 @@ func (results ExtensionTestResults) ToJUnit(suiteName string) junit.TestSuite { return suite } + +//go:embed viewer.html +var viewerHtml []byte + +// RenderResultsHTML renders the HTML viewer template with the provided JSON data. +// The caller is responsible for marshaling their results to JSON. This allows +// callers with different result struct types to use the same HTML viewer. +func RenderResultsHTML(jsonData []byte, suiteName string) ([]byte, error) { + tmpl, err := template.New("viewer").Parse(string(viewerHtml)) + if err != nil { + return nil, fmt.Errorf("failed to parse template: %w", err) + } + var out bytes.Buffer + if err := tmpl.Execute(&out, struct { + Data string + SuiteName string + }{ + string(jsonData), + suiteName, + }); err != nil { + return nil, fmt.Errorf("failed to execute template: %w", err) + } + return out.Bytes(), nil +} + +func (results ExtensionTestResults) ToHTML(suiteName string) ([]byte, error) { + encoded, err := json.Marshal(results) + if err != nil { + return nil, fmt.Errorf("failed to marshal extension test results: %w", err) + } + // pare down the output if there's a lot, we want this to load in some reasonable amount of time + if len(encoded) > 2<<20 { + // n.b. this is wasteful, but we want to mutate our inputs in a safe manner, so the encode/decode/encode + // pass is useful as a deep copy + var copiedResults ExtensionTestResults + if err := json.Unmarshal(encoded, &copiedResults); err != nil { + return nil, fmt.Errorf("failed to unmarshal extension test results: %w", err) + } + copiedResults.Walk(func(result *ExtensionTestResult) { + if result.Result == ResultPassed { + result.Error = "" + result.Output = "" + result.Details = nil + } + }) + encoded, err = json.Marshal(copiedResults) + if err != nil { + return nil, fmt.Errorf("failed to marshal extension test results: %w", err) + } + } + return RenderResultsHTML(encoded, suiteName) +} diff --git a/vendor/github.com/openshift-eng/openshift-tests-extension/pkg/extension/extensiontests/result_writer.go b/vendor/github.com/openshift-eng/openshift-tests-extension/pkg/extension/extensiontests/result_writer.go index aedc409c17..f9ca434cad 100644 --- a/vendor/github.com/openshift-eng/openshift-tests-extension/pkg/extension/extensiontests/result_writer.go +++ b/vendor/github.com/openshift-eng/openshift-tests-extension/pkg/extension/extensiontests/result_writer.go @@ -124,8 +124,9 @@ func NewJSONResultWriter(out io.Writer, format ResultFormat) (*JSONResultWriter, } return &JSONResultWriter{ - out: out, - format: format, + out: out, + format: format, + results: ExtensionTestResults{}, }, nil } @@ -162,3 +163,51 @@ func (w *JSONResultWriter) Flush() error { return nil } + +type HTMLResultWriter struct { + lock sync.Mutex + testSuite *junit.TestSuite + out *os.File + suiteName string + path string + results ExtensionTestResults +} + +func NewHTMLResultWriter(path, suiteName string) (ResultWriter, error) { + file, err := os.Create(path) + if err != nil { + return nil, err + } + + return &HTMLResultWriter{ + testSuite: &junit.TestSuite{ + Name: suiteName, + }, + out: file, + suiteName: suiteName, + path: path, + }, nil +} + +func (w *HTMLResultWriter) Write(res *ExtensionTestResult) { + w.lock.Lock() + defer w.lock.Unlock() + w.results = append(w.results, res) +} + +func (w *HTMLResultWriter) Flush() error { + w.lock.Lock() + defer w.lock.Unlock() + data, err := w.results.ToHTML(w.suiteName) + if err != nil { + return fmt.Errorf("failed to create result HTML: %w", err) + } + if _, err := w.out.Write(data); err != nil { + return err + } + if err := w.out.Close(); err != nil { + return err + } + + return nil +} \ No newline at end of file diff --git a/vendor/github.com/openshift-eng/openshift-tests-extension/pkg/extension/extensiontests/types.go b/vendor/github.com/openshift-eng/openshift-tests-extension/pkg/extension/extensiontests/types.go index cd23be81ff..f3edf41a6e 100644 --- a/vendor/github.com/openshift-eng/openshift-tests-extension/pkg/extension/extensiontests/types.go +++ b/vendor/github.com/openshift-eng/openshift-tests-extension/pkg/extension/extensiontests/types.go @@ -2,6 +2,7 @@ package extensiontests import ( "context" + "time" "github.com/openshift-eng/openshift-tests-extension/pkg/dbtime" "github.com/openshift-eng/openshift-tests-extension/pkg/util/sets" @@ -59,6 +60,10 @@ type ExtensionTestSpec struct { // to the `ote-binary run-test "test name"` commmand and interpretting the result. RunParallel func(ctx context.Context) *ExtensionTestResult `json:"-"` + // Timeout is the maximum duration for this test. If set, it overrides the default 90-minute + // timeout used by SpawnProcessToRunTest. This is typically populated from Suite.TestTimeout. + Timeout time.Duration `json:"-"` + // Hook functions afterAll []*OneTimeTask beforeAll []*OneTimeTask diff --git a/vendor/github.com/openshift-eng/openshift-tests-extension/pkg/extension/extensiontests/viewer.html b/vendor/github.com/openshift-eng/openshift-tests-extension/pkg/extension/extensiontests/viewer.html new file mode 100644 index 0000000000..2ff236aa32 --- /dev/null +++ b/vendor/github.com/openshift-eng/openshift-tests-extension/pkg/extension/extensiontests/viewer.html @@ -0,0 +1,1520 @@ + + + + + + Results for {{ .SuiteName }} + + + +
+
+

+ + + + + Results for {{ .SuiteName }} +

+

No file loaded

+
+ +
+

Load Test Results

+

Drag and drop a JSON test results file here, or click to browse

+ +
+ + + + +
+ + + + + diff --git a/vendor/github.com/openshift-eng/openshift-tests-extension/pkg/ginkgo/logging.go b/vendor/github.com/openshift-eng/openshift-tests-extension/pkg/ginkgo/logging.go index 0b84ca41cf..1cf299a7c0 100644 --- a/vendor/github.com/openshift-eng/openshift-tests-extension/pkg/ginkgo/logging.go +++ b/vendor/github.com/openshift-eng/openshift-tests-extension/pkg/ginkgo/logging.go @@ -17,5 +17,9 @@ func GinkgoLogrFunc(writer ginkgo.GinkgoWriterInterface) logr.Logger { } else { writer.Printf("%s %s\n", prefix, args) } - }, funcr.Options{}) + }, funcr.Options{ + // LogTimestamp adds timestamps to log lines using the format "2006-01-02 15:04:05.000000" + // See: https://github.com/go-logr/logr/blob/bb8ea8159175ccb4eddf4ac8704f84e40ac6d9b0/funcr/funcr.go#L211 + LogTimestamp: true, + }) } diff --git a/vendor/github.com/openshift-eng/openshift-tests-extension/pkg/ginkgo/util.go b/vendor/github.com/openshift-eng/openshift-tests-extension/pkg/ginkgo/util.go index e970d46ad4..c6b1728765 100644 --- a/vendor/github.com/openshift-eng/openshift-tests-extension/pkg/ginkgo/util.go +++ b/vendor/github.com/openshift-eng/openshift-tests-extension/pkg/ginkgo/util.go @@ -143,10 +143,13 @@ func BuildExtensionTestSpecsFromOpenShiftGinkgoSuite(selectFns ...ext.SelectFunc return result }, - RunParallel: func(ctx context.Context) *ext.ExtensionTestResult { - // TODO pass through timeout and determine Lifecycle - return SpawnProcessToRunTest(ctx, name, 90*time.Minute) - }, + } + testCase.RunParallel = func(ctx context.Context) *ext.ExtensionTestResult { + timeout := 90 * time.Minute + if testCase.Timeout > 0 { + timeout = testCase.Timeout + } + return SpawnProcessToRunTest(ctx, name, timeout) } specs = append(specs, testCase) }) diff --git a/vendor/github.com/openshift/api/config/v1/types_authentication.go b/vendor/github.com/openshift/api/config/v1/types_authentication.go index 1a036bbb67..54a6d2107f 100644 --- a/vendor/github.com/openshift/api/config/v1/types_authentication.go +++ b/vendor/github.com/openshift/api/config/v1/types_authentication.go @@ -5,7 +5,7 @@ import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" // +genclient // +genclient:nonNamespaced // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object -// +openshift:validation:FeatureGateAwareXValidation:featureGate=ExternalOIDC;ExternalOIDCWithUIDAndExtraClaimMappings;ExternalOIDCWithUpstreamParity,rule="!has(self.spec.oidcProviders) || self.spec.oidcProviders.all(p, !has(p.oidcClients) || p.oidcClients.all(specC, self.status.oidcClients.exists(statusC, statusC.componentNamespace == specC.componentNamespace && statusC.componentName == specC.componentName) || (has(oldSelf.spec.oidcProviders) && oldSelf.spec.oidcProviders.exists(oldP, oldP.name == p.name && has(oldP.oidcClients) && oldP.oidcClients.exists(oldC, oldC.componentNamespace == specC.componentNamespace && oldC.componentName == specC.componentName)))))",message="all oidcClients in the oidcProviders must match their componentName and componentNamespace to either a previously configured oidcClient or they must exist in the status.oidcClients" +// +openshift:validation:FeatureGateAwareXValidation:featureGate=ExternalOIDC;ExternalOIDCWithUIDAndExtraClaimMappings;ExternalOIDCWithUpstreamParity;ExternalOIDCExternalClaimsSourcing,rule="!has(self.spec.oidcProviders) || self.spec.oidcProviders.all(p, !has(p.oidcClients) || p.oidcClients.all(specC, self.status.oidcClients.exists(statusC, statusC.componentNamespace == specC.componentNamespace && statusC.componentName == specC.componentName) || (has(oldSelf.spec.oidcProviders) && oldSelf.spec.oidcProviders.exists(oldP, oldP.name == p.name && has(oldP.oidcClients) && oldP.oidcClients.exists(oldC, oldC.componentNamespace == specC.componentNamespace && oldC.componentName == specC.componentName)))))",message="all oidcClients in the oidcProviders must match their componentName and componentNamespace to either a previously configured oidcClient or they must exist in the status.oidcClients" // Authentication specifies cluster-wide settings for authentication (like OAuth and // webhook token authenticators). The canonical name of an instance is `cluster`. @@ -91,6 +91,7 @@ type AuthenticationSpec struct { // +openshift:enable:FeatureGate=ExternalOIDC // +openshift:enable:FeatureGate=ExternalOIDCWithUIDAndExtraClaimMappings // +openshift:enable:FeatureGate=ExternalOIDCWithUpstreamParity + // +openshift:enable:FeatureGate=ExternalOIDCExternalClaimsSourcing // +optional OIDCProviders []OIDCProvider `json:"oidcProviders,omitempty"` } @@ -245,6 +246,28 @@ type OIDCProvider struct { // +optional // +openshift:enable:FeatureGate=ExternalOIDCWithUpstreamParity UserValidationRules []TokenUserValidationRule `json:"userValidationRules,omitempty"` + + // externalClaimsSources is an optional field that can be used to configure + // sources, external to the token provided in a request, in which claims + // should be fetched from and made available to the claim mapping process + // that is used to build the identity of a token holder. + // + // For example, fetching additional user metadata from an OIDC provider's UserInfo endpoint. + // + // When not specified, only claims present in the token itself will be available + // in the claim mapping process. + // + // When specified, at least one external claim source must be specified and no more than 5 + // sources may be specified. + // + // +openshift:enable:FeatureGate=ExternalOIDCExternalClaimsSourcing + // + // +optional + // +kubebuilder:validation:MinItems=1 + // +kubebuilder:validation:MaxItems=5 + // +kubebuilder:validation:XValidation:rule="self.all(s, s.mappings.all(m, self.filter(s2, s2.mappings.exists(m2, m2.name == m.name)).size() == 1))",message="mapping names must be unique across all external claim sources." + // +listType=atomic + ExternalClaimsSources []ExternalClaimsSource `json:"externalClaimsSources,omitempty"` } // +kubebuilder:validation:MinLength=1 @@ -831,3 +854,324 @@ type TokenUserValidationRule struct { // +kubebuilder:validation:MaxLength=256 Message string `json:"message,omitempty"` } + +// ExternalClaimsSource provides the configuration for a single external claim source. +type ExternalClaimsSource struct { + // authentication is an optional field that configures how the apiserver authenticates with an external claims source. + // When not specified, anonymous authentication is used. + // + // +optional + Authentication ExternalSourceAuthentication `json:"authentication,omitzero"` + + // tls is an optional field that configures the http client TLS + // settings when fetching external claims from this source. + // + // When omitted, system default TLS settings will be used + // for fetching claims from the external source. + // + // +optional + TLS ExternalSourceTLS `json:"tls,omitzero"` + + // url is a required configuration of the URL + // for which the external claims are located. + // + // +required + URL SourceURL `json:"url,omitzero"` + + // mappings is a required list of the claim + // and response handling expression pairs + // that produces the claims from the external source. + // mappings must have at least 1 entry and must not exceed 16 entries. + // Entries must have a unique name across all external claim sources. + // + // +required + // +listType=map + // +listMapKey=name + // +kubebuilder:validation:MinItems=1 + // +kubebuilder:validation:MaxItems=16 + Mappings []SourcedClaimMapping `json:"mappings,omitempty"` + + // predicates is an optional list of constraints in + // which claims should attempt to be fetched from this + // external source. + // + // When omitted, claims are always attempted to be fetched + // from this external source. + // + // When specified, all predicates must evaluate to 'true' + // before claims are attempted to be fetched from this external source. + // predicates must have at least 1 entry and must not exceed 16 entries. + // Entries must have unique expressions. + // + // +optional + // +listType=map + // +listMapKey=expression + // +kubebuilder:validation:MinItems=1 + // +kubebuilder:validation:MaxItems=16 + Predicates []ExternalSourcePredicate `json:"predicates,omitempty"` +} + +// ExternalSourceAuthenticationType is the type of authentication that should be used +// when fetching claims from an external source. +// +// +enum +// +kubebuilder:validation:Enum=RequestProvidedToken;ClientCredential +type ExternalSourceAuthenticationType string + +const ( + // ExternalSourceAuthenticationTypeRequestProvidedToken is an ExternalSourceAuthenticationType + // that represents that the token being evaluated for authentication + // should be used for authenticating with the external claims source. + // This is useful for scenarios where a token has multiple audiences + // and scopes so that it can be used to access both the cluster and + // the UserInfo endpoint that contains additional information about the + // user not present in the token. + ExternalSourceAuthenticationTypeRequestProvidedToken ExternalSourceAuthenticationType = "RequestProvidedToken" + + // ExternalSourceAuthenticationTypeClientCredential is an ExternalSourceAuthenticationType + // that represents that the authenticator should use the OAuth2 + // client credentials grant flow to obtain an access token for + // authenticating with the external claims source. + // This is useful for scenarios such as fetching user information + // from Microsoft's Graph API where a separate client credential + // is needed to access the API. + ExternalSourceAuthenticationTypeClientCredential ExternalSourceAuthenticationType = "ClientCredential" +) + +// ExternalSourceAuthentication configures how the apiserver should attempt +// to authenticate with an external claims source. +// +// +kubebuilder:validation:XValidation:rule="self.type == 'ClientCredential' ? has(self.clientCredential) : !has(self.clientCredential)",message="clientCredential is required when type is ClientCredential, and forbidden otherwise" +type ExternalSourceAuthentication struct { + // type is a required field that sets the type of + // authentication method used by the authenticator + // when fetching external claims. + // + // Allowed values are 'RequestProvidedToken' and 'ClientCredential'. + // + // When set to 'RequestProvidedToken', the authenticator will + // use the token provided to the kube-apiserver as part of the + // request to authenticate with the external claims source. + // + // When set to 'ClientCredential', the authenticator will + // use the configured client-id, client-secret, and token endpoint + // to fetch an access token using the OAuth2 client credentials grant + // flow. The fetched access token will then be used to authenticate + // with the external claims source. + // + // +required + Type ExternalSourceAuthenticationType `json:"type,omitempty"` + + // clientCredential configures the client credentials + // and token endpoint to use to get an access token. + // clientCredential is required when type is 'ClientCredential', and forbidden otherwise. + // + // +optional + ClientCredential ClientCredentialConfig `json:"clientCredential,omitzero"` +} + +// ExternalSourceTLS configures the TLS options that the apiserver uses as a client +// when making a request to the external claim source. +type ExternalSourceTLS struct { + // certificateAuthority is a required reference to a ConfigMap in the openshift-config + // namespace that contains the CA certificate to use to validate TLS connections with the external claims source. + // + // +required + CertificateAuthority ExternalSourceCertificateAuthorityConfigMapReference `json:"certificateAuthority,omitzero"` +} + +// ClientCredentialConfig configures the client credentials and token endpoint +// to use to get an access token via the OAuth2 client credentials grant flow. +type ClientCredentialConfig struct { + // clientID is a required client identifier to use during the OAuth2 client credentials flow. + // clientID must be at least 1 character in length, must not exceed 256 characters in length, + // and must only contain printable ASCII characters. + // + // +required + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=256 + // +kubebuilder:validation:XValidation:rule="self.matches('^[[:print:]]+$')",message="clientID must only contain printable ASCII characters" + ClientID string `json:"clientID,omitempty"` + + // clientSecret is a required reference to a Secret in the openshift-config namespace to be used + // as the client secret during the OAuth2 client credentials flow. + // + // The key 'client-secret' is used to locate the client secret data in the Secret. + // + // +required + ClientSecret ClientSecretSecretReference `json:"clientSecret,omitzero"` + + // tokenEndpoint is a required URL to query for an access token using + // the client credential OAuth2 flow. + // tokenEndpoint must be at least 1 character in length and must not exceed 2048 characters in length. + // tokenEndpoint must be a valid HTTPS URL. + // tokenEndpoint must have a host and a path. + // tokenEndpoint must not contain query parameters, fragments, + // or user information (e.g., "user:password@host"). + // + // +required + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=2048 + // +kubebuilder:validation:XValidation:rule="isURL(self)",message="tokenEndpoint must be a valid HTTPS url" + // +kubebuilder:validation:XValidation:rule="isURL(self) && url(self).getScheme() == 'https'",message="tokenEndpoint must be a valid HTTPS url" + // +kubebuilder:validation:XValidation:rule="isURL(self) && url(self).getHost() != ''",message="tokenEndpoint must have a hostname" + // +kubebuilder:validation:XValidation:rule="isURL(self) && url(self).getEscapedPath() != ''",message="tokenEndpoint must have a path" + // +kubebuilder:validation:XValidation:rule="isURL(self) && url(self).getQuery() == {}",message="tokenEndpoint must not have query parameters" + // +kubebuilder:validation:XValidation:rule="isURL(self) && self.find('#(.+)$') == ''",message="tokenEndpoint must not have a fragment" + // +kubebuilder:validation:XValidation:rule="isURL(self) && !self.matches('^https://[^/]+@.+$')",message="tokenEndpoint must not have user info" + TokenEndpoint string `json:"tokenEndpoint,omitempty"` + + // scopes is an optional list of OAuth2 scopes to request when obtaining + // an access token. + // + // If not specified, the token endpoint's default scopes + // will be used. + // + // When specified, there must be at least 1 entry and must not exceed 16 entries. + // Each entry must be at least 1 character in length and must not exceed 256 characters in length. + // Each entry must only contain printable ASCII characters, excluding spaces, double quotes and backslashes. + // Entries must be unique. + // + // +optional + // +kubebuilder:validation:MinItems=1 + // +kubebuilder:validation:MaxItems=16 + // +listType=set + Scopes []OAuth2Scope `json:"scopes,omitempty"` + + // tls is an optional field that allows configuring the TLS + // settings used to interact with the identity provider + // as an OAuth2 client. + // + // When omitted, system default TLS settings will be used + // for the OAuth2 client. + // + // +optional + TLS ExternalSourceTLS `json:"tls,omitzero"` +} + +// OAuth2Scope is a string alias that represents an OAuth2 Scope as defined by https://datatracker.ietf.org/doc/html/rfc6749#appendix-A.4 +// +// +kubebuilder:validation:XValidation:rule="self.matches('^[!#-[\\\\]-~]+$')",message="scopes must only contain printable ASCII characters excluding spaces, double quotes and backslashes" +// +kubebuilder:validation:MinLength=1 +// +kubebuilder:validation:MaxLength=256 +type OAuth2Scope string + +// SourceURL configures the options used to build the URL that is queried for external claims. +type SourceURL struct { + // hostname is a required hostname for which the external claims are located. + // + // It must be a valid DNS subdomain name as per RFC1123. + // + // This means that it must start and end with a lowercase alphanumeric character, + // must only consist of lowercase alphanumeric characters, '-', and '.'. + // hostname must be at least 1 character in length and must not exceed 253 characters in length. + // hostname may optionally specify a port in the format ':{port}'. + // If a port is specified it must not exceed 65535. + // + // +required + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=259 + // +kubebuilder:validation:XValidation:rule="isURL('https://'+self)",message="hostname must be a valid hostname" + // +kubebuilder:validation:XValidation:rule="!format.dns1123Subdomain().validate(self.split(':')[0]).hasValue()",message="hostname before port must start and end with a lowercase alphanumeric character, and must only contain lowercase alphanumeric characters, '-' or '.'" + // +kubebuilder:validation:XValidation:rule="self.split(':').size() > 1 ? int(self.split(':')[1]) <= 65535 : true",message="port must not exceed 65535" + Hostname string `json:"hostname,omitempty"` + + // pathExpression is a required CEL expression that returns a list + // of string values used to construct the URL path. + // Claims from the token used for the request to the kube-apiserver + // are made available via the `claims` variable. + // expression must be at least 1 character in length and must not exceed 1024 characters in length. + // + // Values in the returned list will be joined with the hostname using a forward slash + // (`/`) as a separator. Values in the returned list do not need to include the forward slash. + // + // +required + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=1024 + PathExpression string `json:"pathExpression,omitempty"` +} + +// SourcedClaimMapping configures the mapping behavior for a single external claim +// from the response the apiserver received from the external claim source. +type SourcedClaimMapping struct { + // name is a required name of the claim that + // will be produced and made available during + // the claim-to-identity mapping process. + // name must consist of only lowercase alpha characters and underscores ('_'). + // name must be at least 1 character and must not exceed 256 characters in length. + // + // +required + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=256 + // +kubebuilder:validation:XValidation:rule="self.matches('^[a-z_]+$')",message="name must consist of only lowercase alpha characters and underscores" + Name string `json:"name,omitempty"` + + // expression is a required CEL expression that + // will produce a value to be assigned to the claim. + // The full response body from the request to the + // external claim source is provided via the + // `response` variable. + // expression must be at least 1 character and must not exceed 1024 characters in length. + // + // +required + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=1024 + Expression string `json:"expression,omitempty"` +} + +// ExternalSourcePredicate configures a singular condition +// that must return true before the external source is queried +// to retrieve external claims. +type ExternalSourcePredicate struct { + // expression is a required CEL expression that + // is used to determine whether or not an external + // source should be used to fetch external claims. + // + // The expression must return a boolean value, + // where true means that the source should be consulted + // and false means that it should not. + // + // Claims from the token used for the request to the kube-apiserver + // are made available via the `claims` variable. + // + // expression must be at least 1 character and must not exceed 1024 characters in length. + // + // +required + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=1024 + Expression string `json:"expression,omitempty"` +} + +// CertificateAuthorityConfigMapReference is a reference to a ConfigMap in the openshift-config +// namespace that should be used for configuring the certificate authority to be +// used when sourcing claims from external sources. +type ExternalSourceCertificateAuthorityConfigMapReference struct { + // name is the required name of the ConfigMap that exists in the openshift-config namespace. + // + // It must be at least 1 character in length, must not exceed 253 characters in length, + // must start and end with a lowercase alphanumeric character, and must only contain + // lowercase alphanumeric characters, '-' or '.'. + // + // +required + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=253 + // +kubebuilder:validation:XValidation:rule="!format.dns1123Subdomain().validate(self).hasValue()",message="name must start and end with a lowercase alphanumeric character, and must only contain lowercase alphanumeric characters, '-' or '.'" + Name string `json:"name,omitempty"` +} + +// ClientSecretSecretReference is a reference to a Secret in the openshift-config +// namespace that should be used for configuring the client secret to be +// used when sourcing claims from external sources with the client credential authentication flow. +type ClientSecretSecretReference struct { + // name is the required name of the Secret that exists in the openshift-config namespace. + // + // It must be at least 1 character in length, must not exceed 253 characters in length, + // must start and end with a lowercase alphanumeric character, and must only contain + // lowercase alphanumeric characters, '-' or '.'. + // + // +required + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=253 + // +kubebuilder:validation:XValidation:rule="!format.dns1123Subdomain().validate(self).hasValue()",message="name must start and end with a lowercase alphanumeric character, and must only contain lowercase alphanumeric characters, '-' or '.'" + Name string `json:"name,omitempty"` +} diff --git a/vendor/github.com/openshift/api/config/v1/zz_generated.deepcopy.go b/vendor/github.com/openshift/api/config/v1/zz_generated.deepcopy.go index 84aae76e22..bca5d18192 100644 --- a/vendor/github.com/openshift/api/config/v1/zz_generated.deepcopy.go +++ b/vendor/github.com/openshift/api/config/v1/zz_generated.deepcopy.go @@ -940,6 +940,45 @@ func (in *ClientConnectionOverrides) DeepCopy() *ClientConnectionOverrides { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClientCredentialConfig) DeepCopyInto(out *ClientCredentialConfig) { + *out = *in + out.ClientSecret = in.ClientSecret + if in.Scopes != nil { + in, out := &in.Scopes, &out.Scopes + *out = make([]OAuth2Scope, len(*in)) + copy(*out, *in) + } + out.TLS = in.TLS + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClientCredentialConfig. +func (in *ClientCredentialConfig) DeepCopy() *ClientCredentialConfig { + if in == nil { + return nil + } + out := new(ClientCredentialConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClientSecretSecretReference) DeepCopyInto(out *ClientSecretSecretReference) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClientSecretSecretReference. +func (in *ClientSecretSecretReference) DeepCopy() *ClientSecretSecretReference { + if in == nil { + return nil + } + out := new(ClientSecretSecretReference) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *CloudControllerManagerStatus) DeepCopyInto(out *CloudControllerManagerStatus) { *out = *in @@ -2087,6 +2126,35 @@ func (in *EtcdStorageConfig) DeepCopy() *EtcdStorageConfig { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ExternalClaimsSource) DeepCopyInto(out *ExternalClaimsSource) { + *out = *in + in.Authentication.DeepCopyInto(&out.Authentication) + out.TLS = in.TLS + out.URL = in.URL + if in.Mappings != nil { + in, out := &in.Mappings, &out.Mappings + *out = make([]SourcedClaimMapping, len(*in)) + copy(*out, *in) + } + if in.Predicates != nil { + in, out := &in.Predicates, &out.Predicates + *out = make([]ExternalSourcePredicate, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExternalClaimsSource. +func (in *ExternalClaimsSource) DeepCopy() *ExternalClaimsSource { + if in == nil { + return nil + } + out := new(ExternalClaimsSource) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ExternalIPConfig) DeepCopyInto(out *ExternalIPConfig) { *out = *in @@ -2172,6 +2240,72 @@ func (in *ExternalPlatformStatus) DeepCopy() *ExternalPlatformStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ExternalSourceAuthentication) DeepCopyInto(out *ExternalSourceAuthentication) { + *out = *in + in.ClientCredential.DeepCopyInto(&out.ClientCredential) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExternalSourceAuthentication. +func (in *ExternalSourceAuthentication) DeepCopy() *ExternalSourceAuthentication { + if in == nil { + return nil + } + out := new(ExternalSourceAuthentication) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ExternalSourceCertificateAuthorityConfigMapReference) DeepCopyInto(out *ExternalSourceCertificateAuthorityConfigMapReference) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExternalSourceCertificateAuthorityConfigMapReference. +func (in *ExternalSourceCertificateAuthorityConfigMapReference) DeepCopy() *ExternalSourceCertificateAuthorityConfigMapReference { + if in == nil { + return nil + } + out := new(ExternalSourceCertificateAuthorityConfigMapReference) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ExternalSourcePredicate) DeepCopyInto(out *ExternalSourcePredicate) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExternalSourcePredicate. +func (in *ExternalSourcePredicate) DeepCopy() *ExternalSourcePredicate { + if in == nil { + return nil + } + out := new(ExternalSourcePredicate) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ExternalSourceTLS) DeepCopyInto(out *ExternalSourceTLS) { + *out = *in + out.CertificateAuthority = in.CertificateAuthority + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExternalSourceTLS. +func (in *ExternalSourceTLS) DeepCopy() *ExternalSourceTLS { + if in == nil { + return nil + } + out := new(ExternalSourceTLS) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ExtraMapping) DeepCopyInto(out *ExtraMapping) { *out = *in @@ -4828,6 +4962,13 @@ func (in *OIDCProvider) DeepCopyInto(out *OIDCProvider) { *out = make([]TokenUserValidationRule, len(*in)) copy(*out, *in) } + if in.ExternalClaimsSources != nil { + in, out := &in.ExternalClaimsSources, &out.ExternalClaimsSources + *out = make([]ExternalClaimsSource, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } return } @@ -6168,6 +6309,38 @@ func (in *SignatureStore) DeepCopy() *SignatureStore { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SourceURL) DeepCopyInto(out *SourceURL) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SourceURL. +func (in *SourceURL) DeepCopy() *SourceURL { + if in == nil { + return nil + } + out := new(SourceURL) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SourcedClaimMapping) DeepCopyInto(out *SourcedClaimMapping) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SourcedClaimMapping. +func (in *SourcedClaimMapping) DeepCopy() *SourcedClaimMapping { + if in == nil { + return nil + } + out := new(SourcedClaimMapping) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Storage) DeepCopyInto(out *Storage) { *out = *in diff --git a/vendor/github.com/openshift/api/config/v1/zz_generated.featuregated-crd-manifests.yaml b/vendor/github.com/openshift/api/config/v1/zz_generated.featuregated-crd-manifests.yaml index 75233bff73..9744044bdf 100644 --- a/vendor/github.com/openshift/api/config/v1/zz_generated.featuregated-crd-manifests.yaml +++ b/vendor/github.com/openshift/api/config/v1/zz_generated.featuregated-crd-manifests.yaml @@ -31,6 +31,7 @@ authentications.config.openshift.io: Category: "" FeatureGates: - ExternalOIDC + - ExternalOIDCExternalClaimsSourcing - ExternalOIDCWithUIDAndExtraClaimMappings - ExternalOIDCWithUpstreamParity FilenameOperatorName: config-operator diff --git a/vendor/github.com/openshift/api/config/v1/zz_generated.swagger_doc_generated.go b/vendor/github.com/openshift/api/config/v1/zz_generated.swagger_doc_generated.go index f386a81125..d3379f70ea 100644 --- a/vendor/github.com/openshift/api/config/v1/zz_generated.swagger_doc_generated.go +++ b/vendor/github.com/openshift/api/config/v1/zz_generated.swagger_doc_generated.go @@ -388,6 +388,28 @@ func (AuthenticationStatus) SwaggerDoc() map[string]string { return map_AuthenticationStatus } +var map_ClientCredentialConfig = map[string]string{ + "": "ClientCredentialConfig configures the client credentials and token endpoint to use to get an access token via the OAuth2 client credentials grant flow.", + "clientID": "clientID is a required client identifier to use during the OAuth2 client credentials flow. clientID must be at least 1 character in length, must not exceed 256 characters in length, and must only contain printable ASCII characters.", + "clientSecret": "clientSecret is a required reference to a Secret in the openshift-config namespace to be used as the client secret during the OAuth2 client credentials flow.\n\nThe key 'client-secret' is used to locate the client secret data in the Secret.", + "tokenEndpoint": "tokenEndpoint is a required URL to query for an access token using the client credential OAuth2 flow. tokenEndpoint must be at least 1 character in length and must not exceed 2048 characters in length. tokenEndpoint must be a valid HTTPS URL. tokenEndpoint must have a host and a path. tokenEndpoint must not contain query parameters, fragments, or user information (e.g., \"user:password@host\").", + "scopes": "scopes is an optional list of OAuth2 scopes to request when obtaining an access token.\n\nIf not specified, the token endpoint's default scopes will be used.\n\nWhen specified, there must be at least 1 entry and must not exceed 16 entries. Each entry must be at least 1 character in length and must not exceed 256 characters in length. Each entry must only contain printable ASCII characters, excluding spaces, double quotes and backslashes. Entries must be unique.", + "tls": "tls is an optional field that allows configuring the TLS settings used to interact with the identity provider as an OAuth2 client.\n\nWhen omitted, system default TLS settings will be used for the OAuth2 client.", +} + +func (ClientCredentialConfig) SwaggerDoc() map[string]string { + return map_ClientCredentialConfig +} + +var map_ClientSecretSecretReference = map[string]string{ + "": "ClientSecretSecretReference is a reference to a Secret in the openshift-config namespace that should be used for configuring the client secret to be used when sourcing claims from external sources with the client credential authentication flow.", + "name": "name is the required name of the Secret that exists in the openshift-config namespace.\n\nIt must be at least 1 character in length, must not exceed 253 characters in length, must start and end with a lowercase alphanumeric character, and must only contain lowercase alphanumeric characters, '-' or '.'.", +} + +func (ClientSecretSecretReference) SwaggerDoc() map[string]string { + return map_ClientSecretSecretReference +} + var map_DeprecatedWebhookTokenAuthenticator = map[string]string{ "": "deprecatedWebhookTokenAuthenticator holds the necessary configuration options for a remote token authenticator. It's the same as WebhookTokenAuthenticator but it's missing the 'required' validation on KubeConfig field.", "kubeConfig": "kubeConfig contains kube config file data which describes how to access the remote webhook service. For further details, see: https://kubernetes.io/docs/reference/access-authn-authz/authentication/#webhook-token-authentication The key \"kubeConfig\" is used to locate the data. If the secret or expected key is not found, the webhook is not honored. If the specified kube config data is not valid, the webhook is not honored. The namespace for this secret is determined by the point of use.", @@ -397,6 +419,56 @@ func (DeprecatedWebhookTokenAuthenticator) SwaggerDoc() map[string]string { return map_DeprecatedWebhookTokenAuthenticator } +var map_ExternalClaimsSource = map[string]string{ + "": "ExternalClaimsSource provides the configuration for a single external claim source.", + "authentication": "authentication is an optional field that configures how the apiserver authenticates with an external claims source. When not specified, anonymous authentication is used.", + "tls": "tls is an optional field that configures the http client TLS settings when fetching external claims from this source.\n\nWhen omitted, system default TLS settings will be used for fetching claims from the external source.", + "url": "url is a required configuration of the URL for which the external claims are located.", + "mappings": "mappings is a required list of the claim and response handling expression pairs that produces the claims from the external source. mappings must have at least 1 entry and must not exceed 16 entries. Entries must have a unique name across all external claim sources.", + "predicates": "predicates is an optional list of constraints in which claims should attempt to be fetched from this external source.\n\nWhen omitted, claims are always attempted to be fetched from this external source.\n\nWhen specified, all predicates must evaluate to 'true' before claims are attempted to be fetched from this external source. predicates must have at least 1 entry and must not exceed 16 entries. Entries must have unique expressions.", +} + +func (ExternalClaimsSource) SwaggerDoc() map[string]string { + return map_ExternalClaimsSource +} + +var map_ExternalSourceAuthentication = map[string]string{ + "": "ExternalSourceAuthentication configures how the apiserver should attempt to authenticate with an external claims source.", + "type": "type is a required field that sets the type of authentication method used by the authenticator when fetching external claims.\n\nAllowed values are 'RequestProvidedToken' and 'ClientCredential'.\n\nWhen set to 'RequestProvidedToken', the authenticator will use the token provided to the kube-apiserver as part of the request to authenticate with the external claims source.\n\nWhen set to 'ClientCredential', the authenticator will use the configured client-id, client-secret, and token endpoint to fetch an access token using the OAuth2 client credentials grant flow. The fetched access token will then be used to authenticate with the external claims source.", + "clientCredential": "clientCredential configures the client credentials and token endpoint to use to get an access token. clientCredential is required when type is 'ClientCredential', and forbidden otherwise.", +} + +func (ExternalSourceAuthentication) SwaggerDoc() map[string]string { + return map_ExternalSourceAuthentication +} + +var map_ExternalSourceCertificateAuthorityConfigMapReference = map[string]string{ + "": "CertificateAuthorityConfigMapReference is a reference to a ConfigMap in the openshift-config namespace that should be used for configuring the certificate authority to be used when sourcing claims from external sources.", + "name": "name is the required name of the ConfigMap that exists in the openshift-config namespace.\n\nIt must be at least 1 character in length, must not exceed 253 characters in length, must start and end with a lowercase alphanumeric character, and must only contain lowercase alphanumeric characters, '-' or '.'.", +} + +func (ExternalSourceCertificateAuthorityConfigMapReference) SwaggerDoc() map[string]string { + return map_ExternalSourceCertificateAuthorityConfigMapReference +} + +var map_ExternalSourcePredicate = map[string]string{ + "": "ExternalSourcePredicate configures a singular condition that must return true before the external source is queried to retrieve external claims.", + "expression": "expression is a required CEL expression that is used to determine whether or not an external source should be used to fetch external claims.\n\nThe expression must return a boolean value, where true means that the source should be consulted and false means that it should not.\n\nClaims from the token used for the request to the kube-apiserver are made available via the `claims` variable.\n\nexpression must be at least 1 character and must not exceed 1024 characters in length.", +} + +func (ExternalSourcePredicate) SwaggerDoc() map[string]string { + return map_ExternalSourcePredicate +} + +var map_ExternalSourceTLS = map[string]string{ + "": "ExternalSourceTLS configures the TLS options that the apiserver uses as a client when making a request to the external claim source.", + "certificateAuthority": "certificateAuthority is a required reference to a ConfigMap in the openshift-config namespace that contains the CA certificate to use to validate TLS connections with the external claims source.", +} + +func (ExternalSourceTLS) SwaggerDoc() map[string]string { + return map_ExternalSourceTLS +} + var map_ExtraMapping = map[string]string{ "": "ExtraMapping allows specifying a key and CEL expression to evaluate the keys' value. It is used to create additional mappings and attributes added to a cluster identity from a provided authentication token.", "key": "key is a required field that specifies the string to use as the extra attribute key.\n\nkey must be a domain-prefix path (e.g 'example.org/foo'). key must not exceed 510 characters in length. key must contain the '/' character, separating the domain and path characters. key must not be empty.\n\nThe domain portion of the key (string of characters prior to the '/') must be a valid RFC1123 subdomain. It must not exceed 253 characters in length. It must start and end with an alphanumeric character. It must only contain lower case alphanumeric characters and '-' or '.'. It must not use the reserved domains, or be subdomains of, \"kubernetes.io\", \"k8s.io\", and \"openshift.io\".\n\nThe path portion of the key (string of characters after the '/') must not be empty and must consist of at least one alphanumeric character, percent-encoded octets, '-', '.', '_', '~', '!', '$', '&', ''', '(', ')', '*', '+', ',', ';', '=', and ':'. It must not exceed 256 characters in length.", @@ -445,12 +517,13 @@ func (OIDCClientStatus) SwaggerDoc() map[string]string { } var map_OIDCProvider = map[string]string{ - "name": "name is a required field that configures the unique human-readable identifier associated with the identity provider. It is used to distinguish between multiple identity providers and has no impact on token validation or authentication mechanics.\n\nname must not be an empty string (\"\").", - "issuer": "issuer is a required field that configures how the platform interacts with the identity provider and how tokens issued from the identity provider are evaluated by the Kubernetes API server.", - "oidcClients": "oidcClients is an optional field that configures how on-cluster, platform clients should request tokens from the identity provider. oidcClients must not exceed 20 entries and entries must have unique namespace/name pairs.", - "claimMappings": "claimMappings is a required field that configures the rules to be used by the Kubernetes API server for translating claims in a JWT token, issued by the identity provider, to a cluster identity.", - "claimValidationRules": "claimValidationRules is an optional field that configures the rules to be used by the Kubernetes API server for validating the claims in a JWT token issued by the identity provider.\n\nValidation rules are joined via an AND operation.", - "userValidationRules": "userValidationRules is an optional field that configures the set of rules used to validate the cluster user identity that was constructed via mapping token claims to user identity attributes. Rules are CEL expressions that must evaluate to 'true' for authentication to succeed. If any rule in the chain of rules evaluates to 'false', authentication will fail. When specified, at least one rule must be specified and no more than 64 rules may be specified.", + "name": "name is a required field that configures the unique human-readable identifier associated with the identity provider. It is used to distinguish between multiple identity providers and has no impact on token validation or authentication mechanics.\n\nname must not be an empty string (\"\").", + "issuer": "issuer is a required field that configures how the platform interacts with the identity provider and how tokens issued from the identity provider are evaluated by the Kubernetes API server.", + "oidcClients": "oidcClients is an optional field that configures how on-cluster, platform clients should request tokens from the identity provider. oidcClients must not exceed 20 entries and entries must have unique namespace/name pairs.", + "claimMappings": "claimMappings is a required field that configures the rules to be used by the Kubernetes API server for translating claims in a JWT token, issued by the identity provider, to a cluster identity.", + "claimValidationRules": "claimValidationRules is an optional field that configures the rules to be used by the Kubernetes API server for validating the claims in a JWT token issued by the identity provider.\n\nValidation rules are joined via an AND operation.", + "userValidationRules": "userValidationRules is an optional field that configures the set of rules used to validate the cluster user identity that was constructed via mapping token claims to user identity attributes. Rules are CEL expressions that must evaluate to 'true' for authentication to succeed. If any rule in the chain of rules evaluates to 'false', authentication will fail. When specified, at least one rule must be specified and no more than 64 rules may be specified.", + "externalClaimsSources": "externalClaimsSources is an optional field that can be used to configure sources, external to the token provided in a request, in which claims should be fetched from and made available to the claim mapping process that is used to build the identity of a token holder.\n\nFor example, fetching additional user metadata from an OIDC provider's UserInfo endpoint.\n\nWhen not specified, only claims present in the token itself will be available in the claim mapping process.\n\nWhen specified, at least one external claim source must be specified and no more than 5 sources may be specified.", } func (OIDCProvider) SwaggerDoc() map[string]string { @@ -466,6 +539,26 @@ func (PrefixedClaimMapping) SwaggerDoc() map[string]string { return map_PrefixedClaimMapping } +var map_SourceURL = map[string]string{ + "": "SourceURL configures the options used to build the URL that is queried for external claims.", + "hostname": "hostname is a required hostname for which the external claims are located.\n\nIt must be a valid DNS subdomain name as per RFC1123.\n\nThis means that it must start and end with a lowercase alphanumeric character, must only consist of lowercase alphanumeric characters, '-', and '.'. hostname must be at least 1 character in length and must not exceed 253 characters in length. hostname may optionally specify a port in the format ':{port}'. If a port is specified it must not exceed 65535.", + "pathExpression": "pathExpression is a required CEL expression that returns a list of string values used to construct the URL path. Claims from the token used for the request to the kube-apiserver are made available via the `claims` variable. expression must be at least 1 character in length and must not exceed 1024 characters in length.\n\nValues in the returned list will be joined with the hostname using a forward slash (`/`) as a separator. Values in the returned list do not need to include the forward slash.", +} + +func (SourceURL) SwaggerDoc() map[string]string { + return map_SourceURL +} + +var map_SourcedClaimMapping = map[string]string{ + "": "SourcedClaimMapping configures the mapping behavior for a single external claim from the response the apiserver received from the external claim source.", + "name": "name is a required name of the claim that will be produced and made available during the claim-to-identity mapping process. name must consist of only lowercase alpha characters and underscores ('_'). name must be at least 1 character and must not exceed 256 characters in length.", + "expression": "expression is a required CEL expression that will produce a value to be assigned to the claim. The full response body from the request to the external claim source is provided via the `response` variable. expression must be at least 1 character and must not exceed 1024 characters in length.", +} + +func (SourcedClaimMapping) SwaggerDoc() map[string]string { + return map_SourcedClaimMapping +} + var map_TokenClaimMapping = map[string]string{ "": "TokenClaimMapping allows specifying a JWT token claim to be used when mapping claims from an authentication token to cluster identities.", "claim": "claim is an optional field for specifying the JWT token claim that is used in the mapping. The value of this claim will be assigned to the field in which this mapping is associated. claim must not exceed 256 characters in length. When set to the empty string `\"\"`, this means that no named claim should be used for the group mapping. claim is required when the ExternalOIDCWithUpstreamParity feature gate is not enabled.", diff --git a/vendor/github.com/openshift/build-machinery-go/make/targets/openshift/yq.mk b/vendor/github.com/openshift/build-machinery-go/make/targets/openshift/yq.mk index 07726d87cd..0854374fe5 100644 --- a/vendor/github.com/openshift/build-machinery-go/make/targets/openshift/yq.mk +++ b/vendor/github.com/openshift/build-machinery-go/make/targets/openshift/yq.mk @@ -8,8 +8,7 @@ include $(addprefix $(dir $(lastword $(MAKEFILE_LIST))), \ YQ_VERSION ?=2.4.0 YQ ?=$(PERMANENT_TMP_GOPATH)/bin/yq-$(YQ_VERSION) -yq_dir :=$(dir $(YQ)) - +yq_dir =$(dir $(YQ)) ensure-yq: ifeq "" "$(wildcard $(YQ))" diff --git a/vendor/github.com/openshift/oauth-apiserver/LICENSE b/vendor/github.com/openshift/oauth-apiserver/LICENSE new file mode 100644 index 0000000000..261eeb9e9f --- /dev/null +++ b/vendor/github.com/openshift/oauth-apiserver/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/vendor/github.com/openshift/oauth-apiserver/pkg/externaloidc/apis/authentication/doc.go b/vendor/github.com/openshift/oauth-apiserver/pkg/externaloidc/apis/authentication/doc.go new file mode 100644 index 0000000000..c174f5b575 --- /dev/null +++ b/vendor/github.com/openshift/oauth-apiserver/pkg/externaloidc/apis/authentication/doc.go @@ -0,0 +1,5 @@ +// +k8s:deepcopy-gen=package,register + +// +groupName=authentication.openshift.io +// Package authentication is the internal version of the API. +package authentication diff --git a/vendor/github.com/openshift/oauth-apiserver/pkg/externaloidc/apis/authentication/register.go b/vendor/github.com/openshift/oauth-apiserver/pkg/externaloidc/apis/authentication/register.go new file mode 100644 index 0000000000..56421c7a23 --- /dev/null +++ b/vendor/github.com/openshift/oauth-apiserver/pkg/externaloidc/apis/authentication/register.go @@ -0,0 +1,36 @@ +package authentication + +import ( + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +const ( + GroupName = "authentication.openshift.io" +) + +var ( + schemeBuilder = runtime.NewSchemeBuilder( + addKnownTypes, + ) + Install = schemeBuilder.AddToScheme + + // DEPRECATED kept for generated code + SchemeGroupVersion = schema.GroupVersion{Group: GroupName, Version: runtime.APIVersionInternal} + // DEPRECATED kept for generated code + AddToScheme = schemeBuilder.AddToScheme +) + +// Resource kept for generated code +// DEPRECATED +func Resource(resource string) schema.GroupResource { + return SchemeGroupVersion.WithResource(resource).GroupResource() +} + +// Adds the list of known types to api.Scheme. +func addKnownTypes(scheme *runtime.Scheme) error { + scheme.AddKnownTypes(SchemeGroupVersion, + &AuthenticationConfiguration{}, + ) + return nil +} diff --git a/vendor/github.com/openshift/oauth-apiserver/pkg/externaloidc/apis/authentication/types.go b/vendor/github.com/openshift/oauth-apiserver/pkg/externaloidc/apis/authentication/types.go new file mode 100644 index 0000000000..5026e2b6c3 --- /dev/null +++ b/vendor/github.com/openshift/oauth-apiserver/pkg/externaloidc/apis/authentication/types.go @@ -0,0 +1,555 @@ +package authentication + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +/* + +The types in this file are heavily inspired by the existing +Kubernetes AuthenticationConfiguration type found at +https://github.com/kubernetes/kubernetes/blob/b2f73c0d6b427e2ab5ba225375aaefc0b9bc45b2/staging/src/k8s.io/apiserver/pkg/apis/apiserver/v1/types.go#L56 + +The API surface here intentionally aligns with this API because it is meant +to be a wrapper around that API with some customization to support unique +functionality that OpenShift needs. + +*/ + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +type AuthenticationConfiguration struct { + metav1.TypeMeta + + // jwt is a list of authenticator to authenticate Kubernetes users using + // JWT compliant tokens. The authenticator will attempt to parse a raw ID token, + // verify it's been signed by the configured issuer. The public key to verify the + // signature is discovered from the issuer's public endpoint using OIDC discovery. + // For an incoming token, each JWT authenticator will be attempted in + // the order in which it is specified in this list. Note however that + // other authenticators may run before or after the JWT authenticators. + // The specific position of JWT authenticators in relation to other + // authenticators is neither defined nor stable across releases. Since + // each JWT authenticator must have a unique issuer URL, at most one + // JWT authenticator will attempt to cryptographically validate the token. + // + // The minimum valid JWT payload must contain the following claims: + // { + // "iss": "https://issuer.example.com", + // "aud": ["audience"], + // "exp": 1234567890, + // "": "username" + // } + JWT []JWTAuthenticator +} + +type JWTAuthenticator struct { + // issuer contains the basic OIDC provider connection options. + // +required + Issuer *Issuer + + // claimValidationRules are rules that are applied to validate token claims to authenticate users. + // +optional + ClaimValidationRules []ClaimValidationRule + + // claimMappings points claims of a token to be treated as user attributes. + // +required + ClaimMappings *ClaimMappings + + // userValidationRules are rules that are applied to final user before completing authentication. + // These allow invariants to be applied to incoming identities such as preventing the + // use of the system: prefix that is commonly used by Kubernetes components. + // The validation rules are logically ANDed together and must all return true for the validation to pass. + // +optional + UserValidationRules []UserValidationRule + + // externalClaimSources is an optional field that can be used to configure + // sources, external to the token provided in a request, in which claims + // should be fetched from and made available to the claim mapping process + // that is used to build the identity of a token holder. + // For example, fetching additional user metadata from an OIDC provider's UserInfo endpoint. + // externalClaimSources must not exceed 5 entries. + // +optional + ExternalClaimsSources []ExternalClaimsSource +} + +// Issuer provides the configuration for an external provider's specific settings. +type Issuer struct { + // url points to the issuer URL in a format https://url or https://url/path. + // This must match the "iss" claim in the presented JWT, and the issuer returned from discovery. + // Same value as the --oidc-issuer-url flag. + // Discovery information is fetched from "{url}/.well-known/openid-configuration" unless overridden by discoveryURL. + // Required to be unique across all JWT authenticators. + // Note that egress selection configuration is not used for this network connection. + // +required + URL string + + // discoveryURL, if specified, overrides the URL used to fetch discovery + // information instead of using "{url}/.well-known/openid-configuration". + // The exact value specified is used, so "/.well-known/openid-configuration" + // must be included in discoveryURL if needed. + // + // The "issuer" field in the fetched discovery information must match the "issuer.url" field + // in the AuthenticationConfiguration and will be used to validate the "iss" claim in the presented JWT. + // This is for scenarios where the well-known and jwks endpoints are hosted at a different + // location than the issuer (such as locally in the cluster). + // + // Example: + // A discovery url that is exposed using kubernetes service 'oidc' in namespace 'oidc-namespace' + // and discovery information is available at '/.well-known/openid-configuration'. + // discoveryURL: "https://oidc.oidc-namespace/.well-known/openid-configuration" + // certificateAuthority is used to verify the TLS connection and the hostname on the leaf certificate + // must be set to 'oidc.oidc-namespace'. + // + // curl https://oidc.oidc-namespace/.well-known/openid-configuration (.discoveryURL field) + // { + // issuer: "https://oidc.example.com" (.url field) + // } + // + // discoveryURL must be different from url. + // Required to be unique across all JWT authenticators. + // Note that egress selection configuration is not used for this network connection. + // +optional + DiscoveryURL string + + // certificateAuthority contains PEM-encoded certificate authority certificates + // used to validate the connection when fetching discovery information. + // If unset, the system verifier is used. + // Same value as the content of the file referenced by the --oidc-ca-file flag. + // +optional + CertificateAuthority string + + // audiences is the set of acceptable audiences the JWT must be issued to. + // At least one of the entries must match the "aud" claim in presented JWTs. + // Same value as the --oidc-client-id flag (though this field supports an array). + // Required to be non-empty. + // +required + Audiences []string + + // audienceMatchPolicy defines how the "audiences" field is used to match the "aud" claim in the presented JWT. + // Allowed values are: + // 1. "MatchAny" when multiple audiences are specified and + // 2. empty (or unset) or "MatchAny" when a single audience is specified. + // + // - MatchAny: the "aud" claim in the presented JWT must match at least one of the entries in the "audiences" field. + // For example, if "audiences" is ["foo", "bar"], the "aud" claim in the presented JWT must contain either "foo" or "bar" (and may contain both). + // + // - "": The match policy can be empty (or unset) when a single audience is specified in the "audiences" field. The "aud" claim in the presented JWT must contain the single audience (and may contain others). + // + // For more nuanced audience validation, use claimValidationRules. + // example: claimValidationRule[].expression: 'sets.equivalent(claims.aud, ["bar", "foo", "baz"])' to require an exact match. + // +optional + AudienceMatchPolicy AudienceMatchPolicyType +} + +// AudienceMatchPolicyType is a set of valid values for issuer.audienceMatchPolicy +type AudienceMatchPolicyType string + +// Valid types for AudienceMatchPolicyType +const ( + // MatchAny means the "aud" claim in the presented JWT must match at least one of the entries in the "audiences" field. + AudienceMatchPolicyMatchAny AudienceMatchPolicyType = "MatchAny" +) + +// ClaimValidationRule provides the configuration for a single claim validation rule. +type ClaimValidationRule struct { + // claim is the name of a required claim. + // Same as --oidc-required-claim flag. + // Only string claim keys are supported. + // Mutually exclusive with expression and message. + // +optional + Claim string + // requiredValue is the value of a required claim. + // Same as --oidc-required-claim flag. + // Only string claim values are supported. + // If claim is set and requiredValue is not set, the claim must be present with a value set to the empty string. + // Mutually exclusive with expression and message. + // +optional + RequiredValue string + + // expression represents the expression which will be evaluated by CEL. + // Must produce a boolean. + // + // CEL expressions have access to the contents of the token claims, organized into CEL variable: + // - 'claims' is a map of claim names to claim values. + // For example, a variable named 'sub' can be accessed as 'claims.sub'. + // Nested claims can be accessed using dot notation, e.g. 'claims.foo.bar'. + // Must return true for the validation to pass. + // + // Documentation on CEL: https://kubernetes.io/docs/reference/using-api/cel/ + // + // Mutually exclusive with claim and requiredValue. + // +optional + Expression string + // message customizes the returned error message when expression returns false. + // message is a literal string. + // Mutually exclusive with claim and requiredValue. + // +optional + Message string +} + +// ClaimMappings provides the configuration for claim mapping +type ClaimMappings struct { + // username represents an option for the username attribute. + // The claim's value must be a singular string. + // Same as the --oidc-username-claim and --oidc-username-prefix flags. + // If username.expression is set, the expression must produce a string value. + // If username.expression uses 'claims.email', then 'claims.email_verified' must be used in + // username.expression or extra[*].valueExpression or claimValidationRules[*].expression. + // An example claim validation rule expression that matches the validation automatically + // applied when username.claim is set to 'email' is 'claims.?email_verified.orValue(true) == true'. By explicitly comparing + // the value to true, we let type-checking see the result will be a boolean, and to make sure a non-boolean email_verified + // claim will be caught at runtime. + // + // In the flag based approach, the --oidc-username-claim and --oidc-username-prefix are optional. If --oidc-username-claim is not set, + // the default value is "sub". For the authentication config, there is no defaulting for claim or prefix. The claim and prefix must be set explicitly. + // For claim, if --oidc-username-claim was not set with legacy flag approach, configure username.claim="sub" in the authentication config. + // For prefix: + // (1) --oidc-username-prefix="-", no prefix was added to the username. For the same behavior using authentication config, + // set username.prefix="" + // (2) --oidc-username-prefix="" and --oidc-username-claim != "email", prefix was "#". For the same + // behavior using authentication config, set username.prefix="#" + // (3) --oidc-username-prefix="". For the same behavior using authentication config, set username.prefix="" + // +required + Username PrefixedClaimOrExpression + // groups represents an option for the groups attribute. + // The claim's value must be a string or string array claim. + // If groups.claim is set, the prefix must be specified (and can be the empty string). + // If groups.expression is set, the expression must produce a string or string array value. + // "", [], and null values are treated as the group mapping not being present. + // +optional + Groups PrefixedClaimOrExpression + + // uid represents an option for the uid attribute. + // Claim must be a singular string claim. + // If uid.expression is set, the expression must produce a string value. + // +optional + UID ClaimOrExpression + + // extra represents an option for the extra attribute. + // expression must produce a string or string array value. + // If the value is empty, the extra mapping will not be present. + // + // hard-coded extra key/value + // - key: "foo" + // valueExpression: "'bar'" + // This will result in an extra attribute - foo: ["bar"] + // + // hard-coded key, value copying claim value + // - key: "foo" + // valueExpression: "claims.some_claim" + // This will result in an extra attribute - foo: [value of some_claim] + // + // hard-coded key, value derived from claim value + // - key: "admin" + // valueExpression: '(has(claims.is_admin) && claims.is_admin) ? "true":""' + // This will result in: + // - if is_admin claim is present and true, extra attribute - admin: ["true"] + // - if is_admin claim is present and false or is_admin claim is not present, no extra attribute will be added + // + // +optional + Extra []ExtraMapping +} + +// PrefixedClaimOrExpression provides the configuration for a single prefixed claim or expression. +type PrefixedClaimOrExpression struct { + // claim is the JWT claim to use. + // Mutually exclusive with expression. + // +optional + Claim string + // prefix is prepended to claim's value to prevent clashes with existing names. + // prefix needs to be set if claim is set and can be the empty string. + // Mutually exclusive with expression. + // +optional + Prefix *string + + // expression represents the expression which will be evaluated by CEL. + // + // CEL expressions have access to the contents of the token claims, organized into CEL variable: + // - 'claims' is a map of claim names to claim values. + // For example, a variable named 'sub' can be accessed as 'claims.sub'. + // Nested claims can be accessed using dot notation, e.g. 'claims.foo.bar'. + // + // Documentation on CEL: https://kubernetes.io/docs/reference/using-api/cel/ + // + // Mutually exclusive with claim and prefix. + // +optional + Expression string +} + +// ClaimOrExpression provides the configuration for a single claim or expression. +type ClaimOrExpression struct { + // claim is the JWT claim to use. + // Either claim or expression must be set. + // Mutually exclusive with expression. + // +optional + Claim string + + // expression represents the expression which will be evaluated by CEL. + // + // CEL expressions have access to the contents of the token claims, organized into CEL variable: + // - 'claims' is a map of claim names to claim values. + // For example, a variable named 'sub' can be accessed as 'claims.sub'. + // Nested claims can be accessed using dot notation, e.g. 'claims.foo.bar'. + // + // Documentation on CEL: https://kubernetes.io/docs/reference/using-api/cel/ + // + // Mutually exclusive with claim. + // +optional + Expression string +} + +// ExtraMapping provides the configuration for a single extra mapping. +type ExtraMapping struct { + // key is a string to use as the extra attribute key. + // key must be a domain-prefix path (e.g. example.org/foo). All characters before the first "/" must be a valid + // subdomain as defined by RFC 1123. All characters trailing the first "/" must + // be valid HTTP Path characters as defined by RFC 3986. + // key must be lowercase. + // Required to be unique. + // +required + Key string + + // valueExpression is a CEL expression to extract extra attribute value. + // valueExpression must produce a string or string array value. + // "", [], and null values are treated as the extra mapping not being present. + // Empty string values contained within a string array are filtered out. + // + // CEL expressions have access to the contents of the token claims, organized into CEL variable: + // - 'claims' is a map of claim names to claim values. + // For example, a variable named 'sub' can be accessed as 'claims.sub'. + // Nested claims can be accessed using dot notation, e.g. 'claims.foo.bar'. + // + // Documentation on CEL: https://kubernetes.io/docs/reference/using-api/cel/ + // + // +required + ValueExpression string +} + +// UserValidationRule provides the configuration for a single user info validation rule. +type UserValidationRule struct { + // expression represents the expression which will be evaluated by CEL. + // Must return true for the validation to pass. + // + // CEL expressions have access to the contents of UserInfo, organized into CEL variable: + // - 'user' - authentication.k8s.io/v1, Kind=UserInfo object + // Refer to https://github.com/kubernetes/api/blob/release-1.28/authentication/v1/types.go#L105-L122 for the definition. + // API documentation: https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.28/#userinfo-v1-authentication-k8s-io + // + // Documentation on CEL: https://kubernetes.io/docs/reference/using-api/cel/ + // + // +required + Expression string + + // message customizes the returned error message when rule returns false. + // message is a literal string. + // +optional + Message string +} + +// ExternalClaimsSource provides the configuration for a single external claim source. +type ExternalClaimsSource struct { + // authentication is an optional field that configures how the apiserver authenticates with an external claims source. + // When not specified, anonymous authentication is used. + // +optional + Authentication *Authentication + // tls is an optional field that configures the http client TLS + // settings when fetching external claims from this source. + // At least one subfield must be set when this field is specified. + // +optional + TLS *TLS + // url is a required configuration of the URL + // for which the external claims are located. + // +required + URL *SourceURL + // mappings is a required list of the claim + // and response handling expression pairs + // that produces the claims from the external source. + // mappings must have at least 1 entry and must not exceed 16 entries. + // Entries must have a unique name across all external claim sources. + // + // WARNING: claims sourced using these mappings will override any claims + // that exist within the token during the claim-to-identity mapping + // process. Use caution when sourcing external claims to avoid unintentionally + // overriding token claims. To help guard against this, sourcing + // external claims can have guard conditions defined in the 'conditions' + // field. + // + // +required + Mappings []SourcedClaimMapping + // conditions is an optional list of conditions in + // which claims should attempt to be fetched from this + // external source. + // When omitted or empty, claims are always attempted to be fetched + // from this external source. + // When specified, all conditions must evaluate to 'true' + // before claims are attempted to be fetched from this external source. + // conditions must not exceed 16 entries. + // Entries must have unique expressions. + // +optional + Conditions []ExternalSourceCondition +} + +// TLS configures the TLS options that the apiserver uses as a client +// when making a request to the external claim source. +// At least one field must be set when specified. +type TLS struct { + // certificateAuthority is an optional field that configures the certificate authority + // used to validate TLS connections with the external claims source. + // Must not be empty and must be a valid PEM-encoded certificate. + // +optional + CertificateAuthority *string +} + +func (t TLS) IsZero() bool { + return t.CertificateAuthority == nil +} + +// Authentication configures how the apiserver should attempt to authenticate +// with an external claims source. +type Authentication struct { + // type is a required field that sets the type of + // authentication method used by the authenticator + // when fetching external claims. + // + // Allowed values are 'RequestProvidedToken' and 'ClientCredential'. + // + // When set to 'RequestProvidedToken', the authenticator will + // use the token provided to the kube-apiserver as part of the + // request to authenticate with the external claims source. + // + // When set to 'ClientCredential', the authenticator will + // use the configured client-id, client-secret, and token endpoint + // to fetch an access token using the OAuth2 client credentials grant + // flow. The fetched access token will then be used to authenticate + // with the external claims source. + // +required + Type *AuthenticationType + + // clientCredential configures the client credentials + // and token endpoint to use to get an access token. + // This field must be set when type is ClientCredential. + // This field must not be set when type is not ClientCredential. + // +optional + ClientCredential *ClientCredentialConfig +} + +// AuthenticationType is the type of authentication that should be used +// when fetching claims from an external source. +type AuthenticationType string + +const ( + // AuthenticationTypeRequestProvidedToken is an AuthenticationType + // that represents that the token being evaluated for authentication + // should be used for authenticating with the external claims source. + // This is useful for scenarios where a token has multiple audiences + // and scopes so that it can be used to access both the cluster and + // the UserInfo endpoint that contains additional information about the + // user not present in the token. + AuthenticationTypeRequestProvidedToken AuthenticationType = "RequestProvidedToken" + + // AuthenticationTypeClientCredential is an AuthenticationType + // that represents that the authenticator should use the OAuth2 + // client credentials grant flow to obtain an access token for + // authenticating with the external claims source. + // This is useful for scenarios such as fetching user information + // from Microsoft's Graph API where a separate client credential + // is needed to access the API. + AuthenticationTypeClientCredential AuthenticationType = "ClientCredential" +) + +// ClientCredentialConfig configures the client credentials and token endpoint +// to use to get an access token via the OAuth2 client credentials grant flow. +type ClientCredentialConfig struct { + // clientID is the client identifier to use during the OAuth2 client credentials flow. + // clientID must not be an empty string (""). + // clientID must only contain printable ASCII characters. + // +required + ClientID string + + // clientSecret is the client secret to use during the OAuth2 client credentials flow. + // clientSecret is the literal string value of the client secret. + // clientSecret must not be an empty string (""). + // clientSecret must only contain printable ASCII characters. + // +required + ClientSecret string + + // tokenEndpoint is a required URL to query for an access token using + // the client credential OAuth2 flow. + // tokenEndpoint must not be an empty string (""). + // tokenEndpoint must be a valid HTTPS URL. + // tokenEndpoint must have a host and a path. + // tokenEndpoint must not contain query parameters, fragments, + // or user information (e.g., "user:password@host"). + // +required + TokenEndpoint string + + // scopes is an optional list of OAuth2 scopes to request when obtaining + // an access token. If not specified, the token endpoint's default scopes + // will be used. Each scope must not be an empty string (""). + // +optional + Scopes []string + + // tls is an optional field that configures the http client TLS + // settings when fetching an access token for this source. + // At least one subfield must be set when this field is specified. + // +optional + TLS *TLS +} + +// SourceURL configures the options used to build the URL that is queried for external claims. +type SourceURL struct { + // hostname is a required hostname for which the external claims are located. + // It must be a valid DNS subdomain name as per RFC1123. + // This means that it must start and end with a lowercase alphanumeric character, + // must only consist of lowercase alphanumeric characters, '-', and '.'. + // hostname must not be an empty string ("") and must not exceed 253 characters in length. + // hostname may optionally specify a port in the format ':{port}'. + // +required + Hostname *string + // pathExpression is a required CEL expression that returns a list + // of string values used to construct the URL path. + // Claims from the token used for the request to the kube-apiserver + // are made available via the `claims` variable. + // expression must not be an empty string (""). + // +required + PathExpression *string +} + +// SourcedClaimMapping configures the mapping behavior for a single external claim +// from the response the apiserver received from the external claim source. +type SourcedClaimMapping struct { + // name is a required name of the claim that + // will be produced and made available during + // the claim-to-identity mapping process. + // name must consist of only lowercase alpha characters and underscores ('_'). + // name must not be an empty string ("") and must not exceed 256 characters in length. + // +required + Name *string + + // expression is a required CEL expression that + // will produce a value to be assigned to the claim. + // The full response body from the request to the + // external claim source is provided via the + // `response` variable. + // expression must not be an empty string (""). + // +required + Expression *string +} + +// ExternalSourceCondition configures a singular condition +// that must return true before the external source is queried +// to retrieve external claims. +type ExternalSourceCondition struct { + // expression is a required CEL expression that + // is used to determine whether or not an external + // source should be used to fetch external claims. + // The expression must return a boolean value, + // where true means that the source should be consulted + // and false means that it should not. + // Claims from the token used for the request to the kube-apiserver + // are made available via the `claims` variable. + // expression must not be an empty string (""). + // +required + Expression *string +} diff --git a/vendor/github.com/openshift/oauth-apiserver/pkg/externaloidc/apis/authentication/v1alpha1/doc.go b/vendor/github.com/openshift/oauth-apiserver/pkg/externaloidc/apis/authentication/v1alpha1/doc.go new file mode 100644 index 0000000000..9946dec99b --- /dev/null +++ b/vendor/github.com/openshift/oauth-apiserver/pkg/externaloidc/apis/authentication/v1alpha1/doc.go @@ -0,0 +1,6 @@ +// +k8s:conversion-gen=github.com/openshift/oauth-apiserver/pkg/externaloidc/apis/authentication +// +k8s:deepcopy-gen=package,register + +// +groupName=authentication.openshift.io +// Package v1alpha1 is the v1alpha1 version of the API. +package v1alpha1 diff --git a/vendor/github.com/openshift/oauth-apiserver/pkg/externaloidc/apis/authentication/v1alpha1/register.go b/vendor/github.com/openshift/oauth-apiserver/pkg/externaloidc/apis/authentication/v1alpha1/register.go new file mode 100644 index 0000000000..1f062a1bf4 --- /dev/null +++ b/vendor/github.com/openshift/oauth-apiserver/pkg/externaloidc/apis/authentication/v1alpha1/register.go @@ -0,0 +1,40 @@ +package v1alpha1 + +import ( + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + + "github.com/openshift/oauth-apiserver/pkg/externaloidc/apis/authentication" +) + +const ( + GroupName = "authentication.openshift.io" +) + +var ( + schemeBuilder = runtime.NewSchemeBuilder( + authentication.Install, + addKnownTypes, + ) + Install = schemeBuilder.AddToScheme + + // DEPRECATED kept for generated code + SchemeGroupVersion = schema.GroupVersion{Group: GroupName, Version: "v1alpha1"} + // DEPRECATED kept for generated code + AddToScheme = schemeBuilder.AddToScheme + localSchemeBuilder = &schemeBuilder +) + +// Resource kept for generated code +// DEPRECATED +func Resource(resource string) schema.GroupResource { + return SchemeGroupVersion.WithResource(resource).GroupResource() +} + +// Adds the list of known types to api.Scheme. +func addKnownTypes(scheme *runtime.Scheme) error { + scheme.AddKnownTypes(SchemeGroupVersion, + &AuthenticationConfiguration{}, + ) + return nil +} diff --git a/vendor/github.com/openshift/oauth-apiserver/pkg/externaloidc/apis/authentication/v1alpha1/types.go b/vendor/github.com/openshift/oauth-apiserver/pkg/externaloidc/apis/authentication/v1alpha1/types.go new file mode 100644 index 0000000000..e354e81ad0 --- /dev/null +++ b/vendor/github.com/openshift/oauth-apiserver/pkg/externaloidc/apis/authentication/v1alpha1/types.go @@ -0,0 +1,552 @@ +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +/* + +The types in this file are heavily inspired by the existing +Kubernetes AuthenticationConfiguration type found at +https://github.com/kubernetes/kubernetes/blob/b2f73c0d6b427e2ab5ba225375aaefc0b9bc45b2/staging/src/k8s.io/apiserver/pkg/apis/apiserver/v1/types.go#L56 + +The API surface here intentionally aligns with this API because it is meant +to be a wrapper around that API with some customization to support unique +functionality that OpenShift needs. + +*/ + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +type AuthenticationConfiguration struct { + metav1.TypeMeta + + // jwt is a list of authenticator to authenticate Kubernetes users using + // JWT compliant tokens. The authenticator will attempt to parse a raw ID token, + // verify it's been signed by the configured issuer. The public key to verify the + // signature is discovered from the issuer's public endpoint using OIDC discovery. + // For an incoming token, each JWT authenticator will be attempted in + // the order in which it is specified in this list. Note however that + // other authenticators may run before or after the JWT authenticators. + // The specific position of JWT authenticators in relation to other + // authenticators is neither defined nor stable across releases. Since + // each JWT authenticator must have a unique issuer URL, at most one + // JWT authenticator will attempt to cryptographically validate the token. + // + // The minimum valid JWT payload must contain the following claims: + // { + // "iss": "https://issuer.example.com", + // "aud": ["audience"], + // "exp": 1234567890, + // "": "username" + // } + JWT []JWTAuthenticator `json:"jwt,omitempty"` +} + +type JWTAuthenticator struct { + // issuer contains the basic OIDC provider connection options. + // +required + Issuer *Issuer `json:"issuer,omitempty"` + + // claimValidationRules are rules that are applied to validate token claims to authenticate users. + // +optional + ClaimValidationRules []ClaimValidationRule `json:"claimValidationRules,omitempty"` + + // claimMappings points claims of a token to be treated as user attributes. + // +required + ClaimMappings *ClaimMappings `json:"claimMappings,omitempty"` + + // userValidationRules are rules that are applied to final user before completing authentication. + // These allow invariants to be applied to incoming identities such as preventing the + // use of the system: prefix that is commonly used by Kubernetes components. + // The validation rules are logically ANDed together and must all return true for the validation to pass. + // +optional + UserValidationRules []UserValidationRule `json:"userValidationRules,omitempty"` + + // externalClaimSources is an optional field that can be used to configure + // sources, external to the token provided in a request, in which claims + // should be fetched from and made available to the claim mapping process + // that is used to build the identity of a token holder. + // For example, fetching additional user metadata from an OIDC provider's UserInfo endpoint. + // externalClaimSources must not exceed 5 entries. + // +optional + ExternalClaimsSources []ExternalClaimsSource `json:"externalClaimsSources,omitempty"` +} + +// Issuer provides the configuration for an external provider's specific settings. +type Issuer struct { + // url points to the issuer URL in a format https://url or https://url/path. + // This must match the "iss" claim in the presented JWT, and the issuer returned from discovery. + // Same value as the --oidc-issuer-url flag. + // Discovery information is fetched from "{url}/.well-known/openid-configuration" unless overridden by discoveryURL. + // Required to be unique across all JWT authenticators. + // Note that egress selection configuration is not used for this network connection. + // +required + URL string `json:"url"` + + // discoveryURL, if specified, overrides the URL used to fetch discovery + // information instead of using "{url}/.well-known/openid-configuration". + // The exact value specified is used, so "/.well-known/openid-configuration" + // must be included in discoveryURL if needed. + // + // The "issuer" field in the fetched discovery information must match the "issuer.url" field + // in the AuthenticationConfiguration and will be used to validate the "iss" claim in the presented JWT. + // This is for scenarios where the well-known and jwks endpoints are hosted at a different + // location than the issuer (such as locally in the cluster). + // + // Example: + // A discovery url that is exposed using kubernetes service 'oidc' in namespace 'oidc-namespace' + // and discovery information is available at '/.well-known/openid-configuration'. + // discoveryURL: "https://oidc.oidc-namespace/.well-known/openid-configuration" + // certificateAuthority is used to verify the TLS connection and the hostname on the leaf certificate + // must be set to 'oidc.oidc-namespace'. + // + // curl https://oidc.oidc-namespace/.well-known/openid-configuration (.discoveryURL field) + // { + // issuer: "https://oidc.example.com" (.url field) + // } + // + // discoveryURL must be different from url. + // Required to be unique across all JWT authenticators. + // Note that egress selection configuration is not used for this network connection. + // +optional + DiscoveryURL string `json:"discoveryURL,omitempty"` + + // certificateAuthority contains PEM-encoded certificate authority certificates + // used to validate the connection when fetching discovery information. + // If unset, the system verifier is used. + // Same value as the content of the file referenced by the --oidc-ca-file flag. + // +optional + CertificateAuthority string `json:"certificateAuthority,omitempty"` + + // audiences is the set of acceptable audiences the JWT must be issued to. + // At least one of the entries must match the "aud" claim in presented JWTs. + // Same value as the --oidc-client-id flag (though this field supports an array). + // Required to be non-empty. + // +required + Audiences []string `json:"audiences"` + + // audienceMatchPolicy defines how the "audiences" field is used to match the "aud" claim in the presented JWT. + // Allowed values are: + // 1. "MatchAny" when multiple audiences are specified and + // 2. empty (or unset) or "MatchAny" when a single audience is specified. + // + // - MatchAny: the "aud" claim in the presented JWT must match at least one of the entries in the "audiences" field. + // For example, if "audiences" is ["foo", "bar"], the "aud" claim in the presented JWT must contain either "foo" or "bar" (and may contain both). + // + // - "": The match policy can be empty (or unset) when a single audience is specified in the "audiences" field. The "aud" claim in the presented JWT must contain the single audience (and may contain others). + // + // For more nuanced audience validation, use claimValidationRules. + // example: claimValidationRule[].expression: 'sets.equivalent(claims.aud, ["bar", "foo", "baz"])' to require an exact match. + // +optional + AudienceMatchPolicy AudienceMatchPolicyType `json:"audienceMatchPolicy,omitempty"` +} + +// AudienceMatchPolicyType is a set of valid values for issuer.audienceMatchPolicy +type AudienceMatchPolicyType string + +// Valid types for AudienceMatchPolicyType +const ( + // MatchAny means the "aud" claim in the presented JWT must match at least one of the entries in the "audiences" field. + AudienceMatchPolicyMatchAny AudienceMatchPolicyType = "MatchAny" +) + +// ClaimValidationRule provides the configuration for a single claim validation rule. +type ClaimValidationRule struct { + // claim is the name of a required claim. + // Same as --oidc-required-claim flag. + // Only string claim keys are supported. + // Mutually exclusive with expression and message. + // +optional + Claim string `json:"claim,omitempty"` + // requiredValue is the value of a required claim. + // Same as --oidc-required-claim flag. + // Only string claim values are supported. + // If claim is set and requiredValue is not set, the claim must be present with a value set to the empty string. + // Mutually exclusive with expression and message. + // +optional + RequiredValue string `json:"requiredValue,omitempty"` + + // expression represents the expression which will be evaluated by CEL. + // Must produce a boolean. + // + // CEL expressions have access to the contents of the token claims, organized into CEL variable: + // - 'claims' is a map of claim names to claim values. + // For example, a variable named 'sub' can be accessed as 'claims.sub'. + // Nested claims can be accessed using dot notation, e.g. 'claims.foo.bar'. + // Must return true for the validation to pass. + // + // Documentation on CEL: https://kubernetes.io/docs/reference/using-api/cel/ + // + // Mutually exclusive with claim and requiredValue. + // +optional + Expression string `json:"expression,omitempty"` + // message customizes the returned error message when expression returns false. + // message is a literal string. + // Mutually exclusive with claim and requiredValue. + // +optional + Message string `json:"message,omitempty"` +} + +// ClaimMappings provides the configuration for claim mapping +type ClaimMappings struct { + // username represents an option for the username attribute. + // The claim's value must be a singular string. + // Same as the --oidc-username-claim and --oidc-username-prefix flags. + // If username.expression is set, the expression must produce a string value. + // If username.expression uses 'claims.email', then 'claims.email_verified' must be used in + // username.expression or extra[*].valueExpression or claimValidationRules[*].expression. + // An example claim validation rule expression that matches the validation automatically + // applied when username.claim is set to 'email' is 'claims.?email_verified.orValue(true) == true'. By explicitly comparing + // the value to true, we let type-checking see the result will be a boolean, and to make sure a non-boolean email_verified + // claim will be caught at runtime. + // + // In the flag based approach, the --oidc-username-claim and --oidc-username-prefix are optional. If --oidc-username-claim is not set, + // the default value is "sub". For the authentication config, there is no defaulting for claim or prefix. The claim and prefix must be set explicitly. + // For claim, if --oidc-username-claim was not set with legacy flag approach, configure username.claim="sub" in the authentication config. + // For prefix: + // (1) --oidc-username-prefix="-", no prefix was added to the username. For the same behavior using authentication config, + // set username.prefix="" + // (2) --oidc-username-prefix="" and --oidc-username-claim != "email", prefix was "#". For the same + // behavior using authentication config, set username.prefix="#" + // (3) --oidc-username-prefix="". For the same behavior using authentication config, set username.prefix="" + // +required + Username PrefixedClaimOrExpression `json:"username"` + // groups represents an option for the groups attribute. + // The claim's value must be a string or string array claim. + // If groups.claim is set, the prefix must be specified (and can be the empty string). + // If groups.expression is set, the expression must produce a string or string array value. + // "", [], and null values are treated as the group mapping not being present. + // +optional + Groups PrefixedClaimOrExpression `json:"groups,omitempty"` + + // uid represents an option for the uid attribute. + // Claim must be a singular string claim. + // If uid.expression is set, the expression must produce a string value. + // +optional + UID ClaimOrExpression `json:"uid"` + + // extra represents an option for the extra attribute. + // expression must produce a string or string array value. + // If the value is empty, the extra mapping will not be present. + // + // hard-coded extra key/value + // - key: "foo" + // valueExpression: "'bar'" + // This will result in an extra attribute - foo: ["bar"] + // + // hard-coded key, value copying claim value + // - key: "foo" + // valueExpression: "claims.some_claim" + // This will result in an extra attribute - foo: [value of some_claim] + // + // hard-coded key, value derived from claim value + // - key: "admin" + // valueExpression: '(has(claims.is_admin) && claims.is_admin) ? "true":""' + // This will result in: + // - if is_admin claim is present and true, extra attribute - admin: ["true"] + // - if is_admin claim is present and false or is_admin claim is not present, no extra attribute will be added + // + // +optional + Extra []ExtraMapping `json:"extra,omitempty"` +} + +// PrefixedClaimOrExpression provides the configuration for a single prefixed claim or expression. +type PrefixedClaimOrExpression struct { + // claim is the JWT claim to use. + // Mutually exclusive with expression. + // +optional + Claim string `json:"claim,omitempty"` + // prefix is prepended to claim's value to prevent clashes with existing names. + // prefix needs to be set if claim is set and can be the empty string. + // Mutually exclusive with expression. + // +optional + Prefix *string `json:"prefix,omitempty"` + + // expression represents the expression which will be evaluated by CEL. + // + // CEL expressions have access to the contents of the token claims, organized into CEL variable: + // - 'claims' is a map of claim names to claim values. + // For example, a variable named 'sub' can be accessed as 'claims.sub'. + // Nested claims can be accessed using dot notation, e.g. 'claims.foo.bar'. + // + // Documentation on CEL: https://kubernetes.io/docs/reference/using-api/cel/ + // + // Mutually exclusive with claim and prefix. + // +optional + Expression string `json:"expression,omitempty"` +} + +// ClaimOrExpression provides the configuration for a single claim or expression. +type ClaimOrExpression struct { + // claim is the JWT claim to use. + // Either claim or expression must be set. + // Mutually exclusive with expression. + // +optional + Claim string `json:"claim,omitempty"` + + // expression represents the expression which will be evaluated by CEL. + // + // CEL expressions have access to the contents of the token claims, organized into CEL variable: + // - 'claims' is a map of claim names to claim values. + // For example, a variable named 'sub' can be accessed as 'claims.sub'. + // Nested claims can be accessed using dot notation, e.g. 'claims.foo.bar'. + // + // Documentation on CEL: https://kubernetes.io/docs/reference/using-api/cel/ + // + // Mutually exclusive with claim. + // +optional + Expression string `json:"expression,omitempty"` +} + +// ExtraMapping provides the configuration for a single extra mapping. +type ExtraMapping struct { + // key is a string to use as the extra attribute key. + // key must be a domain-prefix path (e.g. example.org/foo). All characters before the first "/" must be a valid + // subdomain as defined by RFC 1123. All characters trailing the first "/" must + // be valid HTTP Path characters as defined by RFC 3986. + // key must be lowercase. + // Required to be unique. + // +required + Key string `json:"key"` + + // valueExpression is a CEL expression to extract extra attribute value. + // valueExpression must produce a string or string array value. + // "", [], and null values are treated as the extra mapping not being present. + // Empty string values contained within a string array are filtered out. + // + // CEL expressions have access to the contents of the token claims, organized into CEL variable: + // - 'claims' is a map of claim names to claim values. + // For example, a variable named 'sub' can be accessed as 'claims.sub'. + // Nested claims can be accessed using dot notation, e.g. 'claims.foo.bar'. + // + // Documentation on CEL: https://kubernetes.io/docs/reference/using-api/cel/ + // + // +required + ValueExpression string `json:"valueExpression"` +} + +// UserValidationRule provides the configuration for a single user info validation rule. +type UserValidationRule struct { + // expression represents the expression which will be evaluated by CEL. + // Must return true for the validation to pass. + // + // CEL expressions have access to the contents of UserInfo, organized into CEL variable: + // - 'user' - authentication.k8s.io/v1, Kind=UserInfo object + // Refer to https://github.com/kubernetes/api/blob/release-1.28/authentication/v1/types.go#L105-L122 for the definition. + // API documentation: https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.28/#userinfo-v1-authentication-k8s-io + // + // Documentation on CEL: https://kubernetes.io/docs/reference/using-api/cel/ + // + // +required + Expression string `json:"expression"` + + // message customizes the returned error message when rule returns false. + // message is a literal string. + // +optional + Message string `json:"message,omitempty"` +} + +// ExternalClaimsSource provides the configuration for a single external claim source. +type ExternalClaimsSource struct { + // authentication is an optional field that configures how the apiserver authenticates with an external claims source. + // When not specified, anonymous authentication is used. + // +optional + Authentication *Authentication `json:"authentication,omitempty"` + // tls is an optional field that configures the http client TLS + // settings when fetching external claims from this source. + // At least one subfield must be set when this field is specified. + // +optional + TLS *TLS `json:"tls,omitempty"` + // url is a required configuration of the URL + // for which the external claims are located. + // +required + URL *SourceURL `json:"url,omitempty"` + // mappings is a required list of the claim + // and response handling expression pairs + // that produces the claims from the external source. + // + // mappings must have at least 1 entry and must not exceed 16 entries. + // Entries must have a unique name across all external claim sources. + // + // WARNING: claims sourced using these mappings will override any claims + // that exist within the token during the claim-to-identity mapping + // process. Use caution when sourcing external claims to avoid unintentionally + // overriding token claims. To help guard against this, sourcing + // external claims can have guard conditions defined in the 'conditions' + // field. + // + // +required + Mappings []SourcedClaimMapping `json:"mappings,omitempty"` + // conditions is an optional list of conditions in + // which claims should attempt to be fetched from this + // external source. + // When omitted or empty, claims are always attempted to be fetched + // from this external source. + // When specified, all conditions must evaluate to 'true' + // before claims are attempted to be fetched from this external source. + // conditions must not exceed 16 entries. + // Entries must have unique expressions. + // +optional + Conditions []ExternalSourceCondition `json:"conditions,omitempty"` +} + +// TLS configures the TLS options that the apiserver uses as a client +// when making a request to the external claim source. +// At least one field must be set when specified. +type TLS struct { + // certificateAuthority is an optional field that configures the certificate authority + // used to validate TLS connections with the external claims source. + // Must not be empty and must be a valid PEM-encoded certificate. + // +optional + CertificateAuthority *string `json:"certificateAuthority,omitempty"` +} + +// Authentication configures how the apiserver should attempt to authenticate +// with an external claims source. +type Authentication struct { + // type is a required field that sets the type of + // authentication method used by the authenticator + // when fetching external claims. + // + // Allowed values are 'RequestProvidedToken' and 'ClientCredential'. + // + // When set to 'RequestProvidedToken', the authenticator will + // use the token provided to the kube-apiserver as part of the + // request to authenticate with the external claims source. + // + // When set to 'ClientCredential', the authenticator will + // use the configured client-id, client-secret, and token endpoint + // to fetch an access token using the OAuth2 client credentials grant + // flow. The fetched access token will then be used to authenticate + // with the external claims source. + // +required + Type *AuthenticationType `json:"type,omitempty"` + + // clientCredential configures the client credentials + // and token endpoint to use to get an access token. + // This field must be set when type is ClientCredential. + // This field must not be set when type is not ClientCredential. + // +optional + ClientCredential *ClientCredentialConfig `json:"clientCredential,omitempty"` +} + +// AuthenticationType is the type of authentication that should be used +// when fetching claims from an external source. +type AuthenticationType string + +const ( + // AuthenticationTypeRequestProvidedToken is an AuthenticationType + // that represents that the token being evaluated for authentication + // should be used for authenticating with the external claims source. + // This is useful for scenarios where a token has multiple audiences + // and scopes so that it can be used to access both the cluster and + // the UserInfo endpoint that contains additional information about the + // user not present in the token. + AuthenticationTypeRequestProvidedToken AuthenticationType = "RequestProvidedToken" + + // AuthenticationTypeClientCredential is an AuthenticationType + // that represents that the authenticator should use the OAuth2 + // client credentials grant flow to obtain an access token for + // authenticating with the external claims source. + // This is useful for scenarios such as fetching user information + // from Microsoft's Graph API where a separate client credential + // is needed to access the API. + AuthenticationTypeClientCredential AuthenticationType = "ClientCredential" +) + +// ClientCredentialConfig configures the client credentials and token endpoint +// to use to get an access token via the OAuth2 client credentials grant flow. +type ClientCredentialConfig struct { + // clientID is the client identifier to use during the OAuth2 client credentials flow. + // clientID must not be an empty string (""). + // clientID must only contain printable ASCII characters. + // +required + ClientID string `json:"clientID,omitempty"` + + // clientSecret is the client secret to use during the OAuth2 client credentials flow. + // clientSecret is the literal string value of the client secret. + // clientSecret must not be an empty string (""). + // clientSecret must only contain printable ASCII characters. + // +required + ClientSecret string `json:"clientSecret,omitempty"` + + // tokenEndpoint is a required URL to query for an access token using + // the client credential OAuth2 flow. + // tokenEndpoint must not be an empty string (""). + // tokenEndpoint must be a valid HTTPS URL. + // tokenEndpoint must have a host and a path. + // tokenEndpoint must not contain query parameters, fragments, + // or user information (e.g., "user:password@host"). + // +required + TokenEndpoint string `json:"tokenEndpoint,omitempty"` + + // scopes is an optional list of OAuth2 scopes to request when obtaining + // an access token. If not specified, the token endpoint's default scopes + // will be used. Each scope must not be an empty string (""). + // +optional + Scopes []string `json:"scopes,omitempty"` + + // tls is an optional field that configures the http client TLS + // settings when fetching an access token for this source. + // At least one subfield must be set when this field is specified. + // +optional + TLS *TLS `json:"tls,omitempty"` +} + +// SourceURL configures the options used to build the URL that is queried for external claims. +type SourceURL struct { + // hostname is a required hostname for which the external claims are located. + // It must be a valid DNS subdomain name as per RFC1123. + // This means that it must start and end with a lowercase alphanumeric character, + // must only consist of lowercase alphanumeric characters, '-', and '.'. + // hostname must not be an empty string ("") and must not exceed 253 characters in length. + // hostname may optionally specify a port in the format ':{port}'. + // +required + Hostname *string `json:"hostname,omitempty"` + // pathExpression is a required CEL expression that returns a list + // of string values used to construct the URL path. + // Claims from the token used for the request to the kube-apiserver + // are made available via the `claims` variable. + // expression must not be an empty string (""). + // +required + PathExpression *string `json:"pathExpression,omitempty"` +} + +// SourcedClaimMapping configures the mapping behavior for a single external claim +// from the response the apiserver received from the external claim source. +type SourcedClaimMapping struct { + // name is a required name of the claim that + // will be produced and made available during + // the claim-to-identity mapping process. + // name must consist of only lowercase alpha characters and underscores ('_'). + // name must not be an empty string ("") and must not exceed 256 characters in length. + // +required + Name *string `json:"name,omitempty"` + + // expression is a required CEL expression that + // will produce a value to be assigned to the claim. + // The full response body from the request to the + // external claim source is provided via the + // `response` variable. + // expression must not be an empty string (""). + // +required + Expression *string `json:"expression,omitempty"` +} + +// ExternalSourceCondition configures a singular condition +// that must return true before the external source is queried +// to retrieve external claims. +type ExternalSourceCondition struct { + // expression is a required CEL expression that + // is used to determine whether or not an external + // source should be used to fetch external claims. + // The expression must return a boolean value, + // where true means that the source should be consulted + // and false means that it should not. + // Claims from the token used for the request to the kube-apiserver + // are made available via the `claims` variable. + // expression must not be an empty string (""). + // +required + Expression *string `json:"expression,omitempty"` +} diff --git a/vendor/github.com/openshift/oauth-apiserver/pkg/externaloidc/apis/authentication/v1alpha1/zz_generated.conversion.go b/vendor/github.com/openshift/oauth-apiserver/pkg/externaloidc/apis/authentication/v1alpha1/zz_generated.conversion.go new file mode 100644 index 0000000000..307f5a47d3 --- /dev/null +++ b/vendor/github.com/openshift/oauth-apiserver/pkg/externaloidc/apis/authentication/v1alpha1/zz_generated.conversion.go @@ -0,0 +1,576 @@ +//go:build !ignore_autogenerated +// +build !ignore_autogenerated + +// Code generated by conversion-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + unsafe "unsafe" + + authentication "github.com/openshift/oauth-apiserver/pkg/externaloidc/apis/authentication" + conversion "k8s.io/apimachinery/pkg/conversion" + runtime "k8s.io/apimachinery/pkg/runtime" +) + +func init() { + localSchemeBuilder.Register(RegisterConversions) +} + +// RegisterConversions adds conversion functions to the given scheme. +// Public to allow building arbitrary schemes. +func RegisterConversions(s *runtime.Scheme) error { + if err := s.AddGeneratedConversionFunc((*Authentication)(nil), (*authentication.Authentication)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1alpha1_Authentication_To_authentication_Authentication(a.(*Authentication), b.(*authentication.Authentication), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*authentication.Authentication)(nil), (*Authentication)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_authentication_Authentication_To_v1alpha1_Authentication(a.(*authentication.Authentication), b.(*Authentication), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*AuthenticationConfiguration)(nil), (*authentication.AuthenticationConfiguration)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1alpha1_AuthenticationConfiguration_To_authentication_AuthenticationConfiguration(a.(*AuthenticationConfiguration), b.(*authentication.AuthenticationConfiguration), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*authentication.AuthenticationConfiguration)(nil), (*AuthenticationConfiguration)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_authentication_AuthenticationConfiguration_To_v1alpha1_AuthenticationConfiguration(a.(*authentication.AuthenticationConfiguration), b.(*AuthenticationConfiguration), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*ClaimMappings)(nil), (*authentication.ClaimMappings)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1alpha1_ClaimMappings_To_authentication_ClaimMappings(a.(*ClaimMappings), b.(*authentication.ClaimMappings), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*authentication.ClaimMappings)(nil), (*ClaimMappings)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_authentication_ClaimMappings_To_v1alpha1_ClaimMappings(a.(*authentication.ClaimMappings), b.(*ClaimMappings), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*ClaimOrExpression)(nil), (*authentication.ClaimOrExpression)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1alpha1_ClaimOrExpression_To_authentication_ClaimOrExpression(a.(*ClaimOrExpression), b.(*authentication.ClaimOrExpression), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*authentication.ClaimOrExpression)(nil), (*ClaimOrExpression)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_authentication_ClaimOrExpression_To_v1alpha1_ClaimOrExpression(a.(*authentication.ClaimOrExpression), b.(*ClaimOrExpression), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*ClaimValidationRule)(nil), (*authentication.ClaimValidationRule)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1alpha1_ClaimValidationRule_To_authentication_ClaimValidationRule(a.(*ClaimValidationRule), b.(*authentication.ClaimValidationRule), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*authentication.ClaimValidationRule)(nil), (*ClaimValidationRule)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_authentication_ClaimValidationRule_To_v1alpha1_ClaimValidationRule(a.(*authentication.ClaimValidationRule), b.(*ClaimValidationRule), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*ClientCredentialConfig)(nil), (*authentication.ClientCredentialConfig)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1alpha1_ClientCredentialConfig_To_authentication_ClientCredentialConfig(a.(*ClientCredentialConfig), b.(*authentication.ClientCredentialConfig), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*authentication.ClientCredentialConfig)(nil), (*ClientCredentialConfig)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_authentication_ClientCredentialConfig_To_v1alpha1_ClientCredentialConfig(a.(*authentication.ClientCredentialConfig), b.(*ClientCredentialConfig), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*ExternalClaimsSource)(nil), (*authentication.ExternalClaimsSource)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1alpha1_ExternalClaimsSource_To_authentication_ExternalClaimsSource(a.(*ExternalClaimsSource), b.(*authentication.ExternalClaimsSource), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*authentication.ExternalClaimsSource)(nil), (*ExternalClaimsSource)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_authentication_ExternalClaimsSource_To_v1alpha1_ExternalClaimsSource(a.(*authentication.ExternalClaimsSource), b.(*ExternalClaimsSource), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*ExternalSourceCondition)(nil), (*authentication.ExternalSourceCondition)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1alpha1_ExternalSourceCondition_To_authentication_ExternalSourceCondition(a.(*ExternalSourceCondition), b.(*authentication.ExternalSourceCondition), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*authentication.ExternalSourceCondition)(nil), (*ExternalSourceCondition)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_authentication_ExternalSourceCondition_To_v1alpha1_ExternalSourceCondition(a.(*authentication.ExternalSourceCondition), b.(*ExternalSourceCondition), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*ExtraMapping)(nil), (*authentication.ExtraMapping)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1alpha1_ExtraMapping_To_authentication_ExtraMapping(a.(*ExtraMapping), b.(*authentication.ExtraMapping), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*authentication.ExtraMapping)(nil), (*ExtraMapping)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_authentication_ExtraMapping_To_v1alpha1_ExtraMapping(a.(*authentication.ExtraMapping), b.(*ExtraMapping), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*Issuer)(nil), (*authentication.Issuer)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1alpha1_Issuer_To_authentication_Issuer(a.(*Issuer), b.(*authentication.Issuer), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*authentication.Issuer)(nil), (*Issuer)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_authentication_Issuer_To_v1alpha1_Issuer(a.(*authentication.Issuer), b.(*Issuer), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*JWTAuthenticator)(nil), (*authentication.JWTAuthenticator)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1alpha1_JWTAuthenticator_To_authentication_JWTAuthenticator(a.(*JWTAuthenticator), b.(*authentication.JWTAuthenticator), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*authentication.JWTAuthenticator)(nil), (*JWTAuthenticator)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_authentication_JWTAuthenticator_To_v1alpha1_JWTAuthenticator(a.(*authentication.JWTAuthenticator), b.(*JWTAuthenticator), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*PrefixedClaimOrExpression)(nil), (*authentication.PrefixedClaimOrExpression)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1alpha1_PrefixedClaimOrExpression_To_authentication_PrefixedClaimOrExpression(a.(*PrefixedClaimOrExpression), b.(*authentication.PrefixedClaimOrExpression), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*authentication.PrefixedClaimOrExpression)(nil), (*PrefixedClaimOrExpression)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_authentication_PrefixedClaimOrExpression_To_v1alpha1_PrefixedClaimOrExpression(a.(*authentication.PrefixedClaimOrExpression), b.(*PrefixedClaimOrExpression), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*SourceURL)(nil), (*authentication.SourceURL)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1alpha1_SourceURL_To_authentication_SourceURL(a.(*SourceURL), b.(*authentication.SourceURL), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*authentication.SourceURL)(nil), (*SourceURL)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_authentication_SourceURL_To_v1alpha1_SourceURL(a.(*authentication.SourceURL), b.(*SourceURL), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*SourcedClaimMapping)(nil), (*authentication.SourcedClaimMapping)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1alpha1_SourcedClaimMapping_To_authentication_SourcedClaimMapping(a.(*SourcedClaimMapping), b.(*authentication.SourcedClaimMapping), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*authentication.SourcedClaimMapping)(nil), (*SourcedClaimMapping)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_authentication_SourcedClaimMapping_To_v1alpha1_SourcedClaimMapping(a.(*authentication.SourcedClaimMapping), b.(*SourcedClaimMapping), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*TLS)(nil), (*authentication.TLS)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1alpha1_TLS_To_authentication_TLS(a.(*TLS), b.(*authentication.TLS), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*authentication.TLS)(nil), (*TLS)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_authentication_TLS_To_v1alpha1_TLS(a.(*authentication.TLS), b.(*TLS), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*UserValidationRule)(nil), (*authentication.UserValidationRule)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1alpha1_UserValidationRule_To_authentication_UserValidationRule(a.(*UserValidationRule), b.(*authentication.UserValidationRule), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*authentication.UserValidationRule)(nil), (*UserValidationRule)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_authentication_UserValidationRule_To_v1alpha1_UserValidationRule(a.(*authentication.UserValidationRule), b.(*UserValidationRule), scope) + }); err != nil { + return err + } + return nil +} + +func autoConvert_v1alpha1_Authentication_To_authentication_Authentication(in *Authentication, out *authentication.Authentication, s conversion.Scope) error { + out.Type = (*authentication.AuthenticationType)(unsafe.Pointer(in.Type)) + out.ClientCredential = (*authentication.ClientCredentialConfig)(unsafe.Pointer(in.ClientCredential)) + return nil +} + +// Convert_v1alpha1_Authentication_To_authentication_Authentication is an autogenerated conversion function. +func Convert_v1alpha1_Authentication_To_authentication_Authentication(in *Authentication, out *authentication.Authentication, s conversion.Scope) error { + return autoConvert_v1alpha1_Authentication_To_authentication_Authentication(in, out, s) +} + +func autoConvert_authentication_Authentication_To_v1alpha1_Authentication(in *authentication.Authentication, out *Authentication, s conversion.Scope) error { + out.Type = (*AuthenticationType)(unsafe.Pointer(in.Type)) + out.ClientCredential = (*ClientCredentialConfig)(unsafe.Pointer(in.ClientCredential)) + return nil +} + +// Convert_authentication_Authentication_To_v1alpha1_Authentication is an autogenerated conversion function. +func Convert_authentication_Authentication_To_v1alpha1_Authentication(in *authentication.Authentication, out *Authentication, s conversion.Scope) error { + return autoConvert_authentication_Authentication_To_v1alpha1_Authentication(in, out, s) +} + +func autoConvert_v1alpha1_AuthenticationConfiguration_To_authentication_AuthenticationConfiguration(in *AuthenticationConfiguration, out *authentication.AuthenticationConfiguration, s conversion.Scope) error { + out.JWT = *(*[]authentication.JWTAuthenticator)(unsafe.Pointer(&in.JWT)) + return nil +} + +// Convert_v1alpha1_AuthenticationConfiguration_To_authentication_AuthenticationConfiguration is an autogenerated conversion function. +func Convert_v1alpha1_AuthenticationConfiguration_To_authentication_AuthenticationConfiguration(in *AuthenticationConfiguration, out *authentication.AuthenticationConfiguration, s conversion.Scope) error { + return autoConvert_v1alpha1_AuthenticationConfiguration_To_authentication_AuthenticationConfiguration(in, out, s) +} + +func autoConvert_authentication_AuthenticationConfiguration_To_v1alpha1_AuthenticationConfiguration(in *authentication.AuthenticationConfiguration, out *AuthenticationConfiguration, s conversion.Scope) error { + out.JWT = *(*[]JWTAuthenticator)(unsafe.Pointer(&in.JWT)) + return nil +} + +// Convert_authentication_AuthenticationConfiguration_To_v1alpha1_AuthenticationConfiguration is an autogenerated conversion function. +func Convert_authentication_AuthenticationConfiguration_To_v1alpha1_AuthenticationConfiguration(in *authentication.AuthenticationConfiguration, out *AuthenticationConfiguration, s conversion.Scope) error { + return autoConvert_authentication_AuthenticationConfiguration_To_v1alpha1_AuthenticationConfiguration(in, out, s) +} + +func autoConvert_v1alpha1_ClaimMappings_To_authentication_ClaimMappings(in *ClaimMappings, out *authentication.ClaimMappings, s conversion.Scope) error { + if err := Convert_v1alpha1_PrefixedClaimOrExpression_To_authentication_PrefixedClaimOrExpression(&in.Username, &out.Username, s); err != nil { + return err + } + if err := Convert_v1alpha1_PrefixedClaimOrExpression_To_authentication_PrefixedClaimOrExpression(&in.Groups, &out.Groups, s); err != nil { + return err + } + if err := Convert_v1alpha1_ClaimOrExpression_To_authentication_ClaimOrExpression(&in.UID, &out.UID, s); err != nil { + return err + } + out.Extra = *(*[]authentication.ExtraMapping)(unsafe.Pointer(&in.Extra)) + return nil +} + +// Convert_v1alpha1_ClaimMappings_To_authentication_ClaimMappings is an autogenerated conversion function. +func Convert_v1alpha1_ClaimMappings_To_authentication_ClaimMappings(in *ClaimMappings, out *authentication.ClaimMappings, s conversion.Scope) error { + return autoConvert_v1alpha1_ClaimMappings_To_authentication_ClaimMappings(in, out, s) +} + +func autoConvert_authentication_ClaimMappings_To_v1alpha1_ClaimMappings(in *authentication.ClaimMappings, out *ClaimMappings, s conversion.Scope) error { + if err := Convert_authentication_PrefixedClaimOrExpression_To_v1alpha1_PrefixedClaimOrExpression(&in.Username, &out.Username, s); err != nil { + return err + } + if err := Convert_authentication_PrefixedClaimOrExpression_To_v1alpha1_PrefixedClaimOrExpression(&in.Groups, &out.Groups, s); err != nil { + return err + } + if err := Convert_authentication_ClaimOrExpression_To_v1alpha1_ClaimOrExpression(&in.UID, &out.UID, s); err != nil { + return err + } + out.Extra = *(*[]ExtraMapping)(unsafe.Pointer(&in.Extra)) + return nil +} + +// Convert_authentication_ClaimMappings_To_v1alpha1_ClaimMappings is an autogenerated conversion function. +func Convert_authentication_ClaimMappings_To_v1alpha1_ClaimMappings(in *authentication.ClaimMappings, out *ClaimMappings, s conversion.Scope) error { + return autoConvert_authentication_ClaimMappings_To_v1alpha1_ClaimMappings(in, out, s) +} + +func autoConvert_v1alpha1_ClaimOrExpression_To_authentication_ClaimOrExpression(in *ClaimOrExpression, out *authentication.ClaimOrExpression, s conversion.Scope) error { + out.Claim = in.Claim + out.Expression = in.Expression + return nil +} + +// Convert_v1alpha1_ClaimOrExpression_To_authentication_ClaimOrExpression is an autogenerated conversion function. +func Convert_v1alpha1_ClaimOrExpression_To_authentication_ClaimOrExpression(in *ClaimOrExpression, out *authentication.ClaimOrExpression, s conversion.Scope) error { + return autoConvert_v1alpha1_ClaimOrExpression_To_authentication_ClaimOrExpression(in, out, s) +} + +func autoConvert_authentication_ClaimOrExpression_To_v1alpha1_ClaimOrExpression(in *authentication.ClaimOrExpression, out *ClaimOrExpression, s conversion.Scope) error { + out.Claim = in.Claim + out.Expression = in.Expression + return nil +} + +// Convert_authentication_ClaimOrExpression_To_v1alpha1_ClaimOrExpression is an autogenerated conversion function. +func Convert_authentication_ClaimOrExpression_To_v1alpha1_ClaimOrExpression(in *authentication.ClaimOrExpression, out *ClaimOrExpression, s conversion.Scope) error { + return autoConvert_authentication_ClaimOrExpression_To_v1alpha1_ClaimOrExpression(in, out, s) +} + +func autoConvert_v1alpha1_ClaimValidationRule_To_authentication_ClaimValidationRule(in *ClaimValidationRule, out *authentication.ClaimValidationRule, s conversion.Scope) error { + out.Claim = in.Claim + out.RequiredValue = in.RequiredValue + out.Expression = in.Expression + out.Message = in.Message + return nil +} + +// Convert_v1alpha1_ClaimValidationRule_To_authentication_ClaimValidationRule is an autogenerated conversion function. +func Convert_v1alpha1_ClaimValidationRule_To_authentication_ClaimValidationRule(in *ClaimValidationRule, out *authentication.ClaimValidationRule, s conversion.Scope) error { + return autoConvert_v1alpha1_ClaimValidationRule_To_authentication_ClaimValidationRule(in, out, s) +} + +func autoConvert_authentication_ClaimValidationRule_To_v1alpha1_ClaimValidationRule(in *authentication.ClaimValidationRule, out *ClaimValidationRule, s conversion.Scope) error { + out.Claim = in.Claim + out.RequiredValue = in.RequiredValue + out.Expression = in.Expression + out.Message = in.Message + return nil +} + +// Convert_authentication_ClaimValidationRule_To_v1alpha1_ClaimValidationRule is an autogenerated conversion function. +func Convert_authentication_ClaimValidationRule_To_v1alpha1_ClaimValidationRule(in *authentication.ClaimValidationRule, out *ClaimValidationRule, s conversion.Scope) error { + return autoConvert_authentication_ClaimValidationRule_To_v1alpha1_ClaimValidationRule(in, out, s) +} + +func autoConvert_v1alpha1_ClientCredentialConfig_To_authentication_ClientCredentialConfig(in *ClientCredentialConfig, out *authentication.ClientCredentialConfig, s conversion.Scope) error { + out.ClientID = in.ClientID + out.ClientSecret = in.ClientSecret + out.TokenEndpoint = in.TokenEndpoint + out.Scopes = *(*[]string)(unsafe.Pointer(&in.Scopes)) + out.TLS = (*authentication.TLS)(unsafe.Pointer(in.TLS)) + return nil +} + +// Convert_v1alpha1_ClientCredentialConfig_To_authentication_ClientCredentialConfig is an autogenerated conversion function. +func Convert_v1alpha1_ClientCredentialConfig_To_authentication_ClientCredentialConfig(in *ClientCredentialConfig, out *authentication.ClientCredentialConfig, s conversion.Scope) error { + return autoConvert_v1alpha1_ClientCredentialConfig_To_authentication_ClientCredentialConfig(in, out, s) +} + +func autoConvert_authentication_ClientCredentialConfig_To_v1alpha1_ClientCredentialConfig(in *authentication.ClientCredentialConfig, out *ClientCredentialConfig, s conversion.Scope) error { + out.ClientID = in.ClientID + out.ClientSecret = in.ClientSecret + out.TokenEndpoint = in.TokenEndpoint + out.Scopes = *(*[]string)(unsafe.Pointer(&in.Scopes)) + out.TLS = (*TLS)(unsafe.Pointer(in.TLS)) + return nil +} + +// Convert_authentication_ClientCredentialConfig_To_v1alpha1_ClientCredentialConfig is an autogenerated conversion function. +func Convert_authentication_ClientCredentialConfig_To_v1alpha1_ClientCredentialConfig(in *authentication.ClientCredentialConfig, out *ClientCredentialConfig, s conversion.Scope) error { + return autoConvert_authentication_ClientCredentialConfig_To_v1alpha1_ClientCredentialConfig(in, out, s) +} + +func autoConvert_v1alpha1_ExternalClaimsSource_To_authentication_ExternalClaimsSource(in *ExternalClaimsSource, out *authentication.ExternalClaimsSource, s conversion.Scope) error { + out.Authentication = (*authentication.Authentication)(unsafe.Pointer(in.Authentication)) + out.TLS = (*authentication.TLS)(unsafe.Pointer(in.TLS)) + out.URL = (*authentication.SourceURL)(unsafe.Pointer(in.URL)) + out.Mappings = *(*[]authentication.SourcedClaimMapping)(unsafe.Pointer(&in.Mappings)) + out.Conditions = *(*[]authentication.ExternalSourceCondition)(unsafe.Pointer(&in.Conditions)) + return nil +} + +// Convert_v1alpha1_ExternalClaimsSource_To_authentication_ExternalClaimsSource is an autogenerated conversion function. +func Convert_v1alpha1_ExternalClaimsSource_To_authentication_ExternalClaimsSource(in *ExternalClaimsSource, out *authentication.ExternalClaimsSource, s conversion.Scope) error { + return autoConvert_v1alpha1_ExternalClaimsSource_To_authentication_ExternalClaimsSource(in, out, s) +} + +func autoConvert_authentication_ExternalClaimsSource_To_v1alpha1_ExternalClaimsSource(in *authentication.ExternalClaimsSource, out *ExternalClaimsSource, s conversion.Scope) error { + out.Authentication = (*Authentication)(unsafe.Pointer(in.Authentication)) + out.TLS = (*TLS)(unsafe.Pointer(in.TLS)) + out.URL = (*SourceURL)(unsafe.Pointer(in.URL)) + out.Mappings = *(*[]SourcedClaimMapping)(unsafe.Pointer(&in.Mappings)) + out.Conditions = *(*[]ExternalSourceCondition)(unsafe.Pointer(&in.Conditions)) + return nil +} + +// Convert_authentication_ExternalClaimsSource_To_v1alpha1_ExternalClaimsSource is an autogenerated conversion function. +func Convert_authentication_ExternalClaimsSource_To_v1alpha1_ExternalClaimsSource(in *authentication.ExternalClaimsSource, out *ExternalClaimsSource, s conversion.Scope) error { + return autoConvert_authentication_ExternalClaimsSource_To_v1alpha1_ExternalClaimsSource(in, out, s) +} + +func autoConvert_v1alpha1_ExternalSourceCondition_To_authentication_ExternalSourceCondition(in *ExternalSourceCondition, out *authentication.ExternalSourceCondition, s conversion.Scope) error { + out.Expression = (*string)(unsafe.Pointer(in.Expression)) + return nil +} + +// Convert_v1alpha1_ExternalSourceCondition_To_authentication_ExternalSourceCondition is an autogenerated conversion function. +func Convert_v1alpha1_ExternalSourceCondition_To_authentication_ExternalSourceCondition(in *ExternalSourceCondition, out *authentication.ExternalSourceCondition, s conversion.Scope) error { + return autoConvert_v1alpha1_ExternalSourceCondition_To_authentication_ExternalSourceCondition(in, out, s) +} + +func autoConvert_authentication_ExternalSourceCondition_To_v1alpha1_ExternalSourceCondition(in *authentication.ExternalSourceCondition, out *ExternalSourceCondition, s conversion.Scope) error { + out.Expression = (*string)(unsafe.Pointer(in.Expression)) + return nil +} + +// Convert_authentication_ExternalSourceCondition_To_v1alpha1_ExternalSourceCondition is an autogenerated conversion function. +func Convert_authentication_ExternalSourceCondition_To_v1alpha1_ExternalSourceCondition(in *authentication.ExternalSourceCondition, out *ExternalSourceCondition, s conversion.Scope) error { + return autoConvert_authentication_ExternalSourceCondition_To_v1alpha1_ExternalSourceCondition(in, out, s) +} + +func autoConvert_v1alpha1_ExtraMapping_To_authentication_ExtraMapping(in *ExtraMapping, out *authentication.ExtraMapping, s conversion.Scope) error { + out.Key = in.Key + out.ValueExpression = in.ValueExpression + return nil +} + +// Convert_v1alpha1_ExtraMapping_To_authentication_ExtraMapping is an autogenerated conversion function. +func Convert_v1alpha1_ExtraMapping_To_authentication_ExtraMapping(in *ExtraMapping, out *authentication.ExtraMapping, s conversion.Scope) error { + return autoConvert_v1alpha1_ExtraMapping_To_authentication_ExtraMapping(in, out, s) +} + +func autoConvert_authentication_ExtraMapping_To_v1alpha1_ExtraMapping(in *authentication.ExtraMapping, out *ExtraMapping, s conversion.Scope) error { + out.Key = in.Key + out.ValueExpression = in.ValueExpression + return nil +} + +// Convert_authentication_ExtraMapping_To_v1alpha1_ExtraMapping is an autogenerated conversion function. +func Convert_authentication_ExtraMapping_To_v1alpha1_ExtraMapping(in *authentication.ExtraMapping, out *ExtraMapping, s conversion.Scope) error { + return autoConvert_authentication_ExtraMapping_To_v1alpha1_ExtraMapping(in, out, s) +} + +func autoConvert_v1alpha1_Issuer_To_authentication_Issuer(in *Issuer, out *authentication.Issuer, s conversion.Scope) error { + out.URL = in.URL + out.DiscoveryURL = in.DiscoveryURL + out.CertificateAuthority = in.CertificateAuthority + out.Audiences = *(*[]string)(unsafe.Pointer(&in.Audiences)) + out.AudienceMatchPolicy = authentication.AudienceMatchPolicyType(in.AudienceMatchPolicy) + return nil +} + +// Convert_v1alpha1_Issuer_To_authentication_Issuer is an autogenerated conversion function. +func Convert_v1alpha1_Issuer_To_authentication_Issuer(in *Issuer, out *authentication.Issuer, s conversion.Scope) error { + return autoConvert_v1alpha1_Issuer_To_authentication_Issuer(in, out, s) +} + +func autoConvert_authentication_Issuer_To_v1alpha1_Issuer(in *authentication.Issuer, out *Issuer, s conversion.Scope) error { + out.URL = in.URL + out.DiscoveryURL = in.DiscoveryURL + out.CertificateAuthority = in.CertificateAuthority + out.Audiences = *(*[]string)(unsafe.Pointer(&in.Audiences)) + out.AudienceMatchPolicy = AudienceMatchPolicyType(in.AudienceMatchPolicy) + return nil +} + +// Convert_authentication_Issuer_To_v1alpha1_Issuer is an autogenerated conversion function. +func Convert_authentication_Issuer_To_v1alpha1_Issuer(in *authentication.Issuer, out *Issuer, s conversion.Scope) error { + return autoConvert_authentication_Issuer_To_v1alpha1_Issuer(in, out, s) +} + +func autoConvert_v1alpha1_JWTAuthenticator_To_authentication_JWTAuthenticator(in *JWTAuthenticator, out *authentication.JWTAuthenticator, s conversion.Scope) error { + out.Issuer = (*authentication.Issuer)(unsafe.Pointer(in.Issuer)) + out.ClaimValidationRules = *(*[]authentication.ClaimValidationRule)(unsafe.Pointer(&in.ClaimValidationRules)) + out.ClaimMappings = (*authentication.ClaimMappings)(unsafe.Pointer(in.ClaimMappings)) + out.UserValidationRules = *(*[]authentication.UserValidationRule)(unsafe.Pointer(&in.UserValidationRules)) + out.ExternalClaimsSources = *(*[]authentication.ExternalClaimsSource)(unsafe.Pointer(&in.ExternalClaimsSources)) + return nil +} + +// Convert_v1alpha1_JWTAuthenticator_To_authentication_JWTAuthenticator is an autogenerated conversion function. +func Convert_v1alpha1_JWTAuthenticator_To_authentication_JWTAuthenticator(in *JWTAuthenticator, out *authentication.JWTAuthenticator, s conversion.Scope) error { + return autoConvert_v1alpha1_JWTAuthenticator_To_authentication_JWTAuthenticator(in, out, s) +} + +func autoConvert_authentication_JWTAuthenticator_To_v1alpha1_JWTAuthenticator(in *authentication.JWTAuthenticator, out *JWTAuthenticator, s conversion.Scope) error { + out.Issuer = (*Issuer)(unsafe.Pointer(in.Issuer)) + out.ClaimValidationRules = *(*[]ClaimValidationRule)(unsafe.Pointer(&in.ClaimValidationRules)) + out.ClaimMappings = (*ClaimMappings)(unsafe.Pointer(in.ClaimMappings)) + out.UserValidationRules = *(*[]UserValidationRule)(unsafe.Pointer(&in.UserValidationRules)) + out.ExternalClaimsSources = *(*[]ExternalClaimsSource)(unsafe.Pointer(&in.ExternalClaimsSources)) + return nil +} + +// Convert_authentication_JWTAuthenticator_To_v1alpha1_JWTAuthenticator is an autogenerated conversion function. +func Convert_authentication_JWTAuthenticator_To_v1alpha1_JWTAuthenticator(in *authentication.JWTAuthenticator, out *JWTAuthenticator, s conversion.Scope) error { + return autoConvert_authentication_JWTAuthenticator_To_v1alpha1_JWTAuthenticator(in, out, s) +} + +func autoConvert_v1alpha1_PrefixedClaimOrExpression_To_authentication_PrefixedClaimOrExpression(in *PrefixedClaimOrExpression, out *authentication.PrefixedClaimOrExpression, s conversion.Scope) error { + out.Claim = in.Claim + out.Prefix = (*string)(unsafe.Pointer(in.Prefix)) + out.Expression = in.Expression + return nil +} + +// Convert_v1alpha1_PrefixedClaimOrExpression_To_authentication_PrefixedClaimOrExpression is an autogenerated conversion function. +func Convert_v1alpha1_PrefixedClaimOrExpression_To_authentication_PrefixedClaimOrExpression(in *PrefixedClaimOrExpression, out *authentication.PrefixedClaimOrExpression, s conversion.Scope) error { + return autoConvert_v1alpha1_PrefixedClaimOrExpression_To_authentication_PrefixedClaimOrExpression(in, out, s) +} + +func autoConvert_authentication_PrefixedClaimOrExpression_To_v1alpha1_PrefixedClaimOrExpression(in *authentication.PrefixedClaimOrExpression, out *PrefixedClaimOrExpression, s conversion.Scope) error { + out.Claim = in.Claim + out.Prefix = (*string)(unsafe.Pointer(in.Prefix)) + out.Expression = in.Expression + return nil +} + +// Convert_authentication_PrefixedClaimOrExpression_To_v1alpha1_PrefixedClaimOrExpression is an autogenerated conversion function. +func Convert_authentication_PrefixedClaimOrExpression_To_v1alpha1_PrefixedClaimOrExpression(in *authentication.PrefixedClaimOrExpression, out *PrefixedClaimOrExpression, s conversion.Scope) error { + return autoConvert_authentication_PrefixedClaimOrExpression_To_v1alpha1_PrefixedClaimOrExpression(in, out, s) +} + +func autoConvert_v1alpha1_SourceURL_To_authentication_SourceURL(in *SourceURL, out *authentication.SourceURL, s conversion.Scope) error { + out.Hostname = (*string)(unsafe.Pointer(in.Hostname)) + out.PathExpression = (*string)(unsafe.Pointer(in.PathExpression)) + return nil +} + +// Convert_v1alpha1_SourceURL_To_authentication_SourceURL is an autogenerated conversion function. +func Convert_v1alpha1_SourceURL_To_authentication_SourceURL(in *SourceURL, out *authentication.SourceURL, s conversion.Scope) error { + return autoConvert_v1alpha1_SourceURL_To_authentication_SourceURL(in, out, s) +} + +func autoConvert_authentication_SourceURL_To_v1alpha1_SourceURL(in *authentication.SourceURL, out *SourceURL, s conversion.Scope) error { + out.Hostname = (*string)(unsafe.Pointer(in.Hostname)) + out.PathExpression = (*string)(unsafe.Pointer(in.PathExpression)) + return nil +} + +// Convert_authentication_SourceURL_To_v1alpha1_SourceURL is an autogenerated conversion function. +func Convert_authentication_SourceURL_To_v1alpha1_SourceURL(in *authentication.SourceURL, out *SourceURL, s conversion.Scope) error { + return autoConvert_authentication_SourceURL_To_v1alpha1_SourceURL(in, out, s) +} + +func autoConvert_v1alpha1_SourcedClaimMapping_To_authentication_SourcedClaimMapping(in *SourcedClaimMapping, out *authentication.SourcedClaimMapping, s conversion.Scope) error { + out.Name = (*string)(unsafe.Pointer(in.Name)) + out.Expression = (*string)(unsafe.Pointer(in.Expression)) + return nil +} + +// Convert_v1alpha1_SourcedClaimMapping_To_authentication_SourcedClaimMapping is an autogenerated conversion function. +func Convert_v1alpha1_SourcedClaimMapping_To_authentication_SourcedClaimMapping(in *SourcedClaimMapping, out *authentication.SourcedClaimMapping, s conversion.Scope) error { + return autoConvert_v1alpha1_SourcedClaimMapping_To_authentication_SourcedClaimMapping(in, out, s) +} + +func autoConvert_authentication_SourcedClaimMapping_To_v1alpha1_SourcedClaimMapping(in *authentication.SourcedClaimMapping, out *SourcedClaimMapping, s conversion.Scope) error { + out.Name = (*string)(unsafe.Pointer(in.Name)) + out.Expression = (*string)(unsafe.Pointer(in.Expression)) + return nil +} + +// Convert_authentication_SourcedClaimMapping_To_v1alpha1_SourcedClaimMapping is an autogenerated conversion function. +func Convert_authentication_SourcedClaimMapping_To_v1alpha1_SourcedClaimMapping(in *authentication.SourcedClaimMapping, out *SourcedClaimMapping, s conversion.Scope) error { + return autoConvert_authentication_SourcedClaimMapping_To_v1alpha1_SourcedClaimMapping(in, out, s) +} + +func autoConvert_v1alpha1_TLS_To_authentication_TLS(in *TLS, out *authentication.TLS, s conversion.Scope) error { + out.CertificateAuthority = (*string)(unsafe.Pointer(in.CertificateAuthority)) + return nil +} + +// Convert_v1alpha1_TLS_To_authentication_TLS is an autogenerated conversion function. +func Convert_v1alpha1_TLS_To_authentication_TLS(in *TLS, out *authentication.TLS, s conversion.Scope) error { + return autoConvert_v1alpha1_TLS_To_authentication_TLS(in, out, s) +} + +func autoConvert_authentication_TLS_To_v1alpha1_TLS(in *authentication.TLS, out *TLS, s conversion.Scope) error { + out.CertificateAuthority = (*string)(unsafe.Pointer(in.CertificateAuthority)) + return nil +} + +// Convert_authentication_TLS_To_v1alpha1_TLS is an autogenerated conversion function. +func Convert_authentication_TLS_To_v1alpha1_TLS(in *authentication.TLS, out *TLS, s conversion.Scope) error { + return autoConvert_authentication_TLS_To_v1alpha1_TLS(in, out, s) +} + +func autoConvert_v1alpha1_UserValidationRule_To_authentication_UserValidationRule(in *UserValidationRule, out *authentication.UserValidationRule, s conversion.Scope) error { + out.Expression = in.Expression + out.Message = in.Message + return nil +} + +// Convert_v1alpha1_UserValidationRule_To_authentication_UserValidationRule is an autogenerated conversion function. +func Convert_v1alpha1_UserValidationRule_To_authentication_UserValidationRule(in *UserValidationRule, out *authentication.UserValidationRule, s conversion.Scope) error { + return autoConvert_v1alpha1_UserValidationRule_To_authentication_UserValidationRule(in, out, s) +} + +func autoConvert_authentication_UserValidationRule_To_v1alpha1_UserValidationRule(in *authentication.UserValidationRule, out *UserValidationRule, s conversion.Scope) error { + out.Expression = in.Expression + out.Message = in.Message + return nil +} + +// Convert_authentication_UserValidationRule_To_v1alpha1_UserValidationRule is an autogenerated conversion function. +func Convert_authentication_UserValidationRule_To_v1alpha1_UserValidationRule(in *authentication.UserValidationRule, out *UserValidationRule, s conversion.Scope) error { + return autoConvert_authentication_UserValidationRule_To_v1alpha1_UserValidationRule(in, out, s) +} diff --git a/vendor/github.com/openshift/oauth-apiserver/pkg/externaloidc/apis/authentication/v1alpha1/zz_generated.deepcopy.go b/vendor/github.com/openshift/oauth-apiserver/pkg/externaloidc/apis/authentication/v1alpha1/zz_generated.deepcopy.go new file mode 100644 index 0000000000..d62e0272ee --- /dev/null +++ b/vendor/github.com/openshift/oauth-apiserver/pkg/externaloidc/apis/authentication/v1alpha1/zz_generated.deepcopy.go @@ -0,0 +1,406 @@ +//go:build !ignore_autogenerated +// +build !ignore_autogenerated + +// Code generated by deepcopy-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + runtime "k8s.io/apimachinery/pkg/runtime" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Authentication) DeepCopyInto(out *Authentication) { + *out = *in + if in.Type != nil { + in, out := &in.Type, &out.Type + *out = new(AuthenticationType) + **out = **in + } + if in.ClientCredential != nil { + in, out := &in.ClientCredential, &out.ClientCredential + *out = new(ClientCredentialConfig) + (*in).DeepCopyInto(*out) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Authentication. +func (in *Authentication) DeepCopy() *Authentication { + if in == nil { + return nil + } + out := new(Authentication) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AuthenticationConfiguration) DeepCopyInto(out *AuthenticationConfiguration) { + *out = *in + out.TypeMeta = in.TypeMeta + if in.JWT != nil { + in, out := &in.JWT, &out.JWT + *out = make([]JWTAuthenticator, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AuthenticationConfiguration. +func (in *AuthenticationConfiguration) DeepCopy() *AuthenticationConfiguration { + if in == nil { + return nil + } + out := new(AuthenticationConfiguration) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *AuthenticationConfiguration) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClaimMappings) DeepCopyInto(out *ClaimMappings) { + *out = *in + in.Username.DeepCopyInto(&out.Username) + in.Groups.DeepCopyInto(&out.Groups) + out.UID = in.UID + if in.Extra != nil { + in, out := &in.Extra, &out.Extra + *out = make([]ExtraMapping, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClaimMappings. +func (in *ClaimMappings) DeepCopy() *ClaimMappings { + if in == nil { + return nil + } + out := new(ClaimMappings) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClaimOrExpression) DeepCopyInto(out *ClaimOrExpression) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClaimOrExpression. +func (in *ClaimOrExpression) DeepCopy() *ClaimOrExpression { + if in == nil { + return nil + } + out := new(ClaimOrExpression) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClaimValidationRule) DeepCopyInto(out *ClaimValidationRule) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClaimValidationRule. +func (in *ClaimValidationRule) DeepCopy() *ClaimValidationRule { + if in == nil { + return nil + } + out := new(ClaimValidationRule) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClientCredentialConfig) DeepCopyInto(out *ClientCredentialConfig) { + *out = *in + if in.Scopes != nil { + in, out := &in.Scopes, &out.Scopes + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.TLS != nil { + in, out := &in.TLS, &out.TLS + *out = new(TLS) + (*in).DeepCopyInto(*out) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClientCredentialConfig. +func (in *ClientCredentialConfig) DeepCopy() *ClientCredentialConfig { + if in == nil { + return nil + } + out := new(ClientCredentialConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ExternalClaimsSource) DeepCopyInto(out *ExternalClaimsSource) { + *out = *in + if in.Authentication != nil { + in, out := &in.Authentication, &out.Authentication + *out = new(Authentication) + (*in).DeepCopyInto(*out) + } + if in.TLS != nil { + in, out := &in.TLS, &out.TLS + *out = new(TLS) + (*in).DeepCopyInto(*out) + } + if in.URL != nil { + in, out := &in.URL, &out.URL + *out = new(SourceURL) + (*in).DeepCopyInto(*out) + } + if in.Mappings != nil { + in, out := &in.Mappings, &out.Mappings + *out = make([]SourcedClaimMapping, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]ExternalSourceCondition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExternalClaimsSource. +func (in *ExternalClaimsSource) DeepCopy() *ExternalClaimsSource { + if in == nil { + return nil + } + out := new(ExternalClaimsSource) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ExternalSourceCondition) DeepCopyInto(out *ExternalSourceCondition) { + *out = *in + if in.Expression != nil { + in, out := &in.Expression, &out.Expression + *out = new(string) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExternalSourceCondition. +func (in *ExternalSourceCondition) DeepCopy() *ExternalSourceCondition { + if in == nil { + return nil + } + out := new(ExternalSourceCondition) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ExtraMapping) DeepCopyInto(out *ExtraMapping) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExtraMapping. +func (in *ExtraMapping) DeepCopy() *ExtraMapping { + if in == nil { + return nil + } + out := new(ExtraMapping) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Issuer) DeepCopyInto(out *Issuer) { + *out = *in + if in.Audiences != nil { + in, out := &in.Audiences, &out.Audiences + *out = make([]string, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Issuer. +func (in *Issuer) DeepCopy() *Issuer { + if in == nil { + return nil + } + out := new(Issuer) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *JWTAuthenticator) DeepCopyInto(out *JWTAuthenticator) { + *out = *in + if in.Issuer != nil { + in, out := &in.Issuer, &out.Issuer + *out = new(Issuer) + (*in).DeepCopyInto(*out) + } + if in.ClaimValidationRules != nil { + in, out := &in.ClaimValidationRules, &out.ClaimValidationRules + *out = make([]ClaimValidationRule, len(*in)) + copy(*out, *in) + } + if in.ClaimMappings != nil { + in, out := &in.ClaimMappings, &out.ClaimMappings + *out = new(ClaimMappings) + (*in).DeepCopyInto(*out) + } + if in.UserValidationRules != nil { + in, out := &in.UserValidationRules, &out.UserValidationRules + *out = make([]UserValidationRule, len(*in)) + copy(*out, *in) + } + if in.ExternalClaimsSources != nil { + in, out := &in.ExternalClaimsSources, &out.ExternalClaimsSources + *out = make([]ExternalClaimsSource, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new JWTAuthenticator. +func (in *JWTAuthenticator) DeepCopy() *JWTAuthenticator { + if in == nil { + return nil + } + out := new(JWTAuthenticator) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PrefixedClaimOrExpression) DeepCopyInto(out *PrefixedClaimOrExpression) { + *out = *in + if in.Prefix != nil { + in, out := &in.Prefix, &out.Prefix + *out = new(string) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PrefixedClaimOrExpression. +func (in *PrefixedClaimOrExpression) DeepCopy() *PrefixedClaimOrExpression { + if in == nil { + return nil + } + out := new(PrefixedClaimOrExpression) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SourceURL) DeepCopyInto(out *SourceURL) { + *out = *in + if in.Hostname != nil { + in, out := &in.Hostname, &out.Hostname + *out = new(string) + **out = **in + } + if in.PathExpression != nil { + in, out := &in.PathExpression, &out.PathExpression + *out = new(string) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SourceURL. +func (in *SourceURL) DeepCopy() *SourceURL { + if in == nil { + return nil + } + out := new(SourceURL) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SourcedClaimMapping) DeepCopyInto(out *SourcedClaimMapping) { + *out = *in + if in.Name != nil { + in, out := &in.Name, &out.Name + *out = new(string) + **out = **in + } + if in.Expression != nil { + in, out := &in.Expression, &out.Expression + *out = new(string) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SourcedClaimMapping. +func (in *SourcedClaimMapping) DeepCopy() *SourcedClaimMapping { + if in == nil { + return nil + } + out := new(SourcedClaimMapping) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TLS) DeepCopyInto(out *TLS) { + *out = *in + if in.CertificateAuthority != nil { + in, out := &in.CertificateAuthority, &out.CertificateAuthority + *out = new(string) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TLS. +func (in *TLS) DeepCopy() *TLS { + if in == nil { + return nil + } + out := new(TLS) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *UserValidationRule) DeepCopyInto(out *UserValidationRule) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new UserValidationRule. +func (in *UserValidationRule) DeepCopy() *UserValidationRule { + if in == nil { + return nil + } + out := new(UserValidationRule) + in.DeepCopyInto(out) + return out +} diff --git a/vendor/github.com/openshift/oauth-apiserver/pkg/externaloidc/apis/authentication/zz_generated.deepcopy.go b/vendor/github.com/openshift/oauth-apiserver/pkg/externaloidc/apis/authentication/zz_generated.deepcopy.go new file mode 100644 index 0000000000..1332d28cb1 --- /dev/null +++ b/vendor/github.com/openshift/oauth-apiserver/pkg/externaloidc/apis/authentication/zz_generated.deepcopy.go @@ -0,0 +1,406 @@ +//go:build !ignore_autogenerated +// +build !ignore_autogenerated + +// Code generated by deepcopy-gen. DO NOT EDIT. + +package authentication + +import ( + runtime "k8s.io/apimachinery/pkg/runtime" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Authentication) DeepCopyInto(out *Authentication) { + *out = *in + if in.Type != nil { + in, out := &in.Type, &out.Type + *out = new(AuthenticationType) + **out = **in + } + if in.ClientCredential != nil { + in, out := &in.ClientCredential, &out.ClientCredential + *out = new(ClientCredentialConfig) + (*in).DeepCopyInto(*out) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Authentication. +func (in *Authentication) DeepCopy() *Authentication { + if in == nil { + return nil + } + out := new(Authentication) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AuthenticationConfiguration) DeepCopyInto(out *AuthenticationConfiguration) { + *out = *in + out.TypeMeta = in.TypeMeta + if in.JWT != nil { + in, out := &in.JWT, &out.JWT + *out = make([]JWTAuthenticator, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AuthenticationConfiguration. +func (in *AuthenticationConfiguration) DeepCopy() *AuthenticationConfiguration { + if in == nil { + return nil + } + out := new(AuthenticationConfiguration) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *AuthenticationConfiguration) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClaimMappings) DeepCopyInto(out *ClaimMappings) { + *out = *in + in.Username.DeepCopyInto(&out.Username) + in.Groups.DeepCopyInto(&out.Groups) + out.UID = in.UID + if in.Extra != nil { + in, out := &in.Extra, &out.Extra + *out = make([]ExtraMapping, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClaimMappings. +func (in *ClaimMappings) DeepCopy() *ClaimMappings { + if in == nil { + return nil + } + out := new(ClaimMappings) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClaimOrExpression) DeepCopyInto(out *ClaimOrExpression) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClaimOrExpression. +func (in *ClaimOrExpression) DeepCopy() *ClaimOrExpression { + if in == nil { + return nil + } + out := new(ClaimOrExpression) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClaimValidationRule) DeepCopyInto(out *ClaimValidationRule) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClaimValidationRule. +func (in *ClaimValidationRule) DeepCopy() *ClaimValidationRule { + if in == nil { + return nil + } + out := new(ClaimValidationRule) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClientCredentialConfig) DeepCopyInto(out *ClientCredentialConfig) { + *out = *in + if in.Scopes != nil { + in, out := &in.Scopes, &out.Scopes + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.TLS != nil { + in, out := &in.TLS, &out.TLS + *out = new(TLS) + (*in).DeepCopyInto(*out) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClientCredentialConfig. +func (in *ClientCredentialConfig) DeepCopy() *ClientCredentialConfig { + if in == nil { + return nil + } + out := new(ClientCredentialConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ExternalClaimsSource) DeepCopyInto(out *ExternalClaimsSource) { + *out = *in + if in.Authentication != nil { + in, out := &in.Authentication, &out.Authentication + *out = new(Authentication) + (*in).DeepCopyInto(*out) + } + if in.TLS != nil { + in, out := &in.TLS, &out.TLS + *out = new(TLS) + (*in).DeepCopyInto(*out) + } + if in.URL != nil { + in, out := &in.URL, &out.URL + *out = new(SourceURL) + (*in).DeepCopyInto(*out) + } + if in.Mappings != nil { + in, out := &in.Mappings, &out.Mappings + *out = make([]SourcedClaimMapping, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]ExternalSourceCondition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExternalClaimsSource. +func (in *ExternalClaimsSource) DeepCopy() *ExternalClaimsSource { + if in == nil { + return nil + } + out := new(ExternalClaimsSource) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ExternalSourceCondition) DeepCopyInto(out *ExternalSourceCondition) { + *out = *in + if in.Expression != nil { + in, out := &in.Expression, &out.Expression + *out = new(string) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExternalSourceCondition. +func (in *ExternalSourceCondition) DeepCopy() *ExternalSourceCondition { + if in == nil { + return nil + } + out := new(ExternalSourceCondition) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ExtraMapping) DeepCopyInto(out *ExtraMapping) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExtraMapping. +func (in *ExtraMapping) DeepCopy() *ExtraMapping { + if in == nil { + return nil + } + out := new(ExtraMapping) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Issuer) DeepCopyInto(out *Issuer) { + *out = *in + if in.Audiences != nil { + in, out := &in.Audiences, &out.Audiences + *out = make([]string, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Issuer. +func (in *Issuer) DeepCopy() *Issuer { + if in == nil { + return nil + } + out := new(Issuer) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *JWTAuthenticator) DeepCopyInto(out *JWTAuthenticator) { + *out = *in + if in.Issuer != nil { + in, out := &in.Issuer, &out.Issuer + *out = new(Issuer) + (*in).DeepCopyInto(*out) + } + if in.ClaimValidationRules != nil { + in, out := &in.ClaimValidationRules, &out.ClaimValidationRules + *out = make([]ClaimValidationRule, len(*in)) + copy(*out, *in) + } + if in.ClaimMappings != nil { + in, out := &in.ClaimMappings, &out.ClaimMappings + *out = new(ClaimMappings) + (*in).DeepCopyInto(*out) + } + if in.UserValidationRules != nil { + in, out := &in.UserValidationRules, &out.UserValidationRules + *out = make([]UserValidationRule, len(*in)) + copy(*out, *in) + } + if in.ExternalClaimsSources != nil { + in, out := &in.ExternalClaimsSources, &out.ExternalClaimsSources + *out = make([]ExternalClaimsSource, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new JWTAuthenticator. +func (in *JWTAuthenticator) DeepCopy() *JWTAuthenticator { + if in == nil { + return nil + } + out := new(JWTAuthenticator) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PrefixedClaimOrExpression) DeepCopyInto(out *PrefixedClaimOrExpression) { + *out = *in + if in.Prefix != nil { + in, out := &in.Prefix, &out.Prefix + *out = new(string) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PrefixedClaimOrExpression. +func (in *PrefixedClaimOrExpression) DeepCopy() *PrefixedClaimOrExpression { + if in == nil { + return nil + } + out := new(PrefixedClaimOrExpression) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SourceURL) DeepCopyInto(out *SourceURL) { + *out = *in + if in.Hostname != nil { + in, out := &in.Hostname, &out.Hostname + *out = new(string) + **out = **in + } + if in.PathExpression != nil { + in, out := &in.PathExpression, &out.PathExpression + *out = new(string) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SourceURL. +func (in *SourceURL) DeepCopy() *SourceURL { + if in == nil { + return nil + } + out := new(SourceURL) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SourcedClaimMapping) DeepCopyInto(out *SourcedClaimMapping) { + *out = *in + if in.Name != nil { + in, out := &in.Name, &out.Name + *out = new(string) + **out = **in + } + if in.Expression != nil { + in, out := &in.Expression, &out.Expression + *out = new(string) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SourcedClaimMapping. +func (in *SourcedClaimMapping) DeepCopy() *SourcedClaimMapping { + if in == nil { + return nil + } + out := new(SourcedClaimMapping) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TLS) DeepCopyInto(out *TLS) { + *out = *in + if in.CertificateAuthority != nil { + in, out := &in.CertificateAuthority, &out.CertificateAuthority + *out = new(string) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TLS. +func (in *TLS) DeepCopy() *TLS { + if in == nil { + return nil + } + out := new(TLS) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *UserValidationRule) DeepCopyInto(out *UserValidationRule) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new UserValidationRule. +func (in *UserValidationRule) DeepCopy() *UserValidationRule { + if in == nil { + return nil + } + out := new(UserValidationRule) + in.DeepCopyInto(out) + return out +} diff --git a/vendor/modules.txt b/vendor/modules.txt index d313aa8288..49e94393ed 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -127,6 +127,7 @@ github.com/google/gnostic-models/openapiv3 # github.com/google/go-cmp v0.7.0 ## explicit; go 1.21 github.com/google/go-cmp/cmp +github.com/google/go-cmp/cmp/cmpopts github.com/google/go-cmp/cmp/internal/diff github.com/google/go-cmp/cmp/internal/flags github.com/google/go-cmp/cmp/internal/function @@ -210,7 +211,7 @@ github.com/onsi/gomega/matchers/support/goraph/edge github.com/onsi/gomega/matchers/support/goraph/node github.com/onsi/gomega/matchers/support/goraph/util github.com/onsi/gomega/types -# github.com/openshift-eng/openshift-tests-extension v0.0.0-20251205182537-ff5553e56f33 +# github.com/openshift-eng/openshift-tests-extension v0.0.0-20260408205138-ec501c2bf4a5 ## explicit; go 1.23.0 github.com/openshift-eng/openshift-tests-extension/pkg/cmd github.com/openshift-eng/openshift-tests-extension/pkg/cmd/cmdimages @@ -226,7 +227,7 @@ github.com/openshift-eng/openshift-tests-extension/pkg/ginkgo github.com/openshift-eng/openshift-tests-extension/pkg/junit github.com/openshift-eng/openshift-tests-extension/pkg/util/sets github.com/openshift-eng/openshift-tests-extension/pkg/version -# github.com/openshift/api v0.0.0-20260429122012-1180c0f5c3e9 +# github.com/openshift/api v0.0.0-20260429122012-1180c0f5c3e9 => github.com/everettraven/openshift-api v0.0.0-20260507192020-4affa2ac4dea ## explicit; go 1.25.0 github.com/openshift/api github.com/openshift/api/annotations @@ -306,7 +307,7 @@ github.com/openshift/api/template github.com/openshift/api/template/v1 github.com/openshift/api/user github.com/openshift/api/user/v1 -# github.com/openshift/build-machinery-go v0.0.0-20250530140348-dc5b2804eeee +# github.com/openshift/build-machinery-go v0.0.0-20251023084048-5d77c1a5e5af ## explicit; go 1.22.0 github.com/openshift/build-machinery-go github.com/openshift/build-machinery-go/make @@ -465,6 +466,10 @@ github.com/openshift/multi-operator-manager/pkg/flagtypes github.com/openshift/multi-operator-manager/pkg/library/libraryapplyconfiguration github.com/openshift/multi-operator-manager/pkg/library/libraryinputresources github.com/openshift/multi-operator-manager/pkg/library/libraryoutputresources +# github.com/openshift/oauth-apiserver v0.0.0-20260430140618-160ac7fb4ea6 => github.com/everettraven/openshift-oauth-apiserver v0.0.0-20260512184245-97bf3a3f7bc8 +## explicit; go 1.24.0 +github.com/openshift/oauth-apiserver/pkg/externaloidc/apis/authentication +github.com/openshift/oauth-apiserver/pkg/externaloidc/apis/authentication/v1alpha1 # github.com/pkg/errors v0.9.1 ## explicit github.com/pkg/errors @@ -1646,3 +1651,5 @@ sigs.k8s.io/structured-merge-diff/v6/value ## explicit; go 1.22 sigs.k8s.io/yaml # github.com/onsi/ginkgo/v2 => github.com/openshift/onsi-ginkgo/v2 v2.6.1-0.20251001123353-fd5b1fb35db1 +# github.com/openshift/api => github.com/everettraven/openshift-api v0.0.0-20260507192020-4affa2ac4dea +# github.com/openshift/oauth-apiserver => github.com/everettraven/openshift-oauth-apiserver v0.0.0-20260512184245-97bf3a3f7bc8