feat: Add keycloak (#510)

* feat: Add keycloak

* fix lint error

* Deploy keycloak in default

* Fix role names in deps

* Remove dynamic key in ansible variable dic

* Use custom wait logic because postgresql CRD doesn't have condition status

* Use production mode for keycloak

* Wait until zalando operator ready

* Set admin password explicitly

* Fix ingress config

* Create grafana client in keycloak and enable oauth in grafana

* Use PXC instead of zalando postgres

* Set default value for atmosphere_keycloak_enabled

* Split the mysql queries into multiple parts

* Solve convo

* Use ansible module for keycloak realm

* Concatenate realm arrays

* Tune keycloak config to work with mysql vendor and fix keycloak db user's host config

* Add notes about mysql vendor support in keycloak

* remove default client scope setting

it requires community.general collection upgrade to 4.7.0.
But there are other collections in the deps list which use lower versions.

Need to bump its version at some time surely.

* Tuen grafana oauth config

* Fix keycloak client auth mode

* Use a variable for keycloak database name

* Fix lint error

* Remove unused var from doc

* Manage pxc strict mode in ansible

* revert the changes out of the scope

* ignore changes during realm creation

* Resolve commnents

* Fix default values

* Use official keycloak image instead of bitnami

* Set proxy mode using KC env var

* securely reference oauth secrets in grafana.ini

* Remove implicit octal value in helm values

* Support id token in keycloak clients for rbac and add ingress role

* Update grafana role map attribute

* Use j2 template

https://stackoverflow.com/questions/63961938/ansible-variable-conversion-to-int-is-ignored

* Create client roles

* Fix yaml lint error

* Add keycloak health check in consumer roles

* fix: use correct annotations

* chore: add keystone-keycloak-backend to keystone

* fix: make openstack_helm_endpoints work cleaner

* chore: clean up unecessary docs

* fix: lock down grafana to allow users with roles only

* feat: integrate keystone with keycloak

* chore: added horizon auth via keycloak

* chore: add slo for grafana

* chore: bump to 0.1.5

* Fix yaml lint error

* Fix client role creation

* chore(kube-prometheus-stack): update to latest

* fix: KubeJobFailed should be SEV-3

* chore: refactor softnet alerts

* chore(monitoring): migrate to using jsonnet

* chore: refactor alerts

* chore: major monitoring refactor

* fix: solve alerts

* fix: apiserver selector

* more cleanups

* switch from SEV- to P

* fix: improve port binding alerts

* fix admin state alert for neutron

* map some more alerts

* drop uuid

* Revert "drop uuid"

This reverts commit ad0f05d0e7564759e8259c2cc53c2e2f5c73e1b8.

* fix: drop recording rules

* switch alertmanager to jsonnet

* fix: idempotence for monitoring

* chore: fix linters

* chore: lower ceph osd timeouts to p4

* chore: refactor to using new jsonnet

* chore: use vendor path

* chore: fix mixin for alertmanager

* core: fix selector

* ci: add initial keycloak kind tests

* ci: run keycloak basic scenario

* chore: refactor to using multiple domains

* chore: wip for keycloak multidomain

* adding multi domain support for keycloak as external identity provider (#556)

Co-authored-by: Mohammed Naser <mnaser@vexxhost.com>

* chore: initial impl for multiple domain

* chore: grafan + keycloak wip

* chore: fix missing roles

* chore: remove commented out role

* chore: add ci debug

* chore: use smaller nodes for test

* ci: fix keycloak_user_info

* ci: fix keycloak_user_info

* ci: debug keycloak ci

* chore: fix linters

* ci: retry a few times for keycloak users to appear

* ci: ci fixes

* ci: misc fixes

* ci: fix secret generation

* ci: fix missing secrets

---------

Co-authored-by: okozachenko1203 <okozachenko1203@users.noreply.github.com>
Co-authored-by: Mohammed Naser <mnaser@vexxhost.com>
Co-authored-by: Jeremy Lee <6729613+legit-ninja@users.noreply.github.com>
diff --git a/roles/keystone/README.md b/roles/keystone/README.md
index 5ab571b..ef6921c 100644
--- a/roles/keystone/README.md
+++ b/roles/keystone/README.md
@@ -10,9 +10,6 @@
 ```yaml
 keystone_helm_values:
   conf:
-    keystone:
-      identity:
-        domain_configurations_from_database: true
     ks_domains:
       contoso:
         identity:
diff --git a/roles/keystone/defaults/main.yml b/roles/keystone/defaults/main.yml
index 2dc6dc7..40ca892 100644
--- a/roles/keystone/defaults/main.yml
+++ b/roles/keystone/defaults/main.yml
@@ -21,3 +21,43 @@
 
 # List of annotations to apply to the Ingress
 keystone_ingress_annotations: {}
+
+# The following set of variables can be used for simple setups where you can
+# only need a single domain and realm.  You can use the `keystone_domains`
+# variable to configure more complex setups.
+keystone_keycloak_server_url: "https://{{ keycloak_host }}"
+keystone_keycloak_server_internal_url: http://keycloak.auth-system.svc
+keystone_keycloak_user_realm_name: master
+keystone_keycloak_admin_client_id: admin-cli
+keystone_keycloak_admin_user: admin
+keystone_keycloak_admin_password: "{{ keycloak_admin_password }}"
+keystone_keycloak_realm: atmosphere
+keystone_keycloak_realm_name: Atmosphere
+keystone_keycloak_client_id: keystone
+# keystone_keycloak_client_secret:
+keystone_keycloak_scopes: "openid email profile"
+
+# This variable can be used for more complex setups that require multiple
+# domains that are mapped to multiple realms.  If you are looking to use a
+# single domain and realm, you can skip this section.
+keystone_domains:
+  - name: "{{ keystone_keycloak_realm }}"                                       # Domain name
+    label: "{{ keystone_keycloak_realm_name }}"                                 # Realm display name + Horizon label
+    keycloak_server_url: "{{ keystone_keycloak_server_url }}"                   # Public Keycloak URL
+    keycloak_server_internal_url: "{{ keystone_keycloak_server_internal_url }}" # Internal Keycloak URL
+    keycloak_user_realm_name: "{{ keystone_keycloak_user_realm_name }}"         # Keycloak realm name
+    keycloak_admin_client_id: "{{ keystone_keycloak_admin_client_id }}"         # Keycloak admin client ID
+    keycloak_admin_user: "{{ keystone_keycloak_admin_user }}"                   # Keycloak admin username
+    keycloak_admin_password: "{{ keystone_keycloak_admin_password }}"           # Keycloak admin password
+    keycloak_realm: "{{ keystone_keycloak_realm }}"                             # Keycloak realm name
+    keycloak_client_id: "{{ keystone_keycloak_client_id }}"                     # Keycloak client ID
+    keycloak_client_secret: "{{ keystone_keycloak_client_secret }}"             # Keycloak client secret
+    keycloak_scopes: "{{ keystone_keycloak_scopes }}"                           # Keycloak scopes
+
+# Keystone OpenID Connect settings (defaults to Keycloak for Atmosphere)
+keystone_oidc_ssl_validate_server: "{{ (cluster_issuer_type == 'self-signed') | ternary('Off', 'On') }}"
+keystone_oidc_crypto_passphrase: "{{ undef('You must specify a Keystone OIDC client secret using keystone_oidc_crypto_passphrase') }}"
+keystone_oidc_redirect_uri: "https://{{ openstack_helm_endpoints_keystone_api_host }}/v3/auth/OS-FEDERATION/identity_providers/redirect"
+keystone_oidc_redirect_urls_allowed:
+  - "^https://{{ openstack_helm_endpoints_keystone_api_host }}/v3/auth/OS-FEDERATION/identity_providers/({{ keystone_domains | map(attribute='name') | join('|') }})/protocols/openid/websso" # noqa: yaml[line-length]
+  - "^https://{{ openstack_helm_endpoints_horizon_api_host }}/auth/logout/$"
diff --git a/roles/keystone/meta/main.yml b/roles/keystone/meta/main.yml
index bf58a2f..3e149bf 100644
--- a/roles/keystone/meta/main.yml
+++ b/roles/keystone/meta/main.yml
@@ -25,6 +25,7 @@
 
 dependencies:
   - role: defaults
+  # - role: openstacksdk
   - role: openstack_helm_endpoints
     vars:
       openstack_helm_endpoints_chart: keystone
diff --git a/roles/keystone/tasks/main.yml b/roles/keystone/tasks/main.yml
index 75e2e49..c9e6229 100644
--- a/roles/keystone/tasks/main.yml
+++ b/roles/keystone/tasks/main.yml
@@ -36,6 +36,54 @@
         name: "{{ keystone_helm_release_name }}"
         namespace: "{{ keystone_helm_release_namespace }}"
 
+- name: Create Keycloak realms
+  run_once: true
+  delegate_to: localhost
+  changed_when: false
+  community.general.keycloak_realm:
+    # Keycloak settings
+    auth_keycloak_url: "{{ item.keycloak_server_url }}"
+    auth_realm: "{{ item.keycloak_user_realm_name }}"
+    auth_client_id: "{{ item.keycloak_admin_client_id }}"
+    auth_username: "{{ item.keycloak_admin_user }}"
+    auth_password: "{{ item.keycloak_admin_password }}"
+    validate_certs: "{{ cluster_issuer_type != 'self-signed' }}"
+    # Realm settings
+    id: "{{ item.keycloak_realm }}"
+    realm: "{{ item.keycloak_realm }}"
+    display_name: "{{ item.label }}"
+    enabled: true
+  loop: "{{ keystone_domains }}"
+  loop_control:
+    label: "{{ item.name }}"
+
+- name: Create ConfigMap with all OpenID connect configurations
+  run_once: true
+  kubernetes.core.k8s:
+    template: configmap-openid-metadata.yml.j2
+
+- name: Create Keycloak clients
+  run_once: true
+  delegate_to: localhost
+  community.general.keycloak_client:
+    # Keycloak settings
+    auth_keycloak_url: "{{ item.keycloak_server_url }}"
+    auth_realm: "{{ item.keycloak_user_realm_name }}"
+    auth_client_id: "{{ item.keycloak_admin_client_id }}"
+    auth_username: "{{ item.keycloak_admin_user }}"
+    auth_password: "{{ item.keycloak_admin_password }}"
+    validate_certs: "{{ cluster_issuer_type != 'self-signed' }}"
+    # Realm settings
+    realm: "{{ item.keycloak_realm }}"
+    client_id: "{{ item.keycloak_client_id }}"
+    secret: "{{ item.keycloak_client_secret }}"
+    redirect_uris:
+      - "{{ keystone_oidc_redirect_uri }}"
+      - "https://{{ openstack_helm_endpoints_horizon_api_host }}/auth/logout/"
+  loop: "{{ keystone_domains }}"
+  loop_control:
+    label: "{{ item.name }}"
+
 - name: Deploy Helm chart
   run_once: true
   kubernetes.core.helm:
@@ -54,3 +102,54 @@
     openstack_helm_ingress_service_name: keystone-api
     openstack_helm_ingress_service_port: 5000
     openstack_helm_ingress_annotations: "{{ keystone_ingress_annotations }}"
+
+- name: Create Keystone domains
+  run_once: true
+  delegate_to: localhost
+  vexxhost.atmosphere.identity_domain:
+    name: "{{ item.name }}"
+  register: keystone_domains_result
+  loop: "{{ keystone_domains }}"
+  loop_control:
+    label: "{{ item.name }}"
+
+- name: Create Keystone identity providers
+  run_once: true
+  delegate_to: localhost
+  vexxhost.atmosphere.federation_idp:
+    name: "{{ item.domain.name }}"
+    domain_id: "{{ item.domain.id }}"
+    remote_ids:
+      - "{{ item.item | vexxhost.atmosphere.issuer_from_domain }}"
+  loop: "{{ keystone_domains_result.results }}"
+  loop_control:
+    label: "{{ item.domain.name }}"
+
+- name: Create Keystone federation mappings
+  run_once: true
+  delegate_to: localhost
+  vexxhost.atmosphere.federation_mapping:
+    name: "{{ item.name }}-openid"
+    rules:
+      - local:
+          - user:
+              type: local
+              id: "{0}"
+              domain:
+                name: "{{ item.name }}"
+        remote:
+          - type: OIDC-sub
+  loop: "{{ keystone_domains }}"
+  loop_control:
+    label: "{{ item.name }}"
+
+- name: Create Keystone federation protocols
+  run_once: true
+  delegate_to: localhost
+  vexxhost.atmosphere.keystone_federation_protocol:
+    name: openid
+    idp_id: "{{ item.name }}"
+    mapping_id: "{{ item.name }}-openid"
+  loop: "{{ keystone_domains }}"
+  loop_control:
+    label: "{{ item.name }}"
diff --git a/roles/keystone/templates/configmap-openid-metadata.yml.j2 b/roles/keystone/templates/configmap-openid-metadata.yml.j2
new file mode 100644
index 0000000..4c2e653
--- /dev/null
+++ b/roles/keystone/templates/configmap-openid-metadata.yml.j2
@@ -0,0 +1,13 @@
+apiVersion: v1
+metadata:
+  name: keystone-openid-metadata
+  namespace: "{{ keystone_helm_release_namespace }}"
+  labels:
+    application: keystone
+kind: ConfigMap
+data:
+{% for domain in keystone_domains %}
+  {{ domain.name }}-oidc-client: '{"client_id":"{{ domain.keycloak_client_id }}","client_secret":"{{ domain.keycloak_client_secret }}","response_type":"id_token"}'
+  {{ domain.name }}-oidc-conf: '{"scope":"{{ domain.keycloak_scopes }}"}'
+  {{ domain.name }}-oidc-provider: '{{ lookup('url', domain.keycloak_server_url ~ "/realms/" ~ domain.keycloak_realm ~ "/.well-known/openid-configuration", validate_certs=keystone_oidc_ssl_validate_server) }}'
+{% endfor %}
diff --git a/roles/keystone/vars/main.yml b/roles/keystone/vars/main.yml
index fc16ccd..1cc61ef 100644
--- a/roles/keystone/vars/main.yml
+++ b/roles/keystone/vars/main.yml
@@ -17,38 +17,16 @@
   images:
     tags: "{{ atmosphere_images | vexxhost.atmosphere.openstack_helm_image_tags('keystone') }}"
   pod:
-    #     mounts = {
-    #       keystone_api = {
-    #         keystone_api = {
-    #           volumeMounts = [
-    #             {
-    #               name      = kubernetes_config_map.keystone_ldap_ca.metadata[0].name
-    #               mountPath = "/etc/keystone/ldap"
-    #             },
-    #             {
-    #               name      = kubernetes_config_map.keystone_openid_connect_metadata.metadata[0].name
-    #               mountPath = "/var/lib/apache2/oidc"
-    #             }
-    #           ],
-    #           volumes = [
-    #             {
-    #               name = kubernetes_config_map.keystone_ldap_ca.metadata[0].name
-    #               configMap = {
-    #                 name = kubernetes_config_map.keystone_ldap_ca.metadata[0].name
-    #               }
-    #             },
-    #             {
-    #               name = kubernetes_config_map.keystone_openid_connect_metadata.metadata[0].name
-    #               configMap = {
-    #                 name = kubernetes_config_map.keystone_openid_connect_metadata.metadata[0].name
-    #               }
-    #             }
-    #           ]
-    #         }
-    #       }
-    #     },
     replicas:
       api: 3
+    mounts:
+      keystone_api:
+        keystone_api:
+          volumeMounts: "{{ keystone_domains | vexxhost.atmosphere.keystone_domains_to_mounts }}"
+          volumes:
+            - name: keystone-openid-metadata
+              configMap:
+                name: keystone-openid-metadata
   conf:
     keystone:
       DEFAULT:
@@ -57,15 +35,58 @@
         methods: password,token,openid,application_credential
       cors:
         allowed_origins: "*"
+      openid:
+        remote_id_attribute: HTTP_OIDC_ISS
       federation:
-        assertion_prefix: OIDC-
-        remote_id_attribute: OIDC-iss
         # TODO(mnaser): Lookup using openstack_helm_endpoints
         trusted_dashboard: "https://{{ openstack_helm_endpoints_horizon_api_host }}/auth/websso/"
-      identity:
-        domain_configurations_from_database: true
       oslo_messaging_notifications:
         driver: noop
+    wsgi_keystone: |
+      LoadModule headers_module /usr/lib/apache2/modules/mod_headers.so
+      Listen 0.0.0.0:5000
+      TransferLog /dev/stdout
+      ErrorLog /dev/stderr
+      <VirtualHost *:5000>
+        # WSGI
+        WSGIDaemonProcess keystone-public processes=4 threads=1 user=keystone group=keystone display-name=%{GROUP}
+        WSGIProcessGroup keystone-public
+        WSGIScriptAlias / /var/www/cgi-bin/keystone/keystone-wsgi-public
+        WSGIApplicationGroup %{GLOBAL}
+        WSGIPassAuthorization On
+        # NOTE(mnaser): This is to by-pass large header limits for large tokens
+        LimitRequestFieldSize 16384
+        # OIDC
+        OIDCClaimPrefix "OIDC-"
+        OIDCMetadataDir /var/lib/apache2/oidc
+        OIDCSSLValidateServer "{{ keystone_oidc_ssl_validate_server }}"
+        OIDCCryptoPassphrase {{ keystone_oidc_crypto_passphrase }}
+        OIDCRedirectURI {{ keystone_oidc_redirect_uri }}
+        OIDCRedirectURLsAllowed {{ keystone_oidc_redirect_urls_allowed | join(' ') }}
+        # NOTE(mnaser): These are Atmosphere specific settings.
+        OIDCSessionType client-cookie:store_id_token
+        OIDCXForwardedHeaders X-Forwarded-Host X-Forwarded-Proto
+        <Location /v3/auth/OS-FEDERATION/identity_providers/redirect>
+          AuthType openid-connect
+          Require valid-user
+        </Location>
+        <Location /v3/auth/OS-FEDERATION/websso/openid>
+          Require valid-user
+          AuthType openid-connect
+        </Location>
+        {% for domain in keystone_domains %}
+        <Location /v3/OS-FEDERATION/identity_providers/{{ domain.name }}/protocols/openid/auth>
+          Require valid-user
+          AuthType oauth20
+        </Location>
+        <Location /v3/auth/OS-FEDERATION/identity_providers/{{ domain.name }}/protocols/openid/websso>
+          Require valid-user
+          AuthType openid-connect
+          OIDCDiscoverURL {{ keystone_oidc_redirect_uri }}?iss={{ domain | urlencoded_issuer_from_domain }}
+        </Location>
+        {% endfor %}
+      </VirtualHost>
+    ks_domains: "{{ keystone_domains | vexxhost.atmosphere.to_ks_domains }}"
   manifests:
     job_credential_cleanup: false
     ingress_api: false