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", "")
})
-
})