Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ require (
github.com/openshift/client-go v0.0.0-20260512113608-deb4dc54551a
github.com/openshift/library-go v0.0.0-20260515094948-509d00758cf2
github.com/openshift/multi-operator-manager v0.0.0-20241205181422-20aa3906b99d
github.com/openshift/oauth-apiserver v0.0.0-20260430140618-160ac7fb4ea6
github.com/openshift/oauth-apiserver v0.0.0-20260520145010-97a820bd5412
github.com/spf13/cobra v1.10.0
github.com/spf13/pflag v1.0.9
github.com/stretchr/testify v1.11.1
Expand Down Expand Up @@ -133,3 +133,5 @@ require (
)

replace github.com/onsi/ginkgo/v2 => github.com/openshift/onsi-ginkgo/v2 v2.6.1-0.20251001123353-fd5b1fb35db1

replace github.com/openshift/api => github.com/everettraven/openshift-api v0.0.0-20260507192020-4affa2ac4dea
8 changes: 4 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU=
github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
github.com/everettraven/openshift-api v0.0.0-20260507192020-4affa2ac4dea h1:2Bw06Lh1m4KaiQIvjhz5Q06cZ5OBsT1lEwT0CVrY4EE=
github.com/everettraven/openshift-api v0.0.0-20260507192020-4affa2ac4dea/go.mod h1:pyVjK0nZ4sRs4fuQVQ4rubsJdahI1PB94LnQ8sGdvxo=
github.com/felixge/fgprof v0.9.3/go.mod h1:RdbpDgzqYVh/T9fPELJyV7EYJuHB55UTEULNun8eiPw=
github.com/felixge/fgprof v0.9.4 h1:ocDNwMFlnA0NU0zSB3I52xkO4sFXk80VK9lXjLClu88=
github.com/felixge/fgprof v0.9.4/go.mod h1:yKl+ERSa++RYOs32d8K6WEXCB4uXdLls4ZaZPpayhMM=
Expand Down Expand Up @@ -146,8 +148,6 @@ 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-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-20260511191110-9b69e5fa27e9 h1:yb8ul1HPFYhO04yp0D8T/qSySZnKv210f4nE//i/Bdg=
github.com/openshift/api v0.0.0-20260511191110-9b69e5fa27e9/go.mod h1:pyVjK0nZ4sRs4fuQVQ4rubsJdahI1PB94LnQ8sGdvxo=
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-20260512113608-deb4dc54551a h1:EKx2XhOKehd1C5ptY7IrLl4WV35E8kP0pRPnG5BUZXk=
Expand All @@ -156,8 +156,8 @@ github.com/openshift/library-go v0.0.0-20260515094948-509d00758cf2 h1:JoiqT7XZ4W
github.com/openshift/library-go v0.0.0-20260515094948-509d00758cf2/go.mod h1:gKG9lctU0yEftSoT3DUyeIWz1oAgF0EHUpwI4pnCo4o=
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/oauth-apiserver v0.0.0-20260520145010-97a820bd5412 h1:oDB0GmUXLp8y85fWz+LGRE0hM5JqbXTfNPi5GjEqiX0=
github.com/openshift/oauth-apiserver v0.0.0-20260520145010-97a820bd5412/go.mod h1:qPt46oOj0jFGgpabBjMazsgQXwrJ7KYBDwAuaesJLdE=
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=
Expand Down
306 changes: 306 additions & 0 deletions pkg/controllers/externaloidc/generation/oauthapiserver/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"io"
"net/http"
"net/url"
"regexp"
"strings"
"time"

Expand All @@ -20,6 +21,7 @@ import (

metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/sets"
authenticationcel "k8s.io/apiserver/pkg/authentication/cel"
corev1listers "k8s.io/client-go/listers/core/v1"
"k8s.io/client-go/util/cert"
Expand Down Expand Up @@ -128,6 +130,15 @@ func generateJWTForProvider(provider configv1.OIDCProvider, configMapLister core
out.UserValidationRules = userValidationRules
}

if featureGates.Enabled(features.FeatureGateExternalOIDCExternalClaimsSourcing) {
externalClaimsSources, err := generateExternalClaimsSources(configMapLister, secretLister, provider.ExternalClaimsSources...)
if err != nil {
return authenticationv1alpha1.JWTAuthenticator{}, fmt.Errorf("generating externalClaimsSources for provider %q: %v", provider.Name, err)
}

out.ExternalClaimsSources = externalClaimsSources
}

out.Issuer = &issuer
out.ClaimMappings = &claimMappings
out.ClaimValidationRules = claimValidationRules
Expand Down Expand Up @@ -752,3 +763,298 @@ func isConstField(exp *exprpb.Expr, field string) bool {
c := exp.GetConstExpr()
return c != nil && c.GetStringValue() == field
}

func generateExternalClaimsSources(cmLister corev1listers.ConfigMapLister, secretLister corev1listers.SecretLister, sources ...configv1.ExternalClaimsSource) ([]authenticationv1alpha1.ExternalClaimsSource, error) {
out := []authenticationv1alpha1.ExternalClaimsSource{}
seenClaimNames := sets.New[string]()
for _, source := range sources {
source, err := generateExternalClaimsSource(source, cmLister, secretLister, seenClaimNames)
if err != nil {
return nil, err
}

if source != nil {
out = append(out, *source)
}
}

return out, nil
}

func generateExternalClaimsSource(source configv1.ExternalClaimsSource, cmLister corev1listers.ConfigMapLister, secretLister corev1listers.SecretLister, seenClaimNames sets.Set[string]) (*authenticationv1alpha1.ExternalClaimsSource, error) {
authentication, err := generateExternalClaimsSourceAuthentication(source.Authentication, secretLister, cmLister)
if err != nil {
return nil, err
}

tls, err := generateExternalClaimsSourceTLS(source.TLS, cmLister)
if err != nil {
return nil, err
}

url, err := generateExternalClaimsSourceURL(source.URL)
if err != nil {
return nil, err
}

mappings, err := generateExternalClaimsSourceMappings(seenClaimNames, source.Mappings...)
if err != nil {
return nil, err
}

conditions, err := generateExternalClaimsSourceConditions(source.Predicates...)
if err != nil {
return nil, err
}

return &authenticationv1alpha1.ExternalClaimsSource{
Authentication: authentication,
TLS: tls,
URL: url,
Mappings: mappings,
Conditions: conditions,
}, nil
}

func generateExternalClaimsSourceAuthentication(externalSourceAuthentication configv1.ExternalSourceAuthentication, secretLister corev1listers.SecretLister, cmLister corev1listers.ConfigMapLister) (*authenticationv1alpha1.Authentication, error) {
switch externalSourceAuthentication.Type {
case "": // signals the omitted case which is valid and means to use anonymous auth. This means we should omit it as well so anonymous auth takes place.
return nil, nil
case configv1.ExternalSourceAuthenticationTypeRequestProvidedToken:
return &authenticationv1alpha1.Authentication{
Type: ptr.To(authenticationv1alpha1.AuthenticationTypeRequestProvidedToken),
}, nil
case configv1.ExternalSourceAuthenticationTypeClientCredential:
cc, err := generateExternalClaimsSourceAuthenticationClientCredential(externalSourceAuthentication.ClientCredential, secretLister, cmLister)
if err != nil {
return nil, fmt.Errorf("generating client credentials configuration: %w", err)
}

return &authenticationv1alpha1.Authentication{
Type: ptr.To(authenticationv1alpha1.AuthenticationTypeClientCredential),
ClientCredential: cc,
}, nil
default:
return nil, fmt.Errorf("unknown external source authentication type %q", externalSourceAuthentication.Type)
}
}

var printableASCIIRegexp = regexp.MustCompile(`^[[:print:]]+$`)

func generateExternalClaimsSourceAuthenticationClientCredential(clientCredentialConfig configv1.ClientCredentialConfig, secretLister corev1listers.SecretLister, cmLister corev1listers.ConfigMapLister) (*authenticationv1alpha1.ClientCredentialConfig, error) {
// TODO: enable validation when it is possible to do so. Currently blocked
// due to oauth-apiserver not being rebased on 1.35 and the KAS library changes
// not existing in the 1.35 branch.
// The following jira tickets track the work necessary to eventually enable this validation:
// 1. https://redhat.atlassian.net/browse/CNTRLPLANE-3491
// 2. https://redhat.atlassian.net/browse/CNTRLPLANE-3492
// 3. https://redhat.atlassian.net/browse/CNTRLPLANE-3493
/*
if err := validation.ValidateClientCredentialConfigClientID(clientCredentialConfig.ClientID, field.NewPath("")); err != nil {
return nil, fmt.Errorf("validating client id: %w", kubeErrorListToGoError(err))
}

if err := validation.ValidateTokenEndpoint(clientCredentialConfig.TokenEndpoint, field.NewPath("")); err != nil {
return nil, fmt.Errorf("validating token endpoint: %w", kubeErrorListToGoError(err))
}
*/

clientSecret, err := getClientSecretFromSecret(clientCredentialConfig.ClientSecret.Name, secretLister)
if err != nil {
return nil, fmt.Errorf("getting client secret: %w", err)
}

// TODO: enable validation when it is possible to do so. Currently blocked
// due to oauth-apiserver not being rebased on 1.35 and the KAS library changes
// not existing in the 1.35 branch.
// The following jira tickets track the work necessary to eventually enable this validation:
// 1. https://redhat.atlassian.net/browse/CNTRLPLANE-3491
// 2. https://redhat.atlassian.net/browse/CNTRLPLANE-3492
// 3. https://redhat.atlassian.net/browse/CNTRLPLANE-3493
/*
if err := validation.ValidateClientCredentialConfigClientSecret(clientSecret, field.NewPath("")); err != nil {
return nil, fmt.Errorf("validating client secret: %w", kubeErrorListToGoError(err))
}
*/

scopes, err := generateClientCredentialScopes(clientCredentialConfig.Scopes...)
if err != nil {
return nil, fmt.Errorf("generating scopes: %w", err)
}

var certificateAuthority *string = nil
if len(clientCredentialConfig.TLS.CertificateAuthority.Name) > 0 {
ca, err := getCertificateAuthorityFromConfigMap(clientCredentialConfig.TLS.CertificateAuthority.Name, cmLister)
if err != nil {
return nil, fmt.Errorf("getting certificate authority: %w", err)
}

certificateAuthority = &ca
}

return &authenticationv1alpha1.ClientCredentialConfig{
ClientID: clientCredentialConfig.ClientID,
ClientSecret: clientSecret,
TokenEndpoint: clientCredentialConfig.TokenEndpoint,
Scopes: scopes,
TLS: &authenticationv1alpha1.TLS{
CertificateAuthority: certificateAuthority,
},
}, nil
}

func generateClientCredentialScopes(scopes ...configv1.OAuth2Scope) ([]string, error) {
out := make([]string, 0, len(scopes))
errs := []error{}
for _, scope := range scopes {
// TODO: enable validation when it is possible to do so. Currently blocked
// due to oauth-apiserver not being rebased on 1.35 and the KAS library changes
// not existing in the 1.35 branch.
// The following jira tickets track the work necessary to eventually enable this validation:
// 1. https://redhat.atlassian.net/browse/CNTRLPLANE-3491
// 2. https://redhat.atlassian.net/browse/CNTRLPLANE-3492
// 3. https://redhat.atlassian.net/browse/CNTRLPLANE-3493
/*
err := validation.ValidateClientCredentialConfigScope(string(scope), field.NewPath(""))
if err != nil {
errs = append(errs, fmt.Errorf("validating scopes[%s]: %w", i, kubeErrorListToGoError(err)))
continue
}
*/

out = append(out, string(scope))
}

return out, errors.Join(errs...)
}

func getClientSecretFromSecret(name string, secretLister corev1listers.SecretLister) (string, error) {
secret, err := secretLister.Secrets(configNamespace).Get(name)
if err != nil {
return "", fmt.Errorf("could not retrieve auth secret %s/%s to get client secret: %v", configNamespace, name, err)
}

clientSecret, ok := secret.Data["client-secret"]
if !ok || len(clientSecret) == 0 {
return "", fmt.Errorf("secret %s/%s key \"client-secret\" missing or empty", configNamespace, name)
}

return string(clientSecret), nil
}

func generateExternalClaimsSourceTLS(externalSourceTLS configv1.ExternalSourceTLS, cmLister corev1listers.ConfigMapLister) (*authenticationv1alpha1.TLS, error) {
caData, err := getCertificateAuthorityFromConfigMap(externalSourceTLS.CertificateAuthority.Name, cmLister)
if err != nil {
return nil, fmt.Errorf("getting certificate authority for external source: %w", err)
}

return &authenticationv1alpha1.TLS{
CertificateAuthority: &caData,
}, nil
}
Comment on lines +945 to +954
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Missing nil check for externalSourceTLS.CertificateAuthority.Name.

If CertificateAuthority.Name is empty, this will attempt to look up a ConfigMap with an empty name, which will fail with a confusing error. Consider validating that the name is provided.

Proposed fix
 func generateExternalClaimsSourceTLS(externalSourceTLS configv1.ExternalSourceTLS, cmLister corev1listers.ConfigMapLister) (*authenticationv1alpha1.TLS, error) {
+	if len(externalSourceTLS.CertificateAuthority.Name) == 0 {
+		return nil, fmt.Errorf("external source TLS certificate authority name is required")
+	}
 	caData, err := getCertificateAuthorityFromConfigMap(externalSourceTLS.CertificateAuthority.Name, cmLister)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/controllers/externaloidc/generation/oauthapiserver/generate.go` around
lines 667 - 676, In generateExternalClaimsSourceTLS, validate that
externalSourceTLS.CertificateAuthority is non-nil and that
externalSourceTLS.CertificateAuthority.Name is not empty before calling
getCertificateAuthorityFromConfigMap; if missing, return a clear error (e.g.,
"missing certificate authority name for external source") instead of attempting
a ConfigMap lookup with an empty name so callers get a descriptive failure;
reference the function generateExternalClaimsSourceTLS and the helper
getCertificateAuthorityFromConfigMap and use cmLister only after the name check.


func generateExternalClaimsSourceURL(sourceURL configv1.SourceURL) (*authenticationv1alpha1.SourceURL, error) {
// TODO: enable validation when it is possible to do so. Currently blocked
// due to oauth-apiserver not being rebased on 1.35 and the KAS library changes
// not existing in the 1.35 branch.
// The following jira tickets track the work necessary to eventually enable this validation:
// 1. https://redhat.atlassian.net/browse/CNTRLPLANE-3491
// 2. https://redhat.atlassian.net/browse/CNTRLPLANE-3492
// 3. https://redhat.atlassian.net/browse/CNTRLPLANE-3493
/*
if err := validation.ValidateExternalClaimsSourceURLHostname(&sourceURL.Hostname, field.NewPath("")); err != nil {
return nil, fmt.Errorf("validating hostname: %w", kubeErrorListToGoError(err))
}

if err := validation.ValidateExternalClaimsSourceURLPathExpression(externaloidccel.NewCompiler(), &sourceURL.PathExpression, field.NewPath("")); err != nil {
return nil, fmt.Errorf("validating path expression: %w", kubeErrorListToGoError(err))
}
*/

return &authenticationv1alpha1.SourceURL{
Hostname: &sourceURL.Hostname,
PathExpression: &sourceURL.PathExpression,
}, nil
}

func generateExternalClaimsSourceMappings(seenClaimNames sets.Set[string], sourcedClaimMappings ...configv1.SourcedClaimMapping) ([]authenticationv1alpha1.SourcedClaimMapping, error) {
out := make([]authenticationv1alpha1.SourcedClaimMapping, 0, len(sourcedClaimMappings))

errs := []error{}
for _, sourcedClaimMapping := range sourcedClaimMappings {
// TODO: enable validation when it is possible to do so. Currently blocked
// due to oauth-apiserver not being rebased on 1.35 and the KAS library changes
// not existing in the 1.35 branch.
// The following jira tickets track the work necessary to eventually enable this validation:
// 1. https://redhat.atlassian.net/browse/CNTRLPLANE-3491
// 2. https://redhat.atlassian.net/browse/CNTRLPLANE-3492
// 3. https://redhat.atlassian.net/browse/CNTRLPLANE-3493
/*
if err := validation.ValidateExternalClaimsSourceMappingName(&sourcedClaimMapping.Name, seenClaimNames, field.NewPath("")); err != nil {
errs = append(errs, fmt.Errorf("validating mappings[%d]: validating name %q: %w", i, sourcedClaimMapping.Name, kubeErrorListToGoError(err)))
continue
}

if err := validation.ValidateExternalClaimsSourceMappingExpression(externaloidccel.NewCompiler(), &sourcedClaimMapping.Expression, field.NewPath("")); err != nil {
errs = append(errs, fmt.Errorf("validating mappings[%d]: validating expression %q: %w", i, sourcedClaimMapping.Expression, kubeErrorListToGoError(err)))
continue
}
*/

out = append(out, authenticationv1alpha1.SourcedClaimMapping{
Name: &sourcedClaimMapping.Name,
Expression: &sourcedClaimMapping.Expression,
})
}

return out, errors.Join(errs...)
}

func generateExternalClaimsSourceConditions(externalSourcePredicates ...configv1.ExternalSourcePredicate) ([]authenticationv1alpha1.ExternalSourceCondition, error) {
out := make([]authenticationv1alpha1.ExternalSourceCondition, 0, len(externalSourcePredicates))

errs := []error{}
// seenConditions := sets.New[string]()
for _, predicate := range externalSourcePredicates {
// TODO: enable validation when it is possible to do so. Currently blocked
// due to oauth-apiserver not being rebased on 1.35 and the KAS library changes
// not existing in the 1.35 branch.
// The following jira tickets track the work necessary to eventually enable this validation:
// 1. https://redhat.atlassian.net/browse/CNTRLPLANE-3491
// 2. https://redhat.atlassian.net/browse/CNTRLPLANE-3492
// 3. https://redhat.atlassian.net/browse/CNTRLPLANE-3493
/*
cond := authentication.ExternalSourceCondition{
Expression: &predicate.Expression,
}

if err := validation.ValidateExternalSourceCondition(externaloidccel.NewCompiler(), cond, seenConditions, field.NewPath("")); err != nil {
errs = append(errs, fmt.Errorf("validating predicates[%d]: validating expression %q: %w", i, predicate.Expression, kubeErrorListToGoError(err)))
}
*/

out = append(out, authenticationv1alpha1.ExternalSourceCondition{
Expression: &predicate.Expression,
})
}

return out, errors.Join(errs...)
}

// TODO: enable validation when it is possible to do so. Currently blocked
// due to oauth-apiserver not being rebased on 1.35 and the KAS library changes
// not existing in the 1.35 branch.
// The following jira tickets track the work necessary to eventually enable this validation:
// 1. https://redhat.atlassian.net/browse/CNTRLPLANE-3491
// 2. https://redhat.atlassian.net/browse/CNTRLPLANE-3492
// 3. https://redhat.atlassian.net/browse/CNTRLPLANE-3493
/*
func kubeErrorListToGoError(list field.ErrorList) error {
errs := make([]error, 0, len(list))
for _, err := range list {
errs = append(errs, errors.New(fmt.Sprintf("%s: %s", err.Type.String(), err.Detail)))
}

return errors.Join(errs...)
}
*/
Loading