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 <andre.desrosiers@ssss.gouv.qc.ca>
This commit is contained in:
desand01 2025-04-19 03:00:44 -04:00 committed by GitHub
parent 80252b29f8
commit a8b977320c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 262 additions and 0 deletions

View file

@ -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).

View file

@ -2224,6 +2224,23 @@ class KeycloakAPI(object):
except Exception as e: except Exception as e:
self.fail_request(e, msg="Unable to add authenticationConfig %s: %s" % (executionId, str(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'): def create_subflow(self, subflowName, flowAlias, realm='master', flowType='basic-flow'):
""" Create new sublow on the flow """ Create new sublow on the flow

View file

@ -308,6 +308,8 @@ def create_or_update_executions(kc, config, realm='master'):
} }
# add the execution configuration # add the execution configuration
if new_exec["authenticationConfig"] is not None: 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) kc.add_authenticationConfig_to_execution(updated_exec["id"], new_exec["authenticationConfig"], realm=realm)
for key in new_exec: for key in new_exec:
# remove unwanted key for the next API call # remove unwanted key for the next API call

View file

@ -0,0 +1,10 @@
<!--
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
-->
# Running keycloak_authentication module integration test
Run integration tests:
ansible-test integration -v keycloak_authentication --allow-unsupported --docker fedora35 --docker-network host

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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\\.:]+"