From b11da288d2b49cb0a5a371fc33dec49a76ad8fbf Mon Sep 17 00:00:00 2001 From: desand01 Date: Mon, 17 Jun 2024 01:06:47 -0400 Subject: [PATCH] Keycloak set client authentification flows by name (#8428) * first commit * Add change logs * fix sanity * Sanity 2 * Test unset flows * Update plugins/modules/keycloak_client.py Co-authored-by: Alexei Znamensky <103110+russoz@users.noreply.github.com> * Update plugins/modules/keycloak_client.py Co-authored-by: Felix Fontein * Update changelogs/fragments/8428-assign-auth-flow-by-name-keycloak-client.yaml Co-authored-by: Felix Fontein * Remove double traitement from "alias" * Update plugins/modules/keycloak_client.py Co-authored-by: Felix Fontein * Update plugins/modules/keycloak_client.py Co-authored-by: Felix Fontein --------- Co-authored-by: Andre Desrosiers Co-authored-by: Alexei Znamensky <103110+russoz@users.noreply.github.com> Co-authored-by: Felix Fontein --- ...ign-auth-flow-by-name-keycloak-client.yaml | 2 + plugins/modules/keycloak_client.py | 111 ++++++++++++++- .../targets/keycloak_client/tasks/main.yml | 128 ++++++++++++++++++ 3 files changed, 240 insertions(+), 1 deletion(-) create mode 100644 changelogs/fragments/8428-assign-auth-flow-by-name-keycloak-client.yaml diff --git a/changelogs/fragments/8428-assign-auth-flow-by-name-keycloak-client.yaml b/changelogs/fragments/8428-assign-auth-flow-by-name-keycloak-client.yaml new file mode 100644 index 0000000000..d9bb9bc3ea --- /dev/null +++ b/changelogs/fragments/8428-assign-auth-flow-by-name-keycloak-client.yaml @@ -0,0 +1,2 @@ +minor_changes: + - keycloak_client - assign auth flow by name (https://github.com/ansible-collections/community.general/pull/8428). diff --git a/plugins/modules/keycloak_client.py b/plugins/modules/keycloak_client.py index 3628e5a517..efaa66e26d 100644 --- a/plugins/modules/keycloak_client.py +++ b/plugins/modules/keycloak_client.py @@ -340,6 +340,42 @@ options: description: - Override realm authentication flow bindings. type: dict + suboptions: + browser: + description: + - Flow ID of the browser authentication flow. + - O(authentication_flow_binding_overrides.browser) + and O(authentication_flow_binding_overrides.browser_name) are mutually exclusive. + type: str + + browser_name: + description: + - Flow name of the browser authentication flow. + - O(authentication_flow_binding_overrides.browser) + and O(authentication_flow_binding_overrides.browser_name) are mutually exclusive. + aliases: + - browserName + type: str + version_added: 9.1.0 + + direct_grant: + description: + - Flow ID of the direct grant authentication flow. + - O(authentication_flow_binding_overrides.direct_grant) + and O(authentication_flow_binding_overrides.direct_grant_name) are mutually exclusive. + aliases: + - directGrant + type: str + + direct_grant_name: + description: + - Flow name of the direct grant authentication flow. + - O(authentication_flow_binding_overrides.direct_grant) + and O(authentication_flow_binding_overrides.direct_grant_name) are mutually exclusive. + aliases: + - directGrantName + type: str + version_added: 9.1.0 aliases: - authenticationFlowBindingOverrides version_added: 3.4.0 @@ -781,6 +817,64 @@ def sanitize_cr(clientrep): return normalise_cr(result) +def get_authentication_flow_id(flow_name, realm, kc): + """ Get the authentication flow ID based on the flow name, realm, and Keycloak client. + + Args: + flow_name (str): The name of the authentication flow. + realm (str): The name of the realm. + kc (KeycloakClient): The Keycloak client instance. + + Returns: + str: The ID of the authentication flow. + + Raises: + KeycloakAPIException: If the authentication flow with the given name is not found in the realm. + """ + flow = kc.get_authentication_flow_by_alias(flow_name, realm) + if flow: + return flow["id"] + kc.module.fail_json(msg='Authentification flow %s not found in realm %s' % (flow_name, realm)) + + +def flow_binding_from_dict_to_model(newClientFlowBinding, realm, kc): + """ Convert a dictionary representing client flow bindings to a model representation. + + Args: + newClientFlowBinding (dict): A dictionary containing client flow bindings. + realm (str): The name of the realm. + kc (KeycloakClient): An instance of the KeycloakClient class. + + Returns: + dict: A dictionary representing the model flow bindings. The dictionary has two keys: + - "browser" (str or None): The ID of the browser authentication flow binding, or None if not provided. + - "direct_grant" (str or None): The ID of the direct grant authentication flow binding, or None if not provided. + + Raises: + KeycloakAPIException: If the authentication flow with the given name is not found in the realm. + + """ + + modelFlow = { + "browser": None, + "direct_grant": None + } + + for k, v in newClientFlowBinding.items(): + if not v: + continue + if k == "browser": + modelFlow["browser"] = v + elif k == "browser_name": + modelFlow["browser"] = get_authentication_flow_id(v, realm, kc) + elif k == "direct_grant": + modelFlow["direct_grant"] = v + elif k == "direct_grant_name": + modelFlow["direct_grant"] = get_authentication_flow_id(v, realm, kc) + + return modelFlow + + def main(): """ Module execution @@ -799,6 +893,13 @@ def main(): config=dict(type='dict'), ) + authentication_flow_spec = dict( + browser=dict(type='str'), + browser_name=dict(type='str', aliases=['browserName']), + direct_grant=dict(type='str', aliases=['directGrant']), + direct_grant_name=dict(type='str', aliases=['directGrantName']), + ) + meta_args = dict( state=dict(default='present', choices=['present', 'absent']), realm=dict(type='str', default='master'), @@ -838,7 +939,13 @@ def main(): use_template_scope=dict(type='bool', aliases=['useTemplateScope']), use_template_mappers=dict(type='bool', aliases=['useTemplateMappers']), always_display_in_console=dict(type='bool', aliases=['alwaysDisplayInConsole']), - authentication_flow_binding_overrides=dict(type='dict', aliases=['authenticationFlowBindingOverrides']), + authentication_flow_binding_overrides=dict( + type='dict', + aliases=['authenticationFlowBindingOverrides'], + options=authentication_flow_spec, + required_one_of=[['browser', 'direct_grant', 'browser_name', 'direct_grant_name']], + mutually_exclusive=[['browser', 'browser_name'], ['direct_grant', 'direct_grant_name']], + ), protocol_mappers=dict(type='list', elements='dict', options=protmapper_spec, aliases=['protocolMappers']), authorization_settings=dict(type='dict', aliases=['authorizationSettings']), default_client_scopes=dict(type='list', elements='str', aliases=['defaultClientScopes']), @@ -900,6 +1007,8 @@ def main(): # they are not specified if client_param == 'protocol_mappers': new_param_value = [dict((k, v) for k, v in x.items() if x[k] is not None) for x in new_param_value] + elif client_param == 'authentication_flow_binding_overrides': + new_param_value = flow_binding_from_dict_to_model(new_param_value, realm, kc) changeset[camel(client_param)] = new_param_value diff --git a/tests/integration/targets/keycloak_client/tasks/main.yml b/tests/integration/targets/keycloak_client/tasks/main.yml index 5e7c7fae39..e1a7d2ebfb 100644 --- a/tests/integration/targets/keycloak_client/tasks/main.yml +++ b/tests/integration/targets/keycloak_client/tasks/main.yml @@ -103,3 +103,131 @@ assert: that: - check_client_when_present_and_changed is changed + +- name: Desire client with flow binding overrides + community.general.keycloak_client: + auth_keycloak_url: "{{ url }}" + auth_realm: "{{ admin_realm }}" + auth_username: "{{ admin_user }}" + auth_password: "{{ admin_password }}" + realm: "{{ realm }}" + client_id: "{{ client_id }}" + state: present + redirect_uris: '{{redirect_uris1}}' + attributes: '{{client_attributes1}}' + protocol_mappers: '{{protocol_mappers1}}' + authentication_flow_binding_overrides: + browser_name: browser + direct_grant_name: direct grant + register: desire_client_with_flow_binding_overrides + +- name: Assert flows are set + assert: + that: + - desire_client_with_flow_binding_overrides is changed + - "'authenticationFlowBindingOverrides' in desire_client_with_flow_binding_overrides.end_state" + - desire_client_with_flow_binding_overrides.end_state.authenticationFlowBindingOverrides.browser | length > 0 + - desire_client_with_flow_binding_overrides.end_state.authenticationFlowBindingOverrides.direct_grant | length > 0 + +- name: Backup flow UUIDs + set_fact: + flow_browser_uuid: "{{ desire_client_with_flow_binding_overrides.end_state.authenticationFlowBindingOverrides.browser }}" + flow_direct_grant_uuid: "{{ desire_client_with_flow_binding_overrides.end_state.authenticationFlowBindingOverrides.direct_grant }}" + +- name: Desire client with flow binding overrides remove direct_grant_name + community.general.keycloak_client: + auth_keycloak_url: "{{ url }}" + auth_realm: "{{ admin_realm }}" + auth_username: "{{ admin_user }}" + auth_password: "{{ admin_password }}" + realm: "{{ realm }}" + client_id: "{{ client_id }}" + state: present + redirect_uris: '{{redirect_uris1}}' + attributes: '{{client_attributes1}}' + protocol_mappers: '{{protocol_mappers1}}' + authentication_flow_binding_overrides: + browser_name: browser + register: desire_client_with_flow_binding_overrides + +- name: Assert flows are updated + assert: + that: + - desire_client_with_flow_binding_overrides is changed + - "'authenticationFlowBindingOverrides' in desire_client_with_flow_binding_overrides.end_state" + - desire_client_with_flow_binding_overrides.end_state.authenticationFlowBindingOverrides.browser | length > 0 + - "'direct_grant' not in desire_client_with_flow_binding_overrides.end_state.authenticationFlowBindingOverrides" + +- name: Desire client with flow binding overrides remove browser add direct_grant + community.general.keycloak_client: + auth_keycloak_url: "{{ url }}" + auth_realm: "{{ admin_realm }}" + auth_username: "{{ admin_user }}" + auth_password: "{{ admin_password }}" + realm: "{{ realm }}" + client_id: "{{ client_id }}" + state: present + redirect_uris: '{{redirect_uris1}}' + attributes: '{{client_attributes1}}' + protocol_mappers: '{{protocol_mappers1}}' + authentication_flow_binding_overrides: + direct_grant_name: direct grant + register: desire_client_with_flow_binding_overrides + +- name: Assert flows are updated + assert: + that: + - desire_client_with_flow_binding_overrides is changed + - "'authenticationFlowBindingOverrides' in desire_client_with_flow_binding_overrides.end_state" + - "'browser' not in desire_client_with_flow_binding_overrides.end_state.authenticationFlowBindingOverrides" + - desire_client_with_flow_binding_overrides.end_state.authenticationFlowBindingOverrides.direct_grant | length > 0 + +- name: Desire client with flow binding overrides with UUIDs + community.general.keycloak_client: + auth_keycloak_url: "{{ url }}" + auth_realm: "{{ admin_realm }}" + auth_username: "{{ admin_user }}" + auth_password: "{{ admin_password }}" + realm: "{{ realm }}" + client_id: "{{ client_id }}" + state: present + redirect_uris: '{{redirect_uris1}}' + attributes: '{{client_attributes1}}' + protocol_mappers: '{{protocol_mappers1}}' + authentication_flow_binding_overrides: + browser: "{{ flow_browser_uuid }}" + direct_grant: "{{ flow_direct_grant_uuid }}" + register: desire_client_with_flow_binding_overrides + +- name: Assert flows are updated + assert: + that: + - desire_client_with_flow_binding_overrides is changed + - "'authenticationFlowBindingOverrides' in desire_client_with_flow_binding_overrides.end_state" + - desire_client_with_flow_binding_overrides.end_state.authenticationFlowBindingOverrides.browser == flow_browser_uuid + - desire_client_with_flow_binding_overrides.end_state.authenticationFlowBindingOverrides.direct_grant == flow_direct_grant_uuid + +- name: Unset flow binding overrides + community.general.keycloak_client: + auth_keycloak_url: "{{ url }}" + auth_realm: "{{ admin_realm }}" + auth_username: "{{ admin_user }}" + auth_password: "{{ admin_password }}" + realm: "{{ realm }}" + client_id: "{{ client_id }}" + state: present + redirect_uris: '{{redirect_uris1}}' + attributes: '{{client_attributes1}}' + protocol_mappers: '{{protocol_mappers1}}' + authentication_flow_binding_overrides: + browser: "{{ None }}" + direct_grant: null + register: desire_client_with_flow_binding_overrides + +- name: Assert flows are removed + assert: + that: + - desire_client_with_flow_binding_overrides is changed + - "'authenticationFlowBindingOverrides' in desire_client_with_flow_binding_overrides.end_state" + - "'browser' not in desire_client_with_flow_binding_overrides.end_state.authenticationFlowBindingOverrides" + - "'direct_grant' not in desire_client_with_flow_binding_overrides.end_state.authenticationFlowBindingOverrides" \ No newline at end of file