From a8b977320c04a75069f4208cf723743b53ed0d20 Mon Sep 17 00:00:00 2001 From: desand01 Date: Sat, 19 Apr 2025 03:00:44 -0400 Subject: [PATCH] Fix Keycloak authentication flow configuration issues (#9987) * Add delete_authentication_config method and integrate it into create_or_update_executions * typo * Sanity * Add integration tests for keycloak_authentication module with README, tasks, and variables * Add copyright and license information to access_token.yml * Sanity * Refactor Keycloak integration tests: streamline README, update access token task, and enhance variable management * Maj changelogs fragments --------- Co-authored-by: Andre Desrosiers --- .../9987-keycloak-auth-flow-fix-config.yaml | 2 + .../identity/keycloak/keycloak.py | 17 ++ plugins/modules/keycloak_authentication.py | 2 + .../targets/keycloak_authentication/README.md | 10 + .../targets/keycloak_authentication/aliases | 5 + .../tasks/access_token.yml | 25 +++ .../keycloak_authentication/tasks/main.yml | 185 ++++++++++++++++++ .../keycloak_authentication/vars/main.yml | 16 ++ 8 files changed, 262 insertions(+) create mode 100644 changelogs/fragments/9987-keycloak-auth-flow-fix-config.yaml create mode 100644 tests/integration/targets/keycloak_authentication/README.md create mode 100644 tests/integration/targets/keycloak_authentication/aliases create mode 100644 tests/integration/targets/keycloak_authentication/tasks/access_token.yml create mode 100644 tests/integration/targets/keycloak_authentication/tasks/main.yml create mode 100644 tests/integration/targets/keycloak_authentication/vars/main.yml diff --git a/changelogs/fragments/9987-keycloak-auth-flow-fix-config.yaml b/changelogs/fragments/9987-keycloak-auth-flow-fix-config.yaml new file mode 100644 index 0000000000..b7df120f4a --- /dev/null +++ b/changelogs/fragments/9987-keycloak-auth-flow-fix-config.yaml @@ -0,0 +1,2 @@ +bugfixes: + - keycloak_authentication - fix authentification config duplication for Keycloak < 26.2.0 (https://github.com/ansible-collections/community.general/pull/9987). \ No newline at end of file diff --git a/plugins/module_utils/identity/keycloak/keycloak.py b/plugins/module_utils/identity/keycloak/keycloak.py index da5080bbfe..c432c5a85e 100644 --- a/plugins/module_utils/identity/keycloak/keycloak.py +++ b/plugins/module_utils/identity/keycloak/keycloak.py @@ -2224,6 +2224,23 @@ class KeycloakAPI(object): except Exception as e: self.fail_request(e, msg="Unable to add authenticationConfig %s: %s" % (executionId, str(e))) + def delete_authentication_config(self, configId, realm='master'): + """ Delete authenticator config + + :param configId: id of authentication config + :param realm: realm of authentication config to be deleted + """ + try: + # Send a DELETE request to remove the specified authentication config from the Keycloak server. + self._request( + URL_AUTHENTICATION_CONFIG.format( + url=self.baseurl, + realm=realm, + id=configId), + method='DELETE') + except Exception as e: + self.fail_request(e, msg="Unable to delete authentication config %s: %s" % (configId, str(e))) + def create_subflow(self, subflowName, flowAlias, realm='master', flowType='basic-flow'): """ Create new sublow on the flow diff --git a/plugins/modules/keycloak_authentication.py b/plugins/modules/keycloak_authentication.py index a117c730e6..a0daf42b35 100644 --- a/plugins/modules/keycloak_authentication.py +++ b/plugins/modules/keycloak_authentication.py @@ -308,6 +308,8 @@ def create_or_update_executions(kc, config, realm='master'): } # add the execution configuration if new_exec["authenticationConfig"] is not None: + if "authenticationConfig" in execution and "id" in execution["authenticationConfig"]: + kc.delete_authentication_config(execution["authenticationConfig"]["id"], realm=realm) kc.add_authenticationConfig_to_execution(updated_exec["id"], new_exec["authenticationConfig"], realm=realm) for key in new_exec: # remove unwanted key for the next API call diff --git a/tests/integration/targets/keycloak_authentication/README.md b/tests/integration/targets/keycloak_authentication/README.md new file mode 100644 index 0000000000..03ca31b199 --- /dev/null +++ b/tests/integration/targets/keycloak_authentication/README.md @@ -0,0 +1,10 @@ + +# Running keycloak_authentication module integration test + +Run integration tests: + + ansible-test integration -v keycloak_authentication --allow-unsupported --docker fedora35 --docker-network host \ No newline at end of file diff --git a/tests/integration/targets/keycloak_authentication/aliases b/tests/integration/targets/keycloak_authentication/aliases new file mode 100644 index 0000000000..bd1f024441 --- /dev/null +++ b/tests/integration/targets/keycloak_authentication/aliases @@ -0,0 +1,5 @@ +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +unsupported diff --git a/tests/integration/targets/keycloak_authentication/tasks/access_token.yml b/tests/integration/targets/keycloak_authentication/tasks/access_token.yml new file mode 100644 index 0000000000..145f4708bc --- /dev/null +++ b/tests/integration/targets/keycloak_authentication/tasks/access_token.yml @@ -0,0 +1,25 @@ +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later +--- +- name: Get access token + ansible.builtin.uri: + url: "{{ url }}/realms/{{ admin_realm }}/protocol/openid-connect/token" + method: POST + status_code: 200 + headers: + Accept: application/json + User-agent: Ansible + body_format: form-urlencoded + body: + grant_type: "password" + client_id: "admin-cli" + username: "{{ admin_user }}" + password: "{{ admin_password }}" + register: token_response + no_log: true + +- name: Extract access token + ansible.builtin.set_fact: + access_token: "{{ token_response.json['access_token'] }}" + no_log: true diff --git a/tests/integration/targets/keycloak_authentication/tasks/main.yml b/tests/integration/targets/keycloak_authentication/tasks/main.yml new file mode 100644 index 0000000000..d286b70a35 --- /dev/null +++ b/tests/integration/targets/keycloak_authentication/tasks/main.yml @@ -0,0 +1,185 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later +- name: Install required packages + pip: + name: + - jmespath + - requests + register: result + until: result is success + +- name: Start container + community.docker.docker_container: + name: mykeycloak + image: "quay.io/keycloak/keycloak:{{ keycloak_version }}" + command: start-dev + env: + KC_HTTP_RELATIVE_PATH: /auth + KEYCLOAK_ADMIN: admin + KEYCLOAK_ADMIN_PASSWORD: password + ports: + - "{{ keycloak_port }}:8080" + detach: true + auto_remove: true + memory: 2200M + +- name: Wait for Keycloak + uri: + url: "{{ url }}/admin/" + status_code: 200 + validate_certs: no + register: result + until: result.status == 200 + retries: 10 + delay: 10 + +- name: Delete realm if exists + community.general.keycloak_realm: + auth_keycloak_url: "{{ url }}" + auth_realm: "{{ admin_realm }}" + auth_username: "{{ admin_user }}" + auth_password: "{{ admin_password }}" + realm: "{{ realm }}" + state: absent + +- name: Create realm + community.general.keycloak_realm: + auth_keycloak_url: "{{ url }}" + auth_realm: "{{ admin_realm }}" + auth_username: "{{ admin_user }}" + auth_password: "{{ admin_password }}" + id: "{{ realm }}" + realm: "{{ realm }}" + state: present + +- name: Create an authentication flow from first broker login and add an execution to it. + community.general.keycloak_authentication: + auth_keycloak_url: "{{ url }}" + auth_realm: "{{ admin_realm }}" + auth_username: "{{ admin_user }}" + auth_password: "{{ admin_password }}" + realm: "{{ realm }}" + alias: "Test first broker login" + copyFrom: "first broker login" + authenticationExecutions: + - providerId: "idp-review-profile" + requirement: "REQUIRED" + authenticationConfig: + alias: "Test review profile config" + config: + update.profile.on.first.login: "missing" + +- name: Create auth flow + community.general.keycloak_authentication: + auth_keycloak_url: "{{ url }}" + auth_realm: "{{ admin_realm }}" + auth_username: "{{ admin_user }}" + auth_password: "{{ admin_password }}" + realm: "{{ realm }}" + alias: "My conditionnal browser otp" + description: "browser based authentication with otp" + providerId: "basic-flow" + authenticationExecutions: + - displayName: Cookie + providerId: auth-cookie + requirement: ALTERNATIVE + - displayName: Kerberos + providerId: auth-spnego + requirement: DISABLED + - displayName: Identity Provider Redirector + providerId: identity-provider-redirector + requirement: ALTERNATIVE + - displayName: My browser otp forms + requirement: ALTERNATIVE + - displayName: Username Password Form + flowAlias: My browser otp forms + providerId: auth-username-password-form + requirement: REQUIRED + - displayName: My browser otp Browser - Conditional OTP + flowAlias: My browser otp forms + requirement: REQUIRED + providerId: "auth-conditional-otp-form" + authenticationConfig: + alias: my-conditional-otp-config + config: + defaultOtpOutcome: "force" + noOtpRequiredForHeaderPattern: "{{ keycloak_no_otp_required_pattern_orinale }}" + state: present + +- name: Modified auth flow with new config + community.general.keycloak_authentication: + auth_keycloak_url: "{{ url }}" + auth_realm: "{{ admin_realm }}" + auth_username: "{{ admin_user }}" + auth_password: "{{ admin_password }}" + realm: "{{ realm }}" + alias: "My conditionnal browser otp" + description: "browser based authentication with otp" + providerId: "basic-flow" + authenticationExecutions: + - displayName: Cookie + providerId: auth-cookie + requirement: ALTERNATIVE + - displayName: Kerberos + providerId: auth-spnego + requirement: DISABLED + - displayName: Identity Provider Redirector + providerId: identity-provider-redirector + requirement: ALTERNATIVE + - displayName: My browser otp forms + requirement: ALTERNATIVE + - displayName: Username Password Form + flowAlias: My browser otp forms + providerId: auth-username-password-form + requirement: REQUIRED + - displayName: My browser otp Browser - Conditional OTP + flowAlias: My browser otp forms + requirement: REQUIRED + providerId: "auth-conditional-otp-form" + authenticationConfig: + alias: my-conditional-otp-config + config: + defaultOtpOutcome: "force" + noOtpRequiredForHeaderPattern: "{{ keycloak_no_otp_required_pattern_modifed }}" + state: present + register: result + +- name: Retrive access + ansible.builtin.include_tasks: + file: access_token.yml + +- name: Export realm + ansible.builtin.uri: + url: "{{ url }}/admin/realms/{{ realm }}/partial-export?exportClients=false&exportGroupsAndRoles=false" + method: POST + headers: + Accept: application/json + User-agent: Ansible + Authorization: "Bearer {{ access_token }}" + body_format: form-urlencoded + body: {} + register: exported_realm + no_log: true + +- name: Assert `my-conditional-otp-config` exists only once + ansible.builtin.assert: + that: + - exported_realm.json | community.general.json_query('authenticatorConfig[?alias==`my-conditional-otp-config`]') | length == 1 + +- name: Delete auth flow + community.general.keycloak_authentication: + auth_keycloak_url: "{{ url }}" + auth_realm: "{{ admin_realm }}" + auth_username: "{{ admin_user }}" + auth_password: "{{ admin_password }}" + realm: "{{ realm }}" + alias: "My conditionnal browser otp" + state: absent + register: result + +- name: Remove container + community.docker.docker_container: + name: mykeycloak + state: absent \ No newline at end of file diff --git a/tests/integration/targets/keycloak_authentication/vars/main.yml b/tests/integration/targets/keycloak_authentication/vars/main.yml new file mode 100644 index 0000000000..03244e18c0 --- /dev/null +++ b/tests/integration/targets/keycloak_authentication/vars/main.yml @@ -0,0 +1,16 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later +keycloak_version: latest +keycloak_port: 8080 + +url: "http://localhost:{{ keycloak_port }}/auth" +admin_realm: master +admin_user: admin +admin_password: password +realm: myrealm + + +keycloak_no_otp_required_pattern_orinale: "X-Forwarded-For: 10\\.[0-9\\.:]+" +keycloak_no_otp_required_pattern_modifed: "X-Original-Forwarded-For: 10\\.[0-9\\.:]+" \ No newline at end of file