From c08b620e8ff05909474d1e7dce07870b1b14f944 Mon Sep 17 00:00:00 2001 From: Johannes Aubart Date: Mon, 8 Sep 2025 15:27:46 +0200 Subject: [PATCH 1/4] fix oidc config validation --- api/common/oidc_types.go | 36 ++++++++----------- ...clusters.openmcp.cloud_accessrequests.yaml | 30 +++++++--------- ....openmcp.cloud_managedcontrolplanev2s.yaml | 30 +++++++--------- 3 files changed, 41 insertions(+), 55 deletions(-) diff --git a/api/common/oidc_types.go b/api/common/oidc_types.go index 07f3881e..bd9d1e04 100644 --- a/api/common/oidc_types.go +++ b/api/common/oidc_types.go @@ -1,23 +1,29 @@ package common import ( - "strings" - rbacv1 "k8s.io/api/rbac/v1" ) type OIDCProviderConfig struct { // Name is the name of the OIDC provider. // May be used in k8s resources, therefore has to be a valid k8s name. + // It is also used (with a ':' suffix) as prefix in k8s resources referencing users or groups from this OIDC provider. + // E.g. if the name is 'example', the username 'alice' from this provider will be referenced as 'example:alice' in k8s resources. + // Must be unique among all OIDC providers configured in the same environment. // +kubebuilder:validation:MinLength=1 // +kubebuilder:validation:MaxLength=253 - // +kubebuilder:validation:Pattern=`[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*` + // +kubebuilder:validation:Pattern=`^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$` + // +kubebuilder:validation:XValidation:rule=`self != "system"`, message="'system' is a reserved string and may not be used as OIDC provider name" Name string `json:"name"` // Issuer is the issuer URL of the OIDC provider. + // Must be a valid URL. + // +kubebuilder:validation:Pattern=`^https?://[^\s/$.?#].[^\s]*$` + // +kubebuilder:validation:MinLength=1 Issuer string `json:"issuer"` // ClientID is the client ID to use for the OIDC provider. + // +kubebuilder:validation:MinLength=1 ClientID string `json:"clientID"` // GroupsClaim is the claim in the OIDC token that contains the groups. @@ -26,24 +32,12 @@ type OIDCProviderConfig struct { // +optional GroupsClaim string `json:"groupsClaim"` - // GroupsPrefix is a prefix that will be added to all group names when referenced in RBAC rules. - // This is required to avoid conflicts with Kubernetes built-in groups. - // If the prefix does not end with a colon (:), it will be added automatically. - // +kubebuilder:validation:MinLength=1 - GroupsPrefix string `json:"groupsPrefix"` - // UsernameClaim is the claim in the OIDC token that contains the username. // If empty, the default claim "sub" will be used. // +kubebuilder:default="sub" // +optional UsernameClaim string `json:"usernameClaim"` - // UsernamePrefix is a prefix that will be added to all usernames when referenced in RBAC rules. - // This is required to avoid conflicts with Kubernetes built-in users. - // If the prefix does not end with a colon (:), it will be added automatically. - // +kubebuilder:validation:MinLength=1 - UsernamePrefix string `json:"usernamePrefix"` - // ExtraScopes is a list of extra scopes that should be requested from the OIDC provider. // +optional ExtraScopes []string `json:"extraScopes,omitempty"` @@ -90,14 +84,14 @@ func (o *OIDCProviderConfig) Default() *OIDCProviderConfig { if o.GroupsClaim == "" { o.GroupsClaim = "groups" } - if !strings.HasSuffix(o.GroupsPrefix, ":") { - o.GroupsPrefix += ":" - } if o.UsernameClaim == "" { o.UsernameClaim = "sub" } - if !strings.HasSuffix(o.UsernamePrefix, ":") { - o.UsernamePrefix += ":" - } return o } + +// UsernameGroupsPrefix returns the prefix for usernames and groups for this OIDC provider. +// It is equivalent to + ":". +func (o *OIDCProviderConfig) UsernameGroupsPrefix() string { + return o.Name + ":" +} diff --git a/api/crds/manifests/clusters.openmcp.cloud_accessrequests.yaml b/api/crds/manifests/clusters.openmcp.cloud_accessrequests.yaml index 5cecd5d1..b07a7cda 100644 --- a/api/crds/manifests/clusters.openmcp.cloud_accessrequests.yaml +++ b/api/crds/manifests/clusters.openmcp.cloud_accessrequests.yaml @@ -73,6 +73,7 @@ spec: properties: clientID: description: ClientID is the client ID to use for the OIDC provider. + minLength: 1 type: string extraScopes: description: ExtraScopes is a list of extra scopes that should @@ -86,24 +87,28 @@ spec: GroupsClaim is the claim in the OIDC token that contains the groups. If empty, the default claim "groups" will be used. type: string - groupsPrefix: + issuer: description: |- - GroupsPrefix is a prefix that will be added to all group names when referenced in RBAC rules. - This is required to avoid conflicts with Kubernetes built-in groups. - If the prefix does not end with a colon (:), it will be added automatically. + Issuer is the issuer URL of the OIDC provider. + Must be a valid URL. minLength: 1 - type: string - issuer: - description: Issuer is the issuer URL of the OIDC provider. + pattern: ^https?://[^\s/$.?#].[^\s]*$ type: string name: description: |- Name is the name of the OIDC provider. May be used in k8s resources, therefore has to be a valid k8s name. + It is also used (with a ':' suffix) as prefix in k8s resources referencing users or groups from this OIDC provider. + E.g. if the name is 'example', the username 'alice' from this provider will be referenced as 'example:alice' in k8s resources. + Must be unique among all OIDC providers configured in the same environment. maxLength: 253 minLength: 1 - pattern: '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*' + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ type: string + x-kubernetes-validations: + - message: '''system'' is a reserved string and may not be used + as OIDC provider name' + rule: self != "system" roleBindings: description: |- RoleBindings is a list of subjects with (cluster) role bindings that should be created for them. @@ -260,20 +265,11 @@ spec: UsernameClaim is the claim in the OIDC token that contains the username. If empty, the default claim "sub" will be used. type: string - usernamePrefix: - description: |- - UsernamePrefix is a prefix that will be added to all usernames when referenced in RBAC rules. - This is required to avoid conflicts with Kubernetes built-in users. - If the prefix does not end with a colon (:), it will be added automatically. - minLength: 1 - type: string required: - clientID - - groupsPrefix - issuer - name - roleBindings - - usernamePrefix type: object requestRef: description: |- diff --git a/api/crds/manifests/core.openmcp.cloud_managedcontrolplanev2s.yaml b/api/crds/manifests/core.openmcp.cloud_managedcontrolplanev2s.yaml index 858cd149..2230b979 100644 --- a/api/crds/manifests/core.openmcp.cloud_managedcontrolplanev2s.yaml +++ b/api/crds/manifests/core.openmcp.cloud_managedcontrolplanev2s.yaml @@ -58,6 +58,7 @@ spec: clientID: description: ClientID is the client ID to use for the OIDC provider. + minLength: 1 type: string extraScopes: description: ExtraScopes is a list of extra scopes that @@ -71,24 +72,28 @@ spec: GroupsClaim is the claim in the OIDC token that contains the groups. If empty, the default claim "groups" will be used. type: string - groupsPrefix: + issuer: description: |- - GroupsPrefix is a prefix that will be added to all group names when referenced in RBAC rules. - This is required to avoid conflicts with Kubernetes built-in groups. - If the prefix does not end with a colon (:), it will be added automatically. + Issuer is the issuer URL of the OIDC provider. + Must be a valid URL. minLength: 1 - type: string - issuer: - description: Issuer is the issuer URL of the OIDC provider. + pattern: ^https?://[^\s/$.?#].[^\s]*$ type: string name: description: |- Name is the name of the OIDC provider. May be used in k8s resources, therefore has to be a valid k8s name. + It is also used (with a ':' suffix) as prefix in k8s resources referencing users or groups from this OIDC provider. + E.g. if the name is 'example', the username 'alice' from this provider will be referenced as 'example:alice' in k8s resources. + Must be unique among all OIDC providers configured in the same environment. maxLength: 253 minLength: 1 - pattern: '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*' + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ type: string + x-kubernetes-validations: + - message: '''system'' is a reserved string and may not + be used as OIDC provider name' + rule: self != "system" roleBindings: description: |- RoleBindings is a list of subjects with (cluster) role bindings that should be created for them. @@ -171,20 +176,11 @@ spec: UsernameClaim is the claim in the OIDC token that contains the username. If empty, the default claim "sub" will be used. type: string - usernamePrefix: - description: |- - UsernamePrefix is a prefix that will be added to all usernames when referenced in RBAC rules. - This is required to avoid conflicts with Kubernetes built-in users. - If the prefix does not end with a colon (:), it will be added automatically. - minLength: 1 - type: string required: - clientID - - groupsPrefix - issuer - name - roleBindings - - usernamePrefix type: object x-kubernetes-validations: - message: OIDC provider name must not be 'default' as this From ec4fd0b9f2d0d211dfc82f4fd7952c0d92d8cdd9 Mon Sep 17 00:00:00 2001 From: Johannes Aubart Date: Mon, 8 Sep 2025 15:40:00 +0200 Subject: [PATCH 2/4] loosen restrictions on default oidc provider name and change default to 'openmcp' --- api/core/v2alpha1/constants.go | 2 +- internal/config/config_managedcontrolplane.go | 6 ++---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/api/core/v2alpha1/constants.go b/api/core/v2alpha1/constants.go index 5a79499c..e67f99f5 100644 --- a/api/core/v2alpha1/constants.go +++ b/api/core/v2alpha1/constants.go @@ -2,7 +2,7 @@ package v2alpha1 const ( // DefaultOIDCProviderName is the identifier for the default OIDC provider. - DefaultOIDCProviderName = "default" + DefaultOIDCProviderName = "openmcp" // DefaultMCPClusterPurpose is the default purpose for ManagedControlPlane clusters. DefaultMCPClusterPurpose = "mcp" ) diff --git a/internal/config/config_managedcontrolplane.go b/internal/config/config_managedcontrolplane.go index 9819ee64..f14ab717 100644 --- a/internal/config/config_managedcontrolplane.go +++ b/internal/config/config_managedcontrolplane.go @@ -1,8 +1,6 @@ package config import ( - "fmt" - "k8s.io/apimachinery/pkg/util/validation/field" commonapi "github.com/openmcp-project/openmcp-operator/api/common" @@ -54,8 +52,8 @@ func (c *ManagedControlPlaneConfig) Validate(fldPath *field.Path) error { if len(c.DefaultOIDCProvider.RoleBindings) > 0 { errs = append(errs, field.Forbidden(oidcFldPath.Child("roleBindings"), "role bindings are specified in the MCP spec and may not be set in the config")) } - if c.DefaultOIDCProvider.Name != "" && c.DefaultOIDCProvider.Name != corev2alpha1.DefaultOIDCProviderName { - errs = append(errs, field.Invalid(oidcFldPath.Child("name"), c.DefaultOIDCProvider.Name, fmt.Sprintf("standard OIDC provider name must be '%s' or left empty (in which case it will be defaulted)", corev2alpha1.DefaultOIDCProviderName))) + if c.DefaultOIDCProvider.Name == "system" { + errs = append(errs, field.Invalid(oidcFldPath.Child("name"), c.DefaultOIDCProvider.Name, "'system' is a reserved string and may not be used as name for the default OIDC provider")) } } From 2b75f42e6094f2f53faa9c72f2df00d6c81ee7c2 Mon Sep 17 00:00:00 2001 From: Johannes Aubart Date: Tue, 9 Sep 2025 09:34:00 +0200 Subject: [PATCH 3/4] validate oidc provider name uniqueness --- api/core/v2alpha1/constants.go | 2 + .../v2alpha1/managedcontrolplane_types.go | 1 - ....openmcp.cloud_managedcontrolplanev2s.yaml | 4 - cmd/openmcp-operator/app/mcp/init.go | 174 +++++++++++++++--- 4 files changed, 153 insertions(+), 28 deletions(-) diff --git a/api/core/v2alpha1/constants.go b/api/core/v2alpha1/constants.go index e67f99f5..7f3807e9 100644 --- a/api/core/v2alpha1/constants.go +++ b/api/core/v2alpha1/constants.go @@ -15,6 +15,8 @@ const ( // ManagedPurposeMCPPurposeOverride is used as value for the managed purpose label. It must not be modified. ManagedPurposeMCPPurposeOverride = "mcp-purpose-override" + // ManagedPurposeOIDCProviderNameUniqueness is used as value for the managed purpose label. It must not be modified. + ManagedPurposeOIDCProviderNameUniqueness = "oidc-provider-name-uniqueness" MCPFinalizer = GroupName + "/mcp" diff --git a/api/core/v2alpha1/managedcontrolplane_types.go b/api/core/v2alpha1/managedcontrolplane_types.go index eb43372c..d9ee3c4c 100644 --- a/api/core/v2alpha1/managedcontrolplane_types.go +++ b/api/core/v2alpha1/managedcontrolplane_types.go @@ -31,7 +31,6 @@ type IAMConfig struct { // OIDCProviders is a list of OIDC providers that should be configured for the ManagedControlPlaneV2. // They are independent of the standard OIDC provider and in addition to it, unless it has been disabled by not specifying any role bindings. - // +kubebuilder:validation:items:XValidation:rule="self.name != 'default'", message="OIDC provider name must not be 'default' as this is reserved for the standard OIDC provider" // +optional OIDCProviders []*commonapi.OIDCProviderConfig `json:"oidcProviders,omitempty"` } diff --git a/api/crds/manifests/core.openmcp.cloud_managedcontrolplanev2s.yaml b/api/crds/manifests/core.openmcp.cloud_managedcontrolplanev2s.yaml index 2230b979..29ac9008 100644 --- a/api/crds/manifests/core.openmcp.cloud_managedcontrolplanev2s.yaml +++ b/api/crds/manifests/core.openmcp.cloud_managedcontrolplanev2s.yaml @@ -182,10 +182,6 @@ spec: - name - roleBindings type: object - x-kubernetes-validations: - - message: OIDC provider name must not be 'default' as this - is reserved for the standard OIDC provider - rule: self.name != 'default' type: array roleBindings: description: |- diff --git a/cmd/openmcp-operator/app/mcp/init.go b/cmd/openmcp-operator/app/mcp/init.go index 70240f43..455f7423 100644 --- a/cmd/openmcp-operator/app/mcp/init.go +++ b/cmd/openmcp-operator/app/mcp/init.go @@ -13,7 +13,9 @@ import ( "k8s.io/utils/ptr" "sigs.k8s.io/controller-runtime/pkg/client" + "github.com/openmcp-project/controller-utils/pkg/clusters" crdutil "github.com/openmcp-project/controller-utils/pkg/crds" + "github.com/openmcp-project/controller-utils/pkg/logging" "github.com/openmcp-project/controller-utils/pkg/resources" clustersv1alpha1 "github.com/openmcp-project/openmcp-operator/api/clusters/v1alpha1" @@ -22,12 +24,16 @@ import ( "github.com/openmcp-project/openmcp-operator/api/crds" "github.com/openmcp-project/openmcp-operator/api/install" "github.com/openmcp-project/openmcp-operator/cmd/openmcp-operator/app/options" + "github.com/openmcp-project/openmcp-operator/internal/config" "github.com/openmcp-project/openmcp-operator/internal/controllers/managedcontrolplane" "github.com/openmcp-project/openmcp-operator/lib/clusteraccess" ) // currently hard-coded, can be made configurable in the future if needed -const MCPPurposeOverrideValidationPolicyName = "mcp-purpose-override-validation" +const ( + MCPPurposeOverrideValidationPolicyName = "mcp-purpose-override-validation." + corev2alpha1.GroupName + OIDCProviderNameUniquenessValidationPolicyName = "oidc-provider-name-uniqueness." + corev2alpha1.GroupName +) func NewInitCommand(po *options.PersistentOptions) *cobra.Command { opts := &InitOptions{ @@ -126,6 +132,42 @@ func (o *InitOptions) Run(ctx context.Context) error { } // ensure ValidatingAdmissionPolicy to prevent removal or changes to the MCP purpose override label + if err := o.ensureMCPPurposeOverrideImmutabilityValidationPolicy(ctx, log, onboardingCluster); err != nil { + return fmt.Errorf("error ensuring MCP purpose override immutability ValidatingAdmissionPolicy: %w", err) + } + + // ensure ValidatingAdmissionPolicy to prevent MCP resources to specify OIDC providers with the same name as the default OIDC provider + var mcpConfig *config.ManagedControlPlaneConfig + if o.Config != nil { + mcpConfig = o.Config.ManagedControlPlane + } + if err := o.ensureDefaultOIDCProviderNameUniquenessValidationPolicy(ctx, log, onboardingCluster, mcpConfig); err != nil { + return fmt.Errorf("error ensuring OIDC provider name uniqueness ValidatingAdmissionPolicy: %w", err) + } + + log.Info("Finished init command") + return nil +} + +func (o *InitOptions) PrintRaw(cmd *cobra.Command) {} + +func (o *InitOptions) PrintRawOptions(cmd *cobra.Command) { + cmd.Println("########## RAW OPTIONS START ##########") + o.PersistentOptions.PrintRaw(cmd) + o.PrintRaw(cmd) + cmd.Println("########## RAW OPTIONS END ##########") +} + +func (o *InitOptions) PrintCompleted(cmd *cobra.Command) {} + +func (o *InitOptions) PrintCompletedOptions(cmd *cobra.Command) { + cmd.Println("########## COMPLETED OPTIONS START ##########") + o.PersistentOptions.PrintCompleted(cmd) + o.PrintCompleted(cmd) + cmd.Println("########## COMPLETED OPTIONS END ##########") +} + +func (o *InitOptions) ensureMCPPurposeOverrideImmutabilityValidationPolicy(ctx context.Context, log logging.Logger, onboardingCluster *clusters.Cluster) error { labelSelector := client.MatchingLabels{ apiconst.ManagedByLabel: managedcontrolplane.ControllerName, apiconst.ManagedPurposeLabel: corev2alpha1.ManagedPurposeMCPPurposeOverride, @@ -136,7 +178,7 @@ func (o *InitOptions) Run(ctx context.Context) error { } for _, evapb := range evapbs.Items { if evapb.Name != MCPPurposeOverrideValidationPolicyName { - log.Info("Deleting existing ValidatingAdmissionPolicyBinding with architecture immutability purpose", "name", evapb.Name) + log.Info("Deleting existing ValidatingAdmissionPolicyBinding for MCP purpose immutability", "name", evapb.Name) if err := onboardingCluster.Client().Delete(ctx, &evapb); client.IgnoreNotFound(err) != nil { return fmt.Errorf("error deleting ValidatingAdmissionPolicyBinding '%s': %w", evapb.Name, err) } @@ -148,13 +190,13 @@ func (o *InitOptions) Run(ctx context.Context) error { } for _, evap := range evaps.Items { if evap.Name != MCPPurposeOverrideValidationPolicyName { - log.Info("Deleting existing ValidatingAdmissionPolicy with architecture immutability purpose", "name", evap.Name) + log.Info("Deleting existing ValidatingAdmissionPolicy for MCP purpose immutability", "name", evap.Name) if err := onboardingCluster.Client().Delete(ctx, &evap); client.IgnoreNotFound(err) != nil { return fmt.Errorf("error deleting ValidatingAdmissionPolicy '%s': %w", evap.Name, err) } } } - log.Info("creating/updating ValidatingAdmissionPolicies to prevent undesired changes to the MCP purpose override label ...") + log.Info("Creating/updating ValidatingAdmissionPolicies to prevent undesired changes to the MCP purpose override label ...") vapm := resources.NewValidatingAdmissionPolicyMutator(MCPPurposeOverrideValidationPolicyName, admissionv1.ValidatingAdmissionPolicySpec{ FailurePolicy: ptr.To(admissionv1.Fail), MatchConstraints: &admissionv1.MatchResources{ @@ -200,7 +242,7 @@ func (o *InitOptions) Run(ctx context.Context) error { apiconst.ManagedPurposeLabel: corev2alpha1.ManagedPurposeMCPPurposeOverride, }) if err := resources.CreateOrUpdateResource(ctx, onboardingCluster.Client(), vapm); err != nil { - return fmt.Errorf("error creating/updating ValidatingAdmissionPolicy for mcp purpose override validation: %w", err) + return fmt.Errorf("error creating/updating ValidatingAdmissionPolicy for MCP purpose immutability validation: %w", err) } vapbm := resources.NewValidatingAdmissionPolicyBindingMutator(MCPPurposeOverrideValidationPolicyName, admissionv1.ValidatingAdmissionPolicyBindingSpec{ @@ -233,28 +275,114 @@ func (o *InitOptions) Run(ctx context.Context) error { apiconst.ManagedPurposeLabel: corev2alpha1.ManagedPurposeMCPPurposeOverride, }) if err := resources.CreateOrUpdateResource(ctx, onboardingCluster.Client(), vapbm); err != nil { - return fmt.Errorf("error creating/updating ValidatingAdmissionPolicyBinding for mcp purpose override validation: %w", err) + return fmt.Errorf("error creating/updating ValidatingAdmissionPolicyBinding for MCP purpose immutability validation: %w", err) } - log.Info("ValidatingAdmissionPolicy and ValidatingAdmissionPolicyBinding for mcp purpose override validation created/updated") - - log.Info("Finished init command") + log.Info("ValidatingAdmissionPolicy and ValidatingAdmissionPolicyBinding for MCP purpose immutability validation created/updated") return nil } -func (o *InitOptions) PrintRaw(cmd *cobra.Command) {} - -func (o *InitOptions) PrintRawOptions(cmd *cobra.Command) { - cmd.Println("########## RAW OPTIONS START ##########") - o.PersistentOptions.PrintRaw(cmd) - o.PrintRaw(cmd) - cmd.Println("########## RAW OPTIONS END ##########") -} +func (o *InitOptions) ensureDefaultOIDCProviderNameUniquenessValidationPolicy(ctx context.Context, log logging.Logger, onboardingCluster *clusters.Cluster, mcpConfig *config.ManagedControlPlaneConfig) error { + if mcpConfig == nil { + mcpConfig = &config.ManagedControlPlaneConfig{} + if err := mcpConfig.Default(nil); err != nil { + return fmt.Errorf("error defaulting ManagedControlPlane controller config: %w", err) + } + } + defaultOIDCProvider := mcpConfig.DefaultOIDCProvider + defaultOIDCProviderName := "" + if defaultOIDCProvider != nil { + defaultOIDCProvider.Default() + defaultOIDCProviderName = defaultOIDCProvider.Name + } -func (o *InitOptions) PrintCompleted(cmd *cobra.Command) {} + labelSelector := client.MatchingLabels{ + apiconst.ManagedByLabel: managedcontrolplane.ControllerName, + apiconst.ManagedPurposeLabel: corev2alpha1.ManagedPurposeOIDCProviderNameUniqueness, + } + evapbs := &admissionv1.ValidatingAdmissionPolicyBindingList{} + if err := onboardingCluster.Client().List(ctx, evapbs, labelSelector); err != nil { + return fmt.Errorf("error listing ValidatingAdmissionPolicyBindings: %w", err) + } + for _, evapb := range evapbs.Items { + if evapb.Name != OIDCProviderNameUniquenessValidationPolicyName { + log.Info("Deleting existing ValidatingAdmissionPolicyBinding for OIDC provider name uniqueness", "name", evapb.Name) + if err := onboardingCluster.Client().Delete(ctx, &evapb); client.IgnoreNotFound(err) != nil { + return fmt.Errorf("error deleting ValidatingAdmissionPolicyBinding '%s': %w", evapb.Name, err) + } + } + } + evaps := &admissionv1.ValidatingAdmissionPolicyList{} + if err := onboardingCluster.Client().List(ctx, evaps, labelSelector); err != nil { + return fmt.Errorf("error listing ValidatingAdmissionPolicies: %w", err) + } + for _, evap := range evaps.Items { + if evap.Name != OIDCProviderNameUniquenessValidationPolicyName { + log.Info("Deleting existing ValidatingAdmissionPolicy for OIDC provider name uniqueness", "name", evap.Name) + if err := onboardingCluster.Client().Delete(ctx, &evap); client.IgnoreNotFound(err) != nil { + return fmt.Errorf("error deleting ValidatingAdmissionPolicy '%s': %w", evap.Name, err) + } + } + } + log.Info("Creating/updating ValidatingAdmissionPolicies to ensure that no MCP specifies duplicate OIDC provider names or an OIDC provider with the same name as the standard OIDC provider ...") + vapm := resources.NewValidatingAdmissionPolicyMutator(OIDCProviderNameUniquenessValidationPolicyName, admissionv1.ValidatingAdmissionPolicySpec{ + FailurePolicy: ptr.To(admissionv1.Fail), + MatchConstraints: &admissionv1.MatchResources{ + ResourceRules: []admissionv1.NamedRuleWithOperations{ + { + RuleWithOperations: admissionv1.RuleWithOperations{ + Operations: []admissionv1.OperationType{ + admissionv1.Create, + admissionv1.Update, + }, + Rule: admissionv1.Rule{ + APIGroups: []string{corev2alpha1.GroupVersion.Group}, + APIVersions: []string{corev2alpha1.GroupVersion.Version}, + Resources: []string{ + "managedcontrolplanev2s", + }, + }, + }, + }, + }, + }, + Variables: []admissionv1.Variable{ + { + Name: "defaultOIDCProviderName", + Expression: fmt.Sprintf(`"%s"`, defaultOIDCProviderName), + }, + }, + Validations: []admissionv1.Validation{ + { + Expression: `!(has(object.spec.iam.oidcProviders) && ( + object.spec.iam.oidcProviders.exists(elem, elem.name == variables.defaultOIDCProviderName) || + object.spec.iam.oidcProviders.exists(elem, !object.spec.iam.oidcProviders.exists_one(elem2, elem2.name == elem.name)) +)) +`, + Message: fmt.Sprintf(`There are no duplicate names allowed in spec.iam.oidcProviders and no OIDC provider may have the same name the default OIDC provider, which is "%s"`, defaultOIDCProviderName), + }, + }, + }) + vapm.MetadataMutator().WithLabels(map[string]string{ + apiconst.ManagedByLabel: managedcontrolplane.ControllerName, + apiconst.ManagedPurposeLabel: corev2alpha1.ManagedPurposeOIDCProviderNameUniqueness, + }) + if err := resources.CreateOrUpdateResource(ctx, onboardingCluster.Client(), vapm); err != nil { + return fmt.Errorf("error creating/updating ValidatingAdmissionPolicy for OIDC provider name uniqueness validation: %w", err) + } -func (o *InitOptions) PrintCompletedOptions(cmd *cobra.Command) { - cmd.Println("########## COMPLETED OPTIONS START ##########") - o.PersistentOptions.PrintCompleted(cmd) - o.PrintCompleted(cmd) - cmd.Println("########## COMPLETED OPTIONS END ##########") + vapbm := resources.NewValidatingAdmissionPolicyBindingMutator(OIDCProviderNameUniquenessValidationPolicyName, admissionv1.ValidatingAdmissionPolicyBindingSpec{ + PolicyName: OIDCProviderNameUniquenessValidationPolicyName, + ValidationActions: []admissionv1.ValidationAction{ + admissionv1.Deny, + }, + }) + vapbm.MetadataMutator().WithLabels(map[string]string{ + apiconst.ManagedByLabel: managedcontrolplane.ControllerName, + apiconst.ManagedPurposeLabel: corev2alpha1.ManagedPurposeOIDCProviderNameUniqueness, + }) + if err := resources.CreateOrUpdateResource(ctx, onboardingCluster.Client(), vapbm); err != nil { + return fmt.Errorf("error creating/updating ValidatingAdmissionPolicyBinding for OIDC provider name uniqueness validation: %w", err) + } + log.Info("ValidatingAdmissionPolicy and ValidatingAdmissionPolicyBinding for OIDC provider name uniqueness validation created/updated") + return nil } From ebd6cbd72a48ef21762b0a3c8978d2a87f32577c Mon Sep 17 00:00:00 2001 From: Johannes Aubart Date: Tue, 9 Sep 2025 09:55:49 +0200 Subject: [PATCH 4/4] adapt docs --- docs/controller/managedcontrolplane.md | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/docs/controller/managedcontrolplane.md b/docs/controller/managedcontrolplane.md index 9b427c0e..c7bf6b2e 100644 --- a/docs/controller/managedcontrolplane.md +++ b/docs/controller/managedcontrolplane.md @@ -12,11 +12,9 @@ managedControlPlane: mcpClusterPurpose: mcp # defaults to 'mcp' reconcileMCPEveryXDays: 7 # defaults to 0 defaultOIDCProvider: - name: default # must be 'default' or omitted for the default oidc provider + name: openmcp # defaults to 'openmcp' when omitted issuer: https://oidc.example.com clientID: my-client-id - usernamePrefix: "my-user:" - groupsPrefix: "my-group:" extraScopes: - foo ``` @@ -45,8 +43,6 @@ spec: - name: my-oidc-provider issuer: https://oidc.example.com clientID: my-client-id - usernamePrefix: "my-user:" - groupsPrefix: "my-group:" extraScopes: - foo roleBindings: