diff --git a/roles/web-app-keycloak/tasks/03_manager_ops/update_routines.yml b/roles/web-app-keycloak/tasks/03_manager_ops/update_routines.yml index 51d773280a..f6497a9145 100644 --- a/roles/web-app-keycloak/tasks/03_manager_ops/update_routines.yml +++ b/roles/web-app-keycloak/tasks/03_manager_ops/update_routines.yml @@ -50,11 +50,11 @@ include_tasks: "{{ role_path }}/tasks/update/08_users/email_verified.yml" when: KEYCLOAK_USER_EMAILS_VERIFIED | bool -- name: "📂 Ensure application-declared clients (existing realms)" +- name: "📂 Ensure application-declared and SAML clients (existing realms)" include_tasks: "{{ role_path }}/tasks/update/10_declared_client.yml" vars: kc_declared_client_id: "{{ declared_client }}" - loop: "{{ KEYCLOAK_DECLARED_CLIENT_IDS }}" + loop: "{{ KEYCLOAK_DECLARED_CLIENT_IDS + KEYCLOAK_SAML_CLIENT_IDS }}" loop_control: loop_var: declared_client diff --git a/roles/web-app-keycloak/tasks/update/10_declared_client.yml b/roles/web-app-keycloak/tasks/update/10_declared_client.yml index 06a5934de8..bd44321fef 100644 --- a/roles/web-app-keycloak/tasks/update/10_declared_client.yml +++ b/roles/web-app-keycloak/tasks/update/10_declared_client.yml @@ -1,6 +1,6 @@ --- # Realm import only runs for a MISSING realm; this kcadm create-or-update brings -# application-declared clients to existing realms too. Desired state is the +# application-declared and SAML clients to existing realms too. Desired state is the # import's own client object, so the realm template stays the one SPOT. - name: "✅ Ensure the '{{ kc_declared_client_id }}' client" vars: diff --git a/roles/web-app-keycloak/vars/main.yml b/roles/web-app-keycloak/vars/main.yml index fb1cb9c339..acec94f56a 100644 --- a/roles/web-app-keycloak/vars/main.yml +++ b/roles/web-app-keycloak/vars/main.yml @@ -154,6 +154,7 @@ KEYCLOAK_MOODLE_ENABLED: "{{ 'web-app-moodle' in group_names }}" KEYCLOAK_DECLARED_CLIENT_APPS: "{{ lookup('applications') | kc_declared_client_apps(lookup('deployment').groups) }}" KEYCLOAK_DECLARED_CLIENT_IDS: "{{ lookup('applications') | kc_declared_client_ids(lookup('deployment').groups) }}" +KEYCLOAK_SAML_CLIENT_IDS: "{{ KEYCLOAK_DICTIONARY_REALM.clients | selectattr('protocol', 'equalto', 'saml') | map(attribute='clientId') | list }}" KEYCLOAK_RECAPTCHA_ENABLED: "{{ lookup('config', application_id, 'services.recaptcha.enabled') }}" KEYCLOAK_RECAPTCHA_KEY: "{{ lookup('config', application_id, 'secrets.credentials.recaptcha.key') }}" diff --git a/tests/unit/python/roles/web_app_keycloak/test_saml_clients_on_existing_realms.py b/tests/unit/python/roles/web_app_keycloak/test_saml_clients_on_existing_realms.py new file mode 100644 index 0000000000..a33fce088e --- /dev/null +++ b/tests/unit/python/roles/web_app_keycloak/test_saml_clients_on_existing_realms.py @@ -0,0 +1,41 @@ +import unittest + +from jinja2 import Environment + +from utils.cache.yaml import load_yaml_any + +from . import PROJECT_ROOT + +ROLE = PROJECT_ROOT / "roles" / "web-app-keycloak" +VARS = ROLE / "vars" / "main.yml" +ROUTINES = ROLE / "tasks" / "03_manager_ops" / "update_routines.yml" + +REALM = { + "clients": [ + {"clientId": "account", "protocol": "openid-connect"}, + {"clientId": "cymais.cloud", "protocol": "openid-connect"}, + {"clientId": "https://crm.example/saml/metadata", "protocol": "saml"}, + ] +} + + +class TestSamlClientsReachExistingRealms(unittest.TestCase): + def test_only_saml_clients_of_the_realm_dictionary_are_selected(self): + template = load_yaml_any(str(VARS))["KEYCLOAK_SAML_CLIENT_IDS"] + expression = template.strip().removeprefix("{{").removesuffix("}}") + selected = Environment(autoescape=True).compile_expression(expression)( + KEYCLOAK_DICTIONARY_REALM=REALM + ) + self.assertEqual(selected, ["https://crm.example/saml/metadata"]) + + def test_the_existing_realm_reconcile_loops_over_saml_clients(self): + task = next( + t + for t in load_yaml_any(str(ROUTINES)) + if "10_declared_client.yml" in str(t.get("include_tasks", "")) + ) + self.assertIn("KEYCLOAK_SAML_CLIENT_IDS", task["loop"]) + + +if __name__ == "__main__": + unittest.main()