diff --git a/api/bases/keystone.openstack.org_keystoneapis.yaml b/api/bases/keystone.openstack.org_keystoneapis.yaml index 8611326c..1b34f8dd 100644 --- a/api/bases/keystone.openstack.org_keystoneapis.yaml +++ b/api/bases/keystone.openstack.org_keystoneapis.yaml @@ -134,6 +134,59 @@ spec: description: NodeSelector to target subset of worker nodes running this service type: object + oidcFederation: + description: KeystoneFederationSpec to provide the configuration values + for OIDC Federation + properties: + keystoneFederationIdentityProviderName: + default: "" + description: KeystoneFederationIdentityProviderName + type: string + oidcCacheType: + default: memcache + description: OIDCCacheType + type: string + oidcClaimDelimiter: + default: ; + description: OIDCClaimDelimiter + type: string + oidcClaimPrefix: + default: OIDC- + description: OIDCClaimPrefix + type: string + oidcClientID: + default: "" + description: OIDCClientID + type: string + oidcIntrospectionEndpoint: + default: "" + description: OIDCIntrospectionEndpoint + type: string + oidcPassClaimsAs: + default: both + description: OIDCPassClaimsAs + type: string + oidcPassUserInfoAs: + default: claims + description: OIDCPassUserInfoAs + type: string + oidcProviderMetadataURL: + default: "" + description: OIDCProviderMetadataURL + type: string + oidcResponseType: + default: id_token + description: OIDCResponseType + type: string + oidcScope: + default: openid email profile + description: OIDCScope + type: string + remoteIDAttribute: + default: HTTP_OIDC_ISS + description: RemoteIDAttribute + type: string + type: object override: description: Override, provides the ability to override the generated manifest of several child resources. @@ -295,14 +348,27 @@ spec: passwordSelectors: default: admin: AdminPassword - description: PasswordSelectors - Selectors to identify the AdminUser - password from the Secret + keystoneOIDCClientSecret: KeystoneOIDCClientSecret + keystoneOIDCCryptoPassphrase: KeystoneOIDCCryptoPassphrase + description: PasswordSelectors - Selectors to identify the AdminUser, + KeystoneOIDCClient, and KeystoneOIDCCryptoPassphrase passwords from + the Secret properties: admin: default: AdminPassword description: Admin - Selector to get the keystone Admin password from the Secret type: string + keystoneOIDCClientSecret: + default: KeystoneOIDCClientSecret + description: OIDCClientSecret - Selector to get the IdP client + secret from the Secret + type: string + keystoneOIDCCryptoPassphrase: + default: KeystoneOIDCCryptoPassphrase + description: OIDCCryptoPassphrase - Selector to get the OIDC crypto + passphrase from the Secret + type: string type: object preserveJobs: default: false diff --git a/api/v1beta1/keystoneapi_types.go b/api/v1beta1/keystoneapi_types.go index a0d86aff..3f274887 100644 --- a/api/v1beta1/keystoneapi_types.go +++ b/api/v1beta1/keystoneapi_types.go @@ -132,8 +132,8 @@ type KeystoneAPISpecCore struct { FernetMaxActiveKeys *int32 `json:"fernetMaxActiveKeys"` // +kubebuilder:validation:Optional - // +kubebuilder:default={admin: AdminPassword} - // PasswordSelectors - Selectors to identify the AdminUser password from the Secret + // +kubebuilder:default={admin: AdminPassword, keystoneOIDCClientSecret: KeystoneOIDCClientSecret, keystoneOIDCCryptoPassphrase: KeystoneOIDCCryptoPassphrase} + // PasswordSelectors - Selectors to identify the AdminUser, KeystoneOIDCClient, and KeystoneOIDCCryptoPassphrase passwords from the Secret PasswordSelectors PasswordSelector `json:"passwordSelectors"` // +kubebuilder:validation:Optional @@ -184,6 +184,10 @@ type KeystoneAPISpecCore struct { // +operator-sdk:csv:customresourcedefinitions:type=spec // TLS - Parameters related to the TLS TLS tls.API `json:"tls,omitempty"` + + // +kubebuilder:validation:Optional + // +OIDCFederation - parameters to configure keystone for OIDC federation + OIDCFederation *KeystoneFederationSpec `json:"oidcFederation,omitempty"` } // APIOverrideSpec to override the generated manifest of several child resources. @@ -199,6 +203,79 @@ type PasswordSelector struct { // +kubebuilder:default="AdminPassword" // Admin - Selector to get the keystone Admin password from the Secret Admin string `json:"admin"` + + // +kubebuilder:validation:Optional + // +kubebuilder:default="KeystoneOIDCClientSecret" + // OIDCClientSecret - Selector to get the IdP client secret from the Secret + KeystoneOIDCClientSecret string `json:"keystoneOIDCClientSecret"` + + // +kubebuilder:validation:Optional + // +kubebuilder:default="KeystoneOIDCCryptoPassphrase" + // OIDCCryptoPassphrase - Selector to get the OIDC crypto passphrase from the Secret + KeystoneOIDCCryptoPassphrase string `json:"keystoneOIDCCryptoPassphrase"` +} + +// KeystoneFederationSpec to provide the configuration values for OIDC Federation +type KeystoneFederationSpec struct { + // +kubebuilder:validation:Optional + // +kubebuilder:default="OIDC-" + // OIDCClaimPrefix + OIDCClaimPrefix string `json:"oidcClaimPrefix"` + + // +kubebuilder:validation:Optional + // +kubebuilder:default="id_token" + // OIDCResponseType + OIDCResponseType string `json:"oidcResponseType"` + + // +kubebuilder:validation:Optional + // +kubebuilder:default="openid email profile" + // OIDCScope + OIDCScope string `json:"oidcScope"` + + // +kubebuilder:validation:Optional + // +kubebuilder:default="" + // OIDCProviderMetadataURL + OIDCProviderMetadataURL string `json:"oidcProviderMetadataURL"` + + // +kubebuilder:validation:Optional + // +kubebuilder:default="" + // OIDCIntrospectionEndpoint + OIDCIntrospectionEndpoint string `json:"oidcIntrospectionEndpoint"` + + // +kubebuilder:validation:Optional + // +kubebuilder:default="" + // OIDCClientID + OIDCClientID string `json:"oidcClientID"` + + // +kubebuilder:validation:Optional + // +kubebuilder:default=";" + // OIDCClaimDelimiter + OIDCClaimDelimiter string `json:"oidcClaimDelimiter"` + + // +kubebuilder:validation:Optional + // +kubebuilder:default="claims" + // OIDCPassUserInfoAs + OIDCPassUserInfoAs string `json:"oidcPassUserInfoAs"` + + // +kubebuilder:validation:Optional + // +kubebuilder:default="both" + // OIDCPassClaimsAs + OIDCPassClaimsAs string `json:"oidcPassClaimsAs"` + + // +kubebuilder:validation:Optional + // +kubebuilder:default="memcache" + // OIDCCacheType + OIDCCacheType string `json:"oidcCacheType"` + + // +kubebuilder:validation:Optional + // +kubebuilder:default="HTTP_OIDC_ISS" + // RemoteIDAttribute + RemoteIDAttribute string `json:"remoteIDAttribute"` + + // +kubebuilder:validation:Optional + // +kubebuilder:default="" + // KeystoneFederationIdentityProviderName + KeystoneFederationIdentityProviderName string `json:"keystoneFederationIdentityProviderName"` } // HttpdCustomization - customize the httpd service @@ -233,7 +310,7 @@ type KeystoneAPIStatus struct { // TransportURLSecret - Secret containing RabbitMQ transportURL TransportURLSecret string `json:"transportURLSecret,omitempty"` - //ObservedGeneration - the most recent generation observed for this service. If the observed generation is less than the spec generation, then the controller has not processed the latest changes. + // ObservedGeneration - the most recent generation observed for this service. If the observed generation is less than the spec generation, then the controller has not processed the latest changes. ObservedGeneration int64 `json:"observedGeneration,omitempty"` } diff --git a/api/v1beta1/zz_generated.deepcopy.go b/api/v1beta1/zz_generated.deepcopy.go index 6f1a04b2..205feff2 100644 --- a/api/v1beta1/zz_generated.deepcopy.go +++ b/api/v1beta1/zz_generated.deepcopy.go @@ -204,6 +204,11 @@ func (in *KeystoneAPISpecCore) DeepCopyInto(out *KeystoneAPISpecCore) { } in.Override.DeepCopyInto(&out.Override) in.TLS.DeepCopyInto(&out.TLS) + if in.OIDCFederation != nil { + in, out := &in.OIDCFederation, &out.OIDCFederation + *out = new(KeystoneFederationSpec) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KeystoneAPISpecCore. @@ -412,6 +417,21 @@ func (in *KeystoneEndpointStatus) DeepCopy() *KeystoneEndpointStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *KeystoneFederationSpec) DeepCopyInto(out *KeystoneFederationSpec) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KeystoneFederationSpec. +func (in *KeystoneFederationSpec) DeepCopy() *KeystoneFederationSpec { + if in == nil { + return nil + } + out := new(KeystoneFederationSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *KeystoneService) DeepCopyInto(out *KeystoneService) { *out = *in diff --git a/config/crd/bases/keystone.openstack.org_keystoneapis.yaml b/config/crd/bases/keystone.openstack.org_keystoneapis.yaml index 8611326c..1b34f8dd 100644 --- a/config/crd/bases/keystone.openstack.org_keystoneapis.yaml +++ b/config/crd/bases/keystone.openstack.org_keystoneapis.yaml @@ -134,6 +134,59 @@ spec: description: NodeSelector to target subset of worker nodes running this service type: object + oidcFederation: + description: KeystoneFederationSpec to provide the configuration values + for OIDC Federation + properties: + keystoneFederationIdentityProviderName: + default: "" + description: KeystoneFederationIdentityProviderName + type: string + oidcCacheType: + default: memcache + description: OIDCCacheType + type: string + oidcClaimDelimiter: + default: ; + description: OIDCClaimDelimiter + type: string + oidcClaimPrefix: + default: OIDC- + description: OIDCClaimPrefix + type: string + oidcClientID: + default: "" + description: OIDCClientID + type: string + oidcIntrospectionEndpoint: + default: "" + description: OIDCIntrospectionEndpoint + type: string + oidcPassClaimsAs: + default: both + description: OIDCPassClaimsAs + type: string + oidcPassUserInfoAs: + default: claims + description: OIDCPassUserInfoAs + type: string + oidcProviderMetadataURL: + default: "" + description: OIDCProviderMetadataURL + type: string + oidcResponseType: + default: id_token + description: OIDCResponseType + type: string + oidcScope: + default: openid email profile + description: OIDCScope + type: string + remoteIDAttribute: + default: HTTP_OIDC_ISS + description: RemoteIDAttribute + type: string + type: object override: description: Override, provides the ability to override the generated manifest of several child resources. @@ -295,14 +348,27 @@ spec: passwordSelectors: default: admin: AdminPassword - description: PasswordSelectors - Selectors to identify the AdminUser - password from the Secret + keystoneOIDCClientSecret: KeystoneOIDCClientSecret + keystoneOIDCCryptoPassphrase: KeystoneOIDCCryptoPassphrase + description: PasswordSelectors - Selectors to identify the AdminUser, + KeystoneOIDCClient, and KeystoneOIDCCryptoPassphrase passwords from + the Secret properties: admin: default: AdminPassword description: Admin - Selector to get the keystone Admin password from the Secret type: string + keystoneOIDCClientSecret: + default: KeystoneOIDCClientSecret + description: OIDCClientSecret - Selector to get the IdP client + secret from the Secret + type: string + keystoneOIDCCryptoPassphrase: + default: KeystoneOIDCCryptoPassphrase + description: OIDCCryptoPassphrase - Selector to get the OIDC crypto + passphrase from the Secret + type: string type: object preserveJobs: default: false diff --git a/config/samples/keystone_v1beta1_keystoneapi_tls_federation.yaml b/config/samples/keystone_v1beta1_keystoneapi_tls_federation.yaml new file mode 100644 index 00000000..460bb411 --- /dev/null +++ b/config/samples/keystone_v1beta1_keystoneapi_tls_federation.yaml @@ -0,0 +1,43 @@ +apiVersion: keystone.openstack.org/v1beta1 +kind: KeystoneAPI +metadata: + name: keystone +spec: + adminProject: admin + adminUser: admin + customServiceConfig: | + [DEFAULT] + debug = true + databaseInstance: openstack + databaseAccount: keystone + preserveJobs: false + region: regionOne + secret: osp-secret + resources: + requests: + memory: "500Mi" + cpu: "1.0" + tls: + api: + # secret holding tls.crt and tls.key for the APIs internal k8s service + internal: + secretName: cert-keystone-internal-svc + # secret holding tls.crt and tls.key for the APIs public k8s service + public: + secretName: cert-keystone-public-svc + # secret holding the tls-ca-bundle.pem to be used as a deploymend env CA bundle + caBundleSecretName: combined-ca-bundle + oidcFederation: + keystoneFederationIdentityProviderName: my_federation_provider_name + oidcCacheType: memcache + oidcClaimDelimiter: ; + oidcClaimPrefix: OIDC- + oidcClientID: my_federation_client_id + oidcIntrospectionEndpoint: my_federation_introspection_endpoint + oidcMemCacheServers: "" + oidcPassClaimsAs: both + oidcPassUserInfoAs: claims + oidcProviderMetadataURL: my_federation_provider_metadata_url + oidcResponseType: id_token + oidcScope: openid email profile + remoteIDAttribute: HTTP_OIDC_ISS diff --git a/controllers/keystoneapi_controller.go b/controllers/keystoneapi_controller.go index d788affc..6484e175 100644 --- a/controllers/keystoneapi_controller.go +++ b/controllers/keystoneapi_controller.go @@ -716,7 +716,7 @@ func (r *KeystoneAPIReconciler) reconcileNormal( // NOTE: VerifySecret handles the "not found" error and returns RequeueAfter ctrl.Result if so, so we don't // need to check the error type here // - hash, result, err := oko_secret.VerifySecret(ctx, types.NamespacedName{Name: instance.Spec.Secret, Namespace: instance.Namespace}, []string{"AdminPassword"}, helper.GetClient(), time.Second*10) + hash, result, err := oko_secret.VerifySecret(ctx, types.NamespacedName{Name: instance.Spec.Secret, Namespace: instance.Namespace}, []string{"AdminPassword", "KeystoneOIDCClientSecret", "KeystoneOIDCCryptoPassphrase"}, helper.GetClient(), time.Second*10) if err != nil { instance.Status.Conditions.Set(condition.FalseCondition( condition.InputReadyCondition, @@ -1185,6 +1185,18 @@ func (r *KeystoneAPIReconciler) generateServiceConfigMaps( databaseAccount := db.GetAccount() dbSecret := db.GetSecret() + var endpointPublic string + var federationRemoteIDAttribute string + enableFederation := false + if instance.Spec.OIDCFederation != nil { + enableFederation = true + endpointPublic, err = instance.GetEndpoint(endpoint.EndpointPublic) + if err != nil { + return err + } + federationRemoteIDAttribute = instance.Spec.OIDCFederation.RemoteIDAttribute + } + templateParameters := map[string]interface{}{ "memcachedServers": mc.GetMemcachedServerListString(), "memcachedServersWithInet": mc.GetMemcachedServerListWithInetString(), @@ -1196,9 +1208,36 @@ func (r *KeystoneAPIReconciler) generateServiceConfigMaps( instance.Status.DatabaseHostname, keystone.DatabaseName, ), - "ProcessNumber": instance.Spec.HttpdCustomization.ProcessNumber, - "enableSecureRBAC": instance.Spec.EnableSecureRBAC, - "fernetMaxActiveKeys": instance.Spec.FernetMaxActiveKeys, + "enableSecureRBAC": instance.Spec.EnableSecureRBAC, + "ProcessNumber": instance.Spec.HttpdCustomization.ProcessNumber, + "enableFederation": enableFederation, + "federationTrustedDashboard": fmt.Sprintf("%s/dashboard/auth/websso/", endpointPublic), + "federationRemoteIDAttribute": federationRemoteIDAttribute, + "fernetMaxActiveKeys": instance.Spec.FernetMaxActiveKeys, + } + + var OIDCClientSecret string + var OIDCCryptoPassphrase string + + if enableFederation { + ospSecret, _, err := oko_secret.GetSecret( + ctx, + h, + instance.Spec.Secret, + instance.Namespace) + if err != nil { + return err + } + + OIDCClientSecret = string(ospSecret.Data[instance.Spec.PasswordSelectors.KeystoneOIDCClientSecret]) + if OIDCClientSecret == "" { + return fmt.Errorf("OIDCClientSecret cannot be empty, no password found for selector %s in secret %s", ospSecret.Name, instance.Spec.PasswordSelectors.KeystoneOIDCClientSecret) + } + + OIDCCryptoPassphrase = string(ospSecret.Data[instance.Spec.PasswordSelectors.KeystoneOIDCCryptoPassphrase]) + if OIDCCryptoPassphrase == "" { + return fmt.Errorf("OIDCCryptoPassphrase cannot be empty, no password found for selector %s in secret %s", ospSecret.Name, instance.Spec.PasswordSelectors.KeystoneOIDCCryptoPassphrase) + } } // create httpd vhost template parameters @@ -1207,11 +1246,30 @@ func (r *KeystoneAPIReconciler) generateServiceConfigMaps( endptConfig := map[string]interface{}{} endptConfig["ServerName"] = fmt.Sprintf("%s-%s.%s.svc", instance.Name, endpt.String(), instance.Namespace) endptConfig["TLS"] = false // default TLS to false, and set it bellow to true if enabled + endptConfig["EnableFederation"] = enableFederation + if enableFederation { + endptConfig["OIDCClaimPrefix"] = instance.Spec.OIDCFederation.OIDCClaimPrefix + endptConfig["OIDCResponseType"] = instance.Spec.OIDCFederation.OIDCResponseType + endptConfig["OIDCScope"] = instance.Spec.OIDCFederation.OIDCScope + endptConfig["OIDCProviderMetadataURL"] = instance.Spec.OIDCFederation.OIDCProviderMetadataURL + endptConfig["OIDCIntrospectionEndpoint"] = instance.Spec.OIDCFederation.OIDCIntrospectionEndpoint + endptConfig["OIDCClientID"] = instance.Spec.OIDCFederation.OIDCClientID + endptConfig["OIDCClientSecret"] = OIDCClientSecret + endptConfig["OIDCCryptoPassphrase"] = OIDCCryptoPassphrase + endptConfig["OIDCPassUserInfoAs"] = instance.Spec.OIDCFederation.OIDCPassUserInfoAs + endptConfig["OIDCPassClaimsAs"] = instance.Spec.OIDCFederation.OIDCPassClaimsAs + endptConfig["OIDCClaimDelimiter"] = instance.Spec.OIDCFederation.OIDCClaimDelimiter + endptConfig["OIDCCacheType"] = instance.Spec.OIDCFederation.OIDCCacheType + endptConfig["OIDCMemCacheServers"] = mc.GetMemcachedServerListString() + endptConfig["KeystoneFederationIdentityProviderName"] = instance.Spec.OIDCFederation.KeystoneFederationIdentityProviderName + endptConfig["KeystoneEndpoint"], _ = instance.GetEndpoint(endpoint.EndpointPublic) + } if instance.Spec.TLS.API.Enabled(endpt) { endptConfig["TLS"] = true endptConfig["SSLCertificateFile"] = fmt.Sprintf("/etc/pki/tls/certs/%s.crt", endpt.String()) endptConfig["SSLCertificateKeyFile"] = fmt.Sprintf("/etc/pki/tls/private/%s.key", endpt.String()) } + httpdVhostConfig[endpt.String()] = endptConfig } templateParameters["VHosts"] = httpdVhostConfig @@ -1366,7 +1424,8 @@ func (r *KeystoneAPIReconciler) ensureFernetKeys( } annotations := map[string]string{ - fernetAnnotation: now.Format(time.RFC3339)} + fernetAnnotation: now.Format(time.RFC3339), + } tmpl := []util.Template{ { @@ -1506,7 +1565,6 @@ func (r *KeystoneAPIReconciler) ensureDB( h *helper.Helper, instance *keystonev1.KeystoneAPI, ) (*mariadbv1.Database, ctrl.Result, error) { - // ensure MariaDBAccount exists. This account record may be created by // openstack-operator or the cloud operator up front without a specific // MariaDBDatabase configured yet. Otherwise, a MariaDBAccount CR is @@ -1517,7 +1575,6 @@ func (r *KeystoneAPIReconciler) ensureDB( ctx, h, instance.Spec.DatabaseAccount, instance.Namespace, false, keystone.DatabaseUsernamePrefix, ) - if err != nil { instance.Status.Conditions.Set(condition.FalseCondition( mariadbv1.MariaDBAccountReadyCondition, @@ -1545,7 +1602,6 @@ func (r *KeystoneAPIReconciler) ensureDB( // create or patch the DB ctrlResult, err := db.CreateOrPatchAll(ctx, h) - if err != nil { instance.Status.Conditions.Set(condition.FalseCondition( condition.DBReadyCondition, diff --git a/templates/keystoneapi/config/httpd.conf b/templates/keystoneapi/config/httpd.conf index 641b6ddf..4d79ab15 100644 --- a/templates/keystoneapi/config/httpd.conf +++ b/templates/keystoneapi/config/httpd.conf @@ -57,5 +57,47 @@ CustomLog /dev/stdout proxy env=forwarded WSGIProcessGroup {{ $endpt }} WSGIScriptAlias / "/usr/bin/keystone-wsgi-public" WSGIPassAuthorization On + +{{ if $vhost.EnableFederation }} + OIDCClaimPrefix "{{ $vhost.OIDCClaimPrefix }}" + OIDCResponseType "{{ $vhost.OIDCResponseType }}" + OIDCScope "{{ $vhost.OIDCScope }}" + OIDCProviderMetadataURL "{{ $vhost.OIDCProviderMetadataURL }}" + OIDCClientID "{{ $vhost.OIDCClientID }}" + OIDCClientSecret "{{ $vhost.OIDCClientSecret }}" + OIDCCryptoPassphrase "{{ $vhost.OIDCCryptoPassphrase }}" + OIDCClaimDelimiter "{{ $vhost.OIDCClaimDelimiter }}" + OIDCPassUserInfoAs "{{ $vhost.OIDCPassUserInfoAs }}" + OIDCPassClaimsAs "{{ $vhost.OIDCPassClaimsAs }}" + + OIDCCacheType "{{ $vhost.OIDCCacheType }}" + OIDCMemCacheServers "{{ $vhost.OIDCMemCacheServers }}" + + # The following directives are necessary to support websso from Horizon + # (Per https://docs.openstack.org/keystone/pike/advanced-topics/federation/websso.html) + OIDCRedirectURI "{{ $vhost.KeystoneEndpoint }}/v3/auth/OS-FEDERATION/identity_providers/{{ $vhost.KeystoneFederationIdentityProviderName }}/protocols/openid/websso" + OIDCRedirectURI "{{ $vhost.KeystoneEndpoint }}/v3/auth/OS-FEDERATION/websso/openid" + + + AuthType "openid-connect" + Require valid-user + + + + AuthType "openid-connect" + Require valid-user + + + OIDCOAuthClientID "{{ $vhost.OIDCClientID }}" + OIDCOAuthClientSecret "{{ $vhost.OIDCClientSecret }}" + OIDCOAuthIntrospectionEndpoint "{{ $vhost.OIDCIntrospectionEndpoint }}" + + + AuthType oauth20 + Require valid-user + + +{{- end }} + {{ end }} diff --git a/templates/keystoneapi/config/keystone.conf b/templates/keystoneapi/config/keystone.conf index f2a2165b..d5f5a1cc 100644 --- a/templates/keystoneapi/config/keystone.conf +++ b/templates/keystoneapi/config/keystone.conf @@ -12,6 +12,17 @@ memcache_servers={{ .memcachedServersWithInet }} enabled=true tls_enabled={{ .memcachedTLS }} +{{if .enableFederation }} +[federation] +trusted_dashboard={{ .federationTrustedDashboard }} + +[openid] +remote_id_attribute={{ .federationRemoteIDAttribute }} + +[auth] +methods = password,token,oauth1,mapped,application_credential,openid +{{ end }} + [database] max_retries=-1 db_max_retries=-1 diff --git a/tests/functional/base_test.go b/tests/functional/base_test.go index f318d672..fa2fcda3 100644 --- a/tests/functional/base_test.go +++ b/tests/functional/base_test.go @@ -65,7 +65,6 @@ func GetTLSKeystoneAPISpec() map[string]interface{} { } func CreateKeystoneAPI(name types.NamespacedName, spec map[string]interface{}) client.Object { - raw := map[string]interface{}{ "apiVersion": "keystone.openstack.org/v1beta1", "kind": "KeystoneAPI", @@ -90,7 +89,9 @@ func CreateKeystoneAPISecret(namespace string, name string) *corev1.Secret { return th.CreateSecret( types.NamespacedName{Namespace: namespace, Name: name}, map[string][]byte{ - "AdminPassword": []byte("12345678"), + "AdminPassword": []byte("12345678"), + "KeystoneOIDCClientSecret": []byte("secret123"), + "KeystoneOIDCCryptoPassphrase": []byte("openstack"), }, ) } diff --git a/tests/functional/keystoneapi_controller_test.go b/tests/functional/keystoneapi_controller_test.go index c041641d..3e5a1c38 100644 --- a/tests/functional/keystoneapi_controller_test.go +++ b/tests/functional/keystoneapi_controller_test.go @@ -41,11 +41,11 @@ import ( ) var _ = Describe("Keystone controller", func() { - var keystoneAPIName types.NamespacedName var keystoneAccountName types.NamespacedName var keystoneDatabaseName types.NamespacedName var keystoneAPIConfigDataName types.NamespacedName + var keystoneEndpointName types.NamespacedName var dbSyncJobName types.NamespacedName var bootstrapJobName types.NamespacedName var deploymentName types.NamespacedName @@ -56,7 +56,6 @@ var _ = Describe("Keystone controller", func() { var cronJobName types.NamespacedName BeforeEach(func() { - keystoneAPIName = types.NamespacedName{ Name: "keystone", Namespace: namespace, @@ -85,6 +84,10 @@ var _ = Describe("Keystone controller", func() { Name: "keystone-config-data", Namespace: namespace, } + keystoneEndpointName = types.NamespacedName{ + Name: "public", + Namespace: namespace, + } caBundleSecretName = types.NamespacedName{ Name: CABundleSecretName, Namespace: namespace, @@ -424,7 +427,6 @@ var _ = Describe("Keystone controller", func() { Namespace: namespace, }) }) - }) When("DB sync is completed", func() { @@ -965,7 +967,6 @@ var _ = Describe("Keystone controller", func() { configData = string(scrt.Data["my.cnf"]) Expect(configData).To( ContainSubstring("[client]\nssl-ca=/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem\nssl=1")) - }) It("it creates deployment with CA and service certs mounted", func() { @@ -1116,6 +1117,117 @@ var _ = Describe("Keystone controller", func() { }) }) + When("A TLS KeystoneAPI is created with an OIDC Federation configuration", func() { + BeforeEach(func() { + spec := GetTLSKeystoneAPISpec() + /* serviceOverride := map[string]interface{}{} + serviceOverride["public"] = map[string]interface{}{ + "endpointURL": "https://keystone-openstack.apps-crc.testing", + } + spec["override"] = map[string]interface{}{ + "service": serviceOverride, + } */ + spec["oidcFederation"] = map[string]interface{}{ + "keystoneFederationIdentityProviderName": "myidp", + "oidcCacheType": "memcache", + "oidcClaimDelimiter": ";", + "oidcClaimPrefix": "OIDC-", + "oidcClientID": "client123", + "oidcIntrospectionEndpoint": "https://idp.example.com/token/introspect", + "oidcPassClaimsAs": "both", + "oidcPassUserInfoAs": "claims", + "oidcProviderMetadataURL": "https://idp.example.com/.well-known/openid-configuration", + "oidcResponseType": "id_token", + "oidcScope": "openid email profile", + "remoteIDAttribute": "HTTP_OIDC_ISS", + } + + DeferCleanup(k8sClient.Delete, ctx, th.CreateCABundleSecret(caBundleSecretName)) + DeferCleanup(k8sClient.Delete, ctx, th.CreateCertSecret(internalCertSecretName)) + DeferCleanup(k8sClient.Delete, ctx, th.CreateCertSecret(publicCertSecretName)) + DeferCleanup(th.DeleteInstance, CreateKeystoneAPI(keystoneAPIName, spec)) + DeferCleanup( + k8sClient.Delete, ctx, CreateKeystoneMessageBusSecret(namespace, "rabbitmq-secret")) + DeferCleanup( + k8sClient.Delete, ctx, CreateKeystoneAPISecret(namespace, SecretName)) + DeferCleanup(infra.DeleteMemcached, infra.CreateMemcached(namespace, "memcached", memcachedSpec)) + DeferCleanup( + mariadb.DeleteDBService, + mariadb.CreateDBService( + namespace, + GetKeystoneAPI(keystoneAPIName).Spec.DatabaseInstance, + corev1.ServiceSpec{ + Ports: []corev1.ServicePort{{Port: 3306}}, + }, + ), + ) + mariadb.SimulateMariaDBAccountCompleted(keystoneAccountName) + mariadb.SimulateMariaDBDatabaseCompleted(keystoneDatabaseName) + infra.SimulateTransportURLReady(types.NamespacedName{ + Name: fmt.Sprintf("%s-keystone-transport", keystoneAPIName.Name), + Namespace: namespace, + }) + infra.SimulateMemcachedReady(types.NamespacedName{ + Name: "memcached", + Namespace: namespace, + }) + th.SimulateJobSuccess(dbSyncJobName) + th.SimulateJobSuccess(bootstrapJobName) + keystone.SimulateKeystoneEndpointReady(keystoneEndpointName) + th.SimulateDeploymentReplicaReady(deploymentName) + }) + + /* It("registers LoadBalancer services keystone endpoints", func() { + instance := keystone.GetKeystoneAPI(keystoneAPIName) + Expect(instance).NotTo(BeNil()) + Expect(instance.Status.APIEndpoints).To(HaveKeyWithValue("public", "https://keystone-openstack.apps-crc.testing")) + Expect(instance.Status.APIEndpoints).To(HaveKeyWithValue("internal", "https://keystone-internal."+keystoneAPIName.Namespace+".svc:5000")) + + th.ExpectCondition( + keystoneAPIName, + ConditionGetterFunc(KeystoneConditionGetter), + condition.ReadyCondition, + corev1.ConditionTrue, + ) + }) */ + + It("should configure OIDC in httpd.conf and keystone.conf", func() { + scrt := th.GetSecret(keystoneAPIConfigDataName) + Expect(scrt).ShouldNot(BeNil()) + + // Verify httpd.conf OIDC configuration + httpdConf := string(scrt.Data["httpd.conf"]) + Expect(httpdConf).Should(ContainSubstring("OIDCClaimPrefix \"OIDC-\"")) + Expect(httpdConf).Should(ContainSubstring("OIDCResponseType \"id_token\"")) + Expect(httpdConf).Should(ContainSubstring("OIDCScope \"openid email profile\"")) + Expect(httpdConf).Should(ContainSubstring("OIDCProviderMetadataURL https://idp.example.com/.well-known/openid-configuration")) + Expect(httpdConf).Should(ContainSubstring("OIDCClientID \"client123\"")) + Expect(httpdConf).Should(ContainSubstring("OIDCClientSecret \"secret123\"")) + Expect(httpdConf).Should(ContainSubstring("OIDCCryptoPassphrase \"openstack\"")) + Expect(httpdConf).Should(ContainSubstring("OIDCCClaimDelimiter \";\"")) + Expect(httpdConf).Should(ContainSubstring("OIDCCPassUserInfoAs \"claims\"")) + Expect(httpdConf).Should(ContainSubstring("OIDCCPassClaimsAs \"both\"")) + Expect(httpdConf).Should(ContainSubstring("OIDCCacheType \"memcache\"")) + Expect(httpdConf).Should(ContainSubstring("OIDCRedirectURI \"https://keystone-openstack.apps-crc.testing/v3/auth/OS-FEDERATION/identity_providers/myidp/protocols/openid/websso\"")) + Expect(httpdConf).Should(ContainSubstring("OIDCRedirectURI \"https://keystone-openstack.apps-crc.testing/v3/auth/OS-FEDERATION/websso/openid\"")) + Expect(httpdConf).Should(ContainSubstring("LocationMatch \"/v3/auth/OS-FEDERATION/websso/openid\"")) + Expect(httpdConf).Should(ContainSubstring("LocationMatch \"/v3/auth/OS-FEDERATION/identity_providers/myidp/protocols/openid/websso\"")) + Expect(httpdConf).Should(ContainSubstring("OIDCAuthClientID \"client123\"")) + Expect(httpdConf).Should(ContainSubstring("OIDCAuthClientSecret \"secret123\"")) + Expect(httpdConf).Should(ContainSubstring("OIDCAuthIntrospectionEndpoint \"https://idp.example.com/token/introspect\"")) + Expect(httpdConf).Should(ContainSubstring("Location ~ \"/v3/auth/OS-FEDERATION/identity_providers/myidp/protocols/openid/auth\"")) + + // Verify keystone.conf federation configuration + keystoneConf := string(scrt.Data["keystone.conf"]) + Expect(keystoneConf).Should(ContainSubstring("[federation]")) + Expect(keystoneConf).Should(ContainSubstring("trusted_dashboard=https://keystone-openstack.apps-crc.testing/dashboard/auth/websso/")) + Expect(keystoneConf).Should(ContainSubstring("[openid]")) + Expect(keystoneConf).Should(ContainSubstring("remote_id_attribute = HTTP_OIDC_ISS")) + Expect(keystoneConf).Should(ContainSubstring("[auth]")) + Expect(keystoneConf).Should(ContainSubstring("methods = password,token,oauth1,mapped,application_credential,openid")) + }) + }) + When("When FernetMaxActiveKeys is created with a number lower than 3", func() { It("should fail", func() { err := InterceptGomegaFailure( @@ -1434,7 +1546,6 @@ var _ = Describe("Keystone controller", func() { } } }, timeout, interval).Should(Succeed()) - }) }) @@ -1584,7 +1695,6 @@ var _ = Describe("Keystone controller", func() { // needs to make it all the way to the end where the mariadb finalizers // are removed from unused accounts since that's part of what we are testing SetupCR: func(accountName types.NamespacedName) { - spec := GetDefaultKeystoneAPISpec() spec["databaseAccount"] = accountName.Name @@ -1627,17 +1737,14 @@ var _ = Describe("Keystone controller", func() { condition.DeploymentReadyCondition, corev1.ConditionTrue, ) - }, // Change the account name in the service to a new name UpdateAccount: func(newAccountName types.NamespacedName) { - Eventually(func(g Gomega) { keystoneapi := GetKeystoneAPI(keystoneAPIName) keystoneapi.Spec.DatabaseAccount = newAccountName.Name g.Expect(th.K8sClient.Update(ctx, keystoneapi)).Should(Succeed()) }, timeout, interval).Should(Succeed()) - }, // delete the keystone instance to exercise finalizer removal DeleteCR: func() { @@ -1656,12 +1763,10 @@ var _ = Describe("Keystone controller", func() { ContainSubstring(fmt.Sprintf("connection=mysql+pymysql://%s:%s@hostname-for-openstack.%s.svc/keystone?read_default_file=/etc/my.cnf", username, password, namespace))) }, timeout, interval).Should(Succeed()) - }) mariadbSuite.RunConfigHashSuite(func() string { deployment := th.GetDeployment(deploymentName) return GetEnvVarValue(deployment.Spec.Template.Spec.Containers[0].Env, "CONFIG_HASH", "") }) - })