diff --git a/api/v1alpha1/organization_types.go b/api/v1alpha1/organization_types.go old mode 100644 new mode 100755 index e078ab2dd..e55d4abdf --- a/api/v1alpha1/organization_types.go +++ b/api/v1alpha1/organization_types.go @@ -85,6 +85,19 @@ type OIDCConfig struct { // OAuth2ClientRedirectURIs are a registered set of redirect URIs. When redirecting from the idproxy to // the client application, the URI requested to redirect to must be contained in this list. OAuth2ClientRedirectURIs []string `json:"oauth2ClientRedirectURIs,omitempty"` + // ExtraConfig contains additional OIDC configuration for claim mapping and token validation behavior. + ExtraConfig *OIDCExtraConfig `json:"extraConfig,omitempty"` +} + +type OIDCExtraConfig struct { + // InsecureSkipEmailVerified if set to true, treats email_verified as true when the claim is absent from the ID token. + // This does not override an explicit email_verified=false. Only enable for providers that omit the claim entirely (e.g. some Okta, EntraID or CloudFoundry configurations). + // +kubebuilder:default:=false + InsecureSkipEmailVerified bool `json:"insecureSkipEmailVerified,omitempty"` + // UserIDClaim is the claim to be used as both user ID and username. + // When set, it overrides both UserIDKey and UserNameKey in the dex OIDC connector config. + // +kubebuilder:default:="login_name" + UserIDClaim string `json:"userIDClaim,omitempty"` } type SCIMConfig struct { diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 9a6a8fb42..f036f579f 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -861,6 +861,11 @@ func (in *OIDCConfig) DeepCopyInto(out *OIDCConfig) { *out = make([]string, len(*in)) copy(*out, *in) } + if in.ExtraConfig != nil { + in, out := &in.ExtraConfig, &out.ExtraConfig + *out = new(OIDCExtraConfig) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OIDCConfig. @@ -873,6 +878,21 @@ func (in *OIDCConfig) DeepCopy() *OIDCConfig { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OIDCExtraConfig) DeepCopyInto(out *OIDCExtraConfig) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OIDCExtraConfig. +func (in *OIDCExtraConfig) DeepCopy() *OIDCExtraConfig { + if in == nil { + return nil + } + out := new(OIDCExtraConfig) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *OptionsOverride) DeepCopyInto(out *OptionsOverride) { *out = *in diff --git a/charts/manager/crds/greenhouse.sap_catalogs.yaml b/charts/manager/crds/greenhouse.sap_catalogs.yaml index 48bf3de2a..93ba9aeb7 100644 --- a/charts/manager/crds/greenhouse.sap_catalogs.yaml +++ b/charts/manager/crds/greenhouse.sap_catalogs.yaml @@ -6,7 +6,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.20.0 + controller-gen.kubebuilder.io/version: v0.20.1 name: catalogs.greenhouse.sap spec: group: greenhouse.sap diff --git a/charts/manager/crds/greenhouse.sap_clusterkubeconfigs.yaml b/charts/manager/crds/greenhouse.sap_clusterkubeconfigs.yaml index b3850d16a..80ee964aa 100644 --- a/charts/manager/crds/greenhouse.sap_clusterkubeconfigs.yaml +++ b/charts/manager/crds/greenhouse.sap_clusterkubeconfigs.yaml @@ -6,7 +6,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.20.0 + controller-gen.kubebuilder.io/version: v0.20.1 name: clusterkubeconfigs.greenhouse.sap spec: group: greenhouse.sap diff --git a/charts/manager/crds/greenhouse.sap_clusterplugindefinitions.yaml b/charts/manager/crds/greenhouse.sap_clusterplugindefinitions.yaml index 1d2934629..ce044ba36 100644 --- a/charts/manager/crds/greenhouse.sap_clusterplugindefinitions.yaml +++ b/charts/manager/crds/greenhouse.sap_clusterplugindefinitions.yaml @@ -6,7 +6,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.20.0 + controller-gen.kubebuilder.io/version: v0.20.1 name: clusterplugindefinitions.greenhouse.sap spec: group: greenhouse.sap diff --git a/charts/manager/crds/greenhouse.sap_clusters.yaml b/charts/manager/crds/greenhouse.sap_clusters.yaml index 285292d74..48abd996a 100644 --- a/charts/manager/crds/greenhouse.sap_clusters.yaml +++ b/charts/manager/crds/greenhouse.sap_clusters.yaml @@ -6,7 +6,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.20.0 + controller-gen.kubebuilder.io/version: v0.20.1 name: clusters.greenhouse.sap spec: group: greenhouse.sap diff --git a/charts/manager/crds/greenhouse.sap_organizations.yaml b/charts/manager/crds/greenhouse.sap_organizations.yaml index 3873d2d53..8ac21917b 100644 --- a/charts/manager/crds/greenhouse.sap_organizations.yaml +++ b/charts/manager/crds/greenhouse.sap_organizations.yaml @@ -6,7 +6,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.20.0 + controller-gen.kubebuilder.io/version: v0.20.1 name: organizations.greenhouse.sap spec: group: greenhouse.sap @@ -92,6 +92,23 @@ spec: - key - name type: object + extraConfig: + description: ExtraConfig contains additional OIDC configuration + for claim mapping and token validation behavior. + properties: + insecureSkipEmailVerified: + default: false + description: |- + InsecureSkipEmailVerified if set to true, treats email_verified as true when the claim is absent from the ID token. + This does not override an explicit email_verified=false. Only enable for providers that omit the claim entirely (e.g. some Okta, EntraID or CloudFoundry configurations). + type: boolean + userIDClaim: + default: login_name + description: |- + UserIDClaim is the claim to be used as both user ID and username. + When set, it overrides both UserIDKey and UserNameKey in the dex OIDC connector config. + type: string + type: object issuer: description: Issuer is the URL of the identity service. type: string diff --git a/charts/manager/crds/greenhouse.sap_plugindefinitions.yaml b/charts/manager/crds/greenhouse.sap_plugindefinitions.yaml index d85bbd8d6..f61a82921 100644 --- a/charts/manager/crds/greenhouse.sap_plugindefinitions.yaml +++ b/charts/manager/crds/greenhouse.sap_plugindefinitions.yaml @@ -6,7 +6,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.20.0 + controller-gen.kubebuilder.io/version: v0.20.1 name: plugindefinitions.greenhouse.sap spec: group: greenhouse.sap diff --git a/charts/manager/crds/greenhouse.sap_pluginpresets.yaml b/charts/manager/crds/greenhouse.sap_pluginpresets.yaml index 08ac86452..55af83d7c 100644 --- a/charts/manager/crds/greenhouse.sap_pluginpresets.yaml +++ b/charts/manager/crds/greenhouse.sap_pluginpresets.yaml @@ -6,7 +6,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.20.0 + controller-gen.kubebuilder.io/version: v0.20.1 name: pluginpresets.greenhouse.sap spec: group: greenhouse.sap diff --git a/charts/manager/crds/greenhouse.sap_plugins.yaml b/charts/manager/crds/greenhouse.sap_plugins.yaml index 1ace7d008..e016b0476 100644 --- a/charts/manager/crds/greenhouse.sap_plugins.yaml +++ b/charts/manager/crds/greenhouse.sap_plugins.yaml @@ -6,7 +6,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.20.0 + controller-gen.kubebuilder.io/version: v0.20.1 name: plugins.greenhouse.sap spec: group: greenhouse.sap diff --git a/charts/manager/crds/greenhouse.sap_teamrolebindings.yaml b/charts/manager/crds/greenhouse.sap_teamrolebindings.yaml index 0e6e02d1c..ed3db6e8a 100644 --- a/charts/manager/crds/greenhouse.sap_teamrolebindings.yaml +++ b/charts/manager/crds/greenhouse.sap_teamrolebindings.yaml @@ -6,7 +6,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.20.0 + controller-gen.kubebuilder.io/version: v0.20.1 name: teamrolebindings.greenhouse.sap spec: group: greenhouse.sap diff --git a/charts/manager/crds/greenhouse.sap_teamroles.yaml b/charts/manager/crds/greenhouse.sap_teamroles.yaml index ecd3cc24f..2439217df 100644 --- a/charts/manager/crds/greenhouse.sap_teamroles.yaml +++ b/charts/manager/crds/greenhouse.sap_teamroles.yaml @@ -6,7 +6,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.20.0 + controller-gen.kubebuilder.io/version: v0.20.1 name: teamroles.greenhouse.sap spec: group: greenhouse.sap diff --git a/charts/manager/crds/greenhouse.sap_teams.yaml b/charts/manager/crds/greenhouse.sap_teams.yaml index 764010e5a..abe5ca3ca 100644 --- a/charts/manager/crds/greenhouse.sap_teams.yaml +++ b/charts/manager/crds/greenhouse.sap_teams.yaml @@ -6,7 +6,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.20.0 + controller-gen.kubebuilder.io/version: v0.20.1 name: teams.greenhouse.sap spec: group: greenhouse.sap diff --git a/config/samples/organization/demo.yaml b/config/samples/organization/demo.yaml index c9f091bd4..051ad3796 100644 --- a/config/samples/organization/demo.yaml +++ b/config/samples/organization/demo.yaml @@ -65,3 +65,5 @@ spec: # name: demo-oidc # issuer: https://global.accounts.dev # redirectURI: https://bogus.accounts.foo +# extraConfig: +# userIDClaim: email diff --git a/docs/reference/api/index.html b/docs/reference/api/index.html index c0d619379..d3d181a36 100644 --- a/docs/reference/api/index.html +++ b/docs/reference/api/index.html @@ -2083,6 +2083,63 @@

OIDCConfig the client application, the URI requested to redirect to must be contained in this list.

+ + +extraConfig
+ + +OIDCExtraConfig + + + + +

ExtraConfig contains additional OIDC configuration for claim mapping and token validation behavior.

+ + + + + + +

OIDCExtraConfig +

+

+(Appears on: +OIDCConfig) +

+
+
+ + + + + + + + + + + + + + + +
FieldDescription
+insecureSkipEmailVerified
+ +bool + +
+

InsecureSkipEmailVerified if set to true, treats email_verified as true when the claim is absent from the ID token. +This does not override an explicit email_verified=false. Only enable for providers that omit the claim entirely (e.g. some Okta, EntraID or CloudFoundry configurations).

+
+userIDClaim
+ +string + +
+

UserIDClaim is the claim to be used as both user ID and username. +When set, it overrides both UserIDKey and UserNameKey in the dex OIDC connector config.

+
diff --git a/docs/reference/api/openapi.yaml b/docs/reference/api/openapi.yaml index 6fe406f38..6418f3628 100755 --- a/docs/reference/api/openapi.yaml +++ b/docs/reference/api/openapi.yaml @@ -719,6 +719,22 @@ components: - key - name type: object + extraConfig: + description: ExtraConfig contains additional OIDC configuration for claim mapping and token validation behavior. + properties: + insecureSkipEmailVerified: + default: false + description: |- + InsecureSkipEmailVerified if set to true, treats email_verified as true when the claim is absent from the ID token. + This does not override an explicit email_verified=false. Only enable for providers that omit the claim entirely (e.g. some Okta, EntraID or CloudFoundry configurations). + type: boolean + userIDClaim: + default: login_name + description: |- + UserIDClaim is the claim to be used as both user ID and username. + When set, it overrides both UserIDKey and UserNameKey in the dex OIDC connector config. + type: string + type: object issuer: description: Issuer is the URL of the identity service. type: string diff --git a/internal/controller/organization/dex.go b/internal/controller/organization/dex.go old mode 100644 new mode 100755 index a027f8c2f..8826d39bc --- a/internal/controller/organization/dex.go +++ b/internal/controller/organization/dex.go @@ -113,15 +113,24 @@ func (r *OrganizationReconciler) reconcileDexConnector(ctx context.Context, org if err != nil { return err } + var userNameKey = "login_name" + var skipEmailVerified = false + if org.Spec.Authentication.OIDCConfig.ExtraConfig != nil { + if org.Spec.Authentication.OIDCConfig.ExtraConfig.UserIDClaim != "" { + userNameKey = org.Spec.Authentication.OIDCConfig.ExtraConfig.UserIDClaim + } + skipEmailVerified = org.Spec.Authentication.OIDCConfig.ExtraConfig.InsecureSkipEmailVerified + } oidcConfig := &oidc.Config{ - Issuer: org.Spec.Authentication.OIDCConfig.Issuer, - ClientID: clientID, - ClientSecret: clientSecret, - RedirectURI: redirectURL, - UserNameKey: "login_name", - UserIDKey: "login_name", - InsecureSkipVerify: true, - InsecureEnableGroups: true, + Issuer: org.Spec.Authentication.OIDCConfig.Issuer, + ClientID: clientID, + ClientSecret: clientSecret, + RedirectURI: redirectURL, + UserNameKey: userNameKey, + UserIDKey: userNameKey, + InsecureSkipEmailVerified: skipEmailVerified, + InsecureSkipVerify: true, + InsecureEnableGroups: true, } configByte, err := json.Marshal(oidcConfig) if err != nil { diff --git a/internal/controller/organization/organization_controller_test.go b/internal/controller/organization/organization_controller_test.go index e09efb785..ff7d77af6 100644 --- a/internal/controller/organization/organization_controller_test.go +++ b/internal/controller/organization/organization_controller_test.go @@ -4,10 +4,12 @@ package organization_test import ( + "encoding/json" "fmt" "slices" + dexoidc "github.com/dexidp/dex/connector/oidc" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" . "github.com/onsi/gomega/gstruct" @@ -390,6 +392,61 @@ var _ = Describe("Test Organization reconciliation", Ordered, func() { } test.EventuallyDeleted(test.Ctx, test.K8sClient, team) }) + + It("should propagate ExtraConfig to dex connector", func() { + team := setup.CreateTeam(test.Ctx, "test-team-extra", test.WithTeamLabel(greenhouseapis.LabelKeySupportGroup, "true")) + + defaultOrg := setup.CreateDefaultOrgWithOIDCSecret(test.Ctx, team.Name) + test.EventuallyCreated(test.Ctx, test.K8sClient, &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: defaultOrg.Name}}) + Eventually(func(g Gomega) { + err := test.K8sClient.Get(test.Ctx, types.NamespacedName{Name: defaultOrg.Name}, defaultOrg) + g.Expect(err).ToNot(HaveOccurred()) + oidcCondition := defaultOrg.Status.GetConditionByType(greenhousev1alpha1.OrganizationOICDConfigured) + g.Expect(oidcCondition).ToNot(BeNil()) + g.Expect(oidcCondition.IsTrue()).To(BeTrue()) + }).Should(Succeed()) + + By("creating a test organization with ExtraConfig") + extraConfigOrg := setup.CreateOrganization(test.Ctx, "test-extraconfig-org", test.WithMappedAdminIDPGroup("EXTRA_CONFIG_GROUP")) + test.EventuallyCreated(test.Ctx, test.K8sClient, &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: extraConfigOrg.Name}}) + + oidcSecret := setup.CreateOrgOIDCSecret(test.Ctx, extraConfigOrg.Name, team.Name) + extraConfigOrg = setup.UpdateOrganization(test.Ctx, + extraConfigOrg.Name, + test.WithOIDCConfig(test.OIDCIssuer, oidcSecret.Name, test.OIDCClientIDKey, test.OIDCClientSecretKey), + test.WithOIDCExtraConfig("email", true), + ) + + By("checking Organization OIDC status is ready") + Eventually(func(g Gomega) { + err := test.K8sClient.Get(test.Ctx, types.NamespacedName{Name: extraConfigOrg.Name}, extraConfigOrg) + g.Expect(err).ToNot(HaveOccurred()) + oidcCondition := extraConfigOrg.Status.GetConditionByType(greenhousev1alpha1.OrganizationOICDConfigured) + g.Expect(oidcCondition).ToNot(BeNil(), "OrganizationOICDConfigured should be set") + g.Expect(oidcCondition.IsTrue()).To(BeTrue(), "OrganizationOICDConfigured should be True") + }).Should(Succeed()) + + if DexStorageType == dex.K8s { + By("verifying the dex connector config contains ExtraConfig values") + connectors := &dexapi.ConnectorList{} + err := setup.List(test.Ctx, connectors) + Expect(err).ToNot(HaveOccurred()) + + filteredConnectors := slices.DeleteFunc(connectors.Items, func(c dexapi.Connector) bool { + return c.ID != extraConfigOrg.Name + }) + Expect(filteredConnectors).To(HaveLen(1), "there should be exactly one connector for the org") + + var connectorConfig dexoidc.Config + Expect(json.Unmarshal(filteredConnectors[0].Config, &connectorConfig)).To(Succeed(), "connector config should be valid JSON") + Expect(connectorConfig.UserNameKey).To(Equal("email"), "UserNameKey should be set to the custom claim") + Expect(connectorConfig.UserIDKey).To(Equal("email"), "UserIDKey should be set to the custom claim") + Expect(connectorConfig.InsecureSkipEmailVerified).To(BeTrue(), "InsecureSkipEmailVerified should be true") + } + + test.EventuallyDeleted(test.Ctx, test.K8sClient, &greenhousev1alpha1.Organization{ObjectMeta: metav1.ObjectMeta{Name: extraConfigOrg.Name}}) + test.EventuallyDeleted(test.Ctx, test.K8sClient, team) + }) }) When("reconciling PluginDefinitionCatalog ServiceAccount for regular organization", Ordered, func() { diff --git a/internal/test/resources.go b/internal/test/resources.go index 07b62c663..9aaa41660 100644 --- a/internal/test/resources.go +++ b/internal/test/resources.go @@ -133,6 +133,23 @@ func WithOIDCConfig(issuer, secretName, clientIDKey, clientSecretKey string) fun } } +// WithOIDCExtraConfig sets the OIDCExtraConfig on an Organization's OIDCConfig. +// Must be used after WithOIDCConfig to ensure OIDCConfig is initialized. +func WithOIDCExtraConfig(userIDClaim string, insecureSkipEmailVerified bool) func(*greenhousev1alpha1.Organization) { + return func(org *greenhousev1alpha1.Organization) { + if org.Spec.Authentication == nil { + org.Spec.Authentication = &greenhousev1alpha1.Authentication{} + } + if org.Spec.Authentication.OIDCConfig == nil { + org.Spec.Authentication.OIDCConfig = &greenhousev1alpha1.OIDCConfig{} + } + org.Spec.Authentication.OIDCConfig.ExtraConfig = &greenhousev1alpha1.OIDCExtraConfig{ + UserIDClaim: userIDClaim, + InsecureSkipEmailVerified: insecureSkipEmailVerified, + } + } +} + // NewOrganization returns a greenhousev1alpha1.Organization object. Opts can be used to set the desired state of the Organization. func NewOrganization(ctx context.Context, name string, opts ...func(*greenhousev1alpha1.Organization)) *greenhousev1alpha1.Organization { org := &greenhousev1alpha1.Organization{ diff --git a/types/typescript/schema.d.ts b/types/typescript/schema.d.ts index 5f3987e5c..41ebe1fa5 100644 --- a/types/typescript/schema.d.ts +++ b/types/typescript/schema.d.ts @@ -556,6 +556,21 @@ export interface components { /** @description Name of the secret in the same namespace. */ name: string; }; + /** @description ExtraConfig contains additional OIDC configuration for claim mapping and token validation behavior. */ + extraConfig?: { + /** + * @description InsecureSkipEmailVerified if set to true, treats email_verified as true when the claim is absent from the ID token. + * This does not override an explicit email_verified=false. Only enable for providers that omit the claim entirely (e.g. some Okta, EntraID or CloudFoundry configurations). + * @default false + */ + insecureSkipEmailVerified: boolean; + /** + * @description UserIDClaim is the claim to be used as both user ID and username. + * When set, it overrides both UserIDKey and UserNameKey in the dex OIDC connector config. + * @default login_name + */ + userIDClaim: string; + }; /** @description Issuer is the URL of the identity service. */ issuer: string; /**