diff --git a/go.mod b/go.mod index a403741fc0..a599bfbdc5 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 diff --git a/go.sum b/go.sum index 672200d91a..c4be05e98c 100644 --- a/go.sum +++ b/go.sum @@ -144,18 +144,20 @@ 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-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/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/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= github.com/openshift/library-go v0.0.0-20260506113849-32460ef09730/go.mod h1:k1tefCr+PAZ7kY8TJjpE6rW6t6Yu4iOmBwO+1+3qD2s= github.com/openshift/multi-operator-manager v0.0.0-20241205181422-20aa3906b99d h1:Rzx23P63JFNNz5D23ubhC0FCN5rK8CeJhKcq5QKcdyU= github.com/openshift/multi-operator-manager v0.0.0-20241205181422-20aa3906b99d/go.mod h1:iVi9Bopa5cLhjG5ie9DoZVVqkH8BGb1FQVTtecOLn4I= +github.com/openshift/oauth-apiserver v0.0.0-20260430140618-160ac7fb4ea6 h1:WvXToDt/IVTXb4NxbqEjY0cuPpVadTK6ATu75mlVM/s= +github.com/openshift/oauth-apiserver v0.0.0-20260430140618-160ac7fb4ea6/go.mod h1:VsfvQ75bRfxT1dBSh1zROlnpDHNUYuSxgUV6vTXtOqs= github.com/openshift/onsi-ginkgo/v2 v2.6.1-0.20251001123353-fd5b1fb35db1 h1:PMTgifBcBRLJJiM+LgSzPDTk9/Rx4qS09OUrfpY6GBQ= github.com/openshift/onsi-ginkgo/v2 v2.6.1-0.20251001123353-fd5b1fb35db1/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0= 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..83d9e10eb2 --- /dev/null +++ b/pkg/controllers/externaloidc/generation/oauthapiserver/generate.go @@ -0,0 +1,754 @@ +package oauthapiserver + +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" + authenticationv1alpha1 "github.com/openshift/oauth-apiserver/pkg/externaloidc/apis/authentication/v1alpha1" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + 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 + } + + 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 +} 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..9ddc7dc797 --- /dev/null +++ b/pkg/controllers/externaloidc/generation/oauthapiserver/generate_test.go @@ -0,0 +1,1586 @@ +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, + }, + ), + }, + } { + 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/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..34dcc153d7 --- /dev/null +++ b/vendor/github.com/openshift/oauth-apiserver/pkg/externaloidc/apis/authentication/types.go @@ -0,0 +1,339 @@ +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 +} + +// 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 +} 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..4493db0c19 --- /dev/null +++ b/vendor/github.com/openshift/oauth-apiserver/pkg/externaloidc/apis/authentication/v1alpha1/types.go @@ -0,0 +1,339 @@ +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"` +} + +// 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"` +} 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..8e0c14e7a6 --- /dev/null +++ b/vendor/github.com/openshift/oauth-apiserver/pkg/externaloidc/apis/authentication/v1alpha1/zz_generated.conversion.go @@ -0,0 +1,342 @@ +//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((*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((*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((*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_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_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)) + 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)) + 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_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..1212676a04 --- /dev/null +++ b/vendor/github.com/openshift/oauth-apiserver/pkg/externaloidc/apis/authentication/v1alpha1/zz_generated.deepcopy.go @@ -0,0 +1,208 @@ +//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 *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 *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) + } + 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 *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..51841b827d --- /dev/null +++ b/vendor/github.com/openshift/oauth-apiserver/pkg/externaloidc/apis/authentication/zz_generated.deepcopy.go @@ -0,0 +1,208 @@ +//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 *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 *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) + } + 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 *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..692474807c 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 @@ -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 +## 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