diff --git a/galaxy.yml b/galaxy.yml index 3ae9809..0596571 100644 --- a/galaxy.yml +++ b/galaxy.yml @@ -8,6 +8,7 @@ authors: - Guido Grazioli - Pavan Kumar Motaparthi - Helmut Wolf + - Harsha Cherukuri description: Install and configure a keycloak, or Red Hat Single Sign-on, service. license_file: "LICENSE" tags: diff --git a/meta/runtime.yml b/meta/runtime.yml index 64490eb..e2907d9 100644 --- a/meta/runtime.yml +++ b/meta/runtime.yml @@ -15,6 +15,7 @@ action_groups: - keycloak_client_rolescope - keycloak_client_scope - keycloak_clientscope_type + - keycloak_clientscope_rolemappings - keycloak_clientsecret_info - keycloak_clientsecret_regenerate - keycloak_clienttemplate diff --git a/molecule/keycloak_modules/verify.yml b/molecule/keycloak_modules/verify.yml index 8194ade..867f687 100644 --- a/molecule/keycloak_modules/verify.yml +++ b/molecule/keycloak_modules/verify.yml @@ -38,6 +38,7 @@ - keycloak_client_rolescope - keycloak_client_scope - keycloak_clientscope_type + - keycloak_clientscope_rolemappings - keycloak_clientsecret_info - keycloak_clientsecret_regenerate - keycloak_clienttemplate @@ -303,6 +304,109 @@ - "{{ role }}" state: present + - name: keycloak_clientscope_rolemappings — map client roles to clientscope + middleware_automation.keycloak.keycloak_clientscope_rolemappings: + realm: "{{ target_realm }}" + client_id: "{{ client }}" + clientscope_id: "{{ scope }}" + role_names: + - "{{ client_role }}" + register: clientscope_rolemappings_result + + - name: Assert clientscope role mappings were created + ansible.builtin.assert: + that: + - clientscope_rolemappings_result is changed + - clientscope_rolemappings_result.end_state | length == 1 + + - name: keycloak_clientscope_rolemappings — remap client role (idempotency) + middleware_automation.keycloak.keycloak_clientscope_rolemappings: + realm: "{{ target_realm }}" + client_id: "{{ client }}" + clientscope_id: "{{ scope }}" + role_names: + - "{{ client_role }}" + register: clientscope_rolemappings_idempotent_result + + - name: Assert clientscope role mappings are idempotent + ansible.builtin.assert: + that: + - clientscope_rolemappings_idempotent_result is not changed + - clientscope_rolemappings_idempotent_result.end_state | length == 1 + + - name: keycloak_clientscope_rolemappings — map realm role to clientscope + middleware_automation.keycloak.keycloak_clientscope_rolemappings: + realm: "{{ target_realm }}" + clientscope_id: "{{ scope }}" + role_names: + - "{{ role }}" + register: clientscope_realm_rolemappings_result + + - name: Assert realm role was mapped to clientscope + ansible.builtin.assert: + that: + - clientscope_realm_rolemappings_result is changed + - clientscope_realm_rolemappings_result.end_state | length == 1 + + - name: keycloak_user — set email_verified explicitly + middleware_automation.keycloak.keycloak_user: + realm: "{{ target_realm }}" + username: "{{ user }}" + email_verified: true + state: present + register: user_email_verified_result + + - name: Assert email_verified was set + ansible.builtin.assert: + that: + - user_email_verified_result is changed + - user_email_verified_result.end_state.emailVerified == true + + - name: keycloak_user — leave email_verified unchanged with no_defaults + middleware_automation.keycloak.keycloak_user: + realm: "{{ target_realm }}" + username: "{{ user }}" + email_verified_behavior: no_defaults + state: present + register: user_email_verified_idempotent_result + + - name: Assert email_verified is unchanged + ansible.builtin.assert: + that: + - user_email_verified_idempotent_result is not changed + - user_email_verified_idempotent_result.end_state.emailVerified == true + + - name: keycloak_user — set required actions + middleware_automation.keycloak.keycloak_user: + realm: "{{ target_realm }}" + username: "{{ user }}" + required_actions: + - UPDATE_PASSWORD + - VERIFY_EMAIL + state: present + register: user_required_actions_result + + - name: Assert required actions were set + ansible.builtin.assert: + that: + - user_required_actions_result is changed + - "'UPDATE_PASSWORD' in user_required_actions_result.end_state.requiredActions" + - "'VERIFY_EMAIL' in user_required_actions_result.end_state.requiredActions" + + - name: keycloak_user — leave required actions unchanged when omitted + middleware_automation.keycloak.keycloak_user: + realm: "{{ target_realm }}" + username: "{{ user }}" + state: present + register: user_required_actions_idempotent_result + + - name: Assert required actions are unchanged + ansible.builtin.assert: + that: + - user_required_actions_idempotent_result is not changed + - "'UPDATE_PASSWORD' in user_required_actions_idempotent_result.end_state.requiredActions" + - "'VERIFY_EMAIL' in user_required_actions_idempotent_result.end_state.requiredActions" + - name: keycloak_clientsecret_info — read client secret middleware_automation.keycloak.keycloak_clientsecret_info: realm: "{{ target_realm }}" @@ -413,6 +517,23 @@ name: "{{ authz_scope }}" state: absent + - name: keycloak_clientscope_rolemappings — remove realm role from clientscope + middleware_automation.keycloak.keycloak_clientscope_rolemappings: + realm: "{{ target_realm }}" + clientscope_id: "{{ scope }}" + role_names: + - "{{ role }}" + state: absent + + - name: keycloak_clientscope_rolemappings — remove client role from clientscope + middleware_automation.keycloak.keycloak_clientscope_rolemappings: + realm: "{{ target_realm }}" + client_id: "{{ client }}" + clientscope_id: "{{ scope }}" + role_names: + - "{{ client_role }}" + state: absent + - name: keycloak_client_rolescope — remove role scope mapping middleware_automation.keycloak.keycloak_client_rolescope: realm: "{{ target_realm }}" diff --git a/plugins/module_utils/identity/keycloak/keycloak.py b/plugins/module_utils/identity/keycloak/keycloak.py index 601fb58..8d19024 100644 --- a/plugins/module_utils/identity/keycloak/keycloak.py +++ b/plugins/module_utils/identity/keycloak/keycloak.py @@ -59,6 +59,9 @@ URL_GROUP_CHILDREN = "{url}/admin/realms/{realm}/groups/{groupid}/children" URL_CLIENTSCOPES = "{url}/admin/realms/{realm}/client-scopes" URL_CLIENTSCOPE = "{url}/admin/realms/{realm}/client-scopes/{id}" +URL_CLIENTSCOPE_SCOPE_MAPPINGS = "{url}/admin/realms/{realm}/client-scopes/{id}/scope-mappings" +URL_CLIENTSCOPE_SCOPE_MAPPINGS_REALM = "{url}/admin/realms/{realm}/client-scopes/{id}/scope-mappings/realm" +URL_CLIENTSCOPE_SCOPE_MAPPINGS_CLIENT = "{url}/admin/realms/{realm}/client-scopes/{id}/scope-mappings/clients/{client}" URL_CLIENTSCOPE_PROTOCOLMAPPERS = "{url}/admin/realms/{realm}/client-scopes/{id}/protocol-mappers/models" URL_CLIENTSCOPE_PROTOCOLMAPPER = "{url}/admin/realms/{realm}/client-scopes/{id}/protocol-mappers/models/{mapper_id}" @@ -331,41 +334,44 @@ def get_token(module_params: dict[str, t.Any]) -> dict[str, str]: return {"Authorization": f"Bearer {token}", "Content-Type": "application/json"} -def is_struct_included(struct1: object, struct2: object, exclude: Sequence[str] | None = None) -> bool: +def is_struct_included( + struct1: dict | list | bool | int | str, + struct2: dict | list | bool | int | str, + exclude: Sequence[str] | None = None, + empty_list_result: bool = True, +) -> bool: """ This function compare if the first parameter structure is included in the second. The function use every elements of struct1 and validates they are present in the struct2 structure. The two structure does not need to be equals for that function to return true. Each elements are compared recursively. :param struct1: - type: - dict for the initial call, can be dict, list, bool, int or str for recursive calls description: reference structure :param struct2: - type: - dict for the initial call, can be dict, list, bool, int or str for recursive calls description: structure to compare with first parameter. :param exclude: - type: - list description: Key to exclude from the comparison. - default: None + :param empty_list_result: + description: + Return this value, when struct1 is an empty list. :return: - type: - bool description: Return True if all element of dict 1 are present in dict 2, return false otherwise. """ if isinstance(struct1, list) and isinstance(struct2, list): if not struct1 and not struct2: return True + + if not struct1: + return empty_list_result + for item1 in struct1: if isinstance(item1, (list, dict)): for item2 in struct2: - if is_struct_included(item1, item2, exclude): + if is_struct_included(item1, item2, exclude, empty_list_result): break else: return False @@ -379,7 +385,7 @@ def is_struct_included(struct1: object, struct2: object, exclude: Sequence[str] try: for key in struct1: if not (exclude and key in exclude): - if not is_struct_included(struct1[key], struct2[key], exclude): + if not is_struct_included(struct1[key], struct2[key], exclude, empty_list_result): return False except KeyError: return False @@ -2937,7 +2943,7 @@ class KeycloakAPI: :return: Representation of the user. """ try: - user_url = URL_USER.format(url=self.baseurl, realm=realm, id=user_id) + user_url = URL_USER.format(url=self.baseurl, realm=realm, id=user_id) + "?userProfileMetadata=True" userrep = json.load(self._request(user_url, method="GET")) return userrep except Exception as e: @@ -3108,11 +3114,19 @@ class KeycloakAPI: realm_group = self.find_group_by_path(group_to_add, realm=realm) if realm_group: self.add_user_to_group(user_id=userrep["id"], group_id=realm_group["id"], realm=realm) + else: + self.module.fail_json( + msg=f"Could not update group membership for user {userrep['username']} in realm {realm}: group not found {group_to_add}" + ) for group_to_remove in groups_to_remove: realm_group = self.find_group_by_path(group_to_remove, realm=realm) if realm_group: self.remove_user_from_group(user_id=userrep["id"], group_id=realm_group["id"], realm=realm) + else: + self.module.fail_json( + msg=f"Could not update group membership for user {userrep['username']} in realm {realm}: group not found {group_to_remove}" + ) return True except Exception as e: @@ -3257,6 +3271,52 @@ class KeycloakAPI: except Exception: return False + def get_all_clientscope_scope_mappings(self, clientscope_id, realm: str = "master"): + """Fetch all (realm and client) roles (scope-mappings) associated with the clientscope for a specific clientscope on the Keycloak server. + :param clientscope_id: ID of the clientscope from which to obtain the associated roles. + :param realm: Realm from which to obtain the scope. + :return: The clientscope scope-mappings. + """ + client_role_scope_url = URL_CLIENTSCOPE_SCOPE_MAPPINGS.format(url=self.baseurl, realm=realm, id=clientscope_id) + try: + return self._request_and_deserialize(client_role_scope_url, method="GET") + except Exception as e: + self.fail_request(e, msg=f"Could not fetch roles for client-scope {clientscope_id} in realm {realm}: {e}") + + def get_clientscope_scope_mappings_realm(self, clientscope_id, realm: str = "master"): + """Fetch the realm roles (scope-mappings) associated with the clientscope for a specific clientscope on the Keycloak server. + :param clientscope_id: ID of the clientscope from which to obtain the associated roles. + :param realm: Realm from which to obtain the scope. + :return: The clientscope realm scope-mappings. + """ + client_role_scope_url = URL_CLIENTSCOPE_SCOPE_MAPPINGS_REALM.format( + url=self.baseurl, realm=realm, id=clientscope_id + ) + try: + return self._request_and_deserialize(client_role_scope_url, method="GET") + except Exception as e: + self.fail_request( + e, msg=f"Could not fetch realm roles for client-scope {clientscope_id} in realm {realm}: {e}" + ) + + def get_clientscope_scope_mappings_client(self, clientscope_id, client_id, realm: str = "master"): + """Fetch the client roles (scope-mappings) associated with the clientscope for a specific clientscope and client on the Keycloak server. + :param clientscope_id: ID of the clientscope from which to obtain the associated roles. + :param clientid: ID of the client from which to obtain the associated roles. + :param realm: Realm from which to obtain the scope. + :return: The clientscope client scope-mappings. + """ + client_role_scope_url = URL_CLIENTSCOPE_SCOPE_MAPPINGS_CLIENT.format( + url=self.baseurl, realm=realm, id=clientscope_id, client=client_id + ) + try: + return self._request_and_deserialize(client_role_scope_url, method="GET") + except Exception as e: + self.fail_request( + e, + msg=f"Could not fetch client roles from client {client_id} for client-scope {clientscope_id} in realm {realm}: {e}", + ) + def get_client_role_scope_from_client(self, clientid, clientscopeid, realm: str = "master"): """Fetch the roles associated with the client's scope for a specific client on the Keycloak server. :param clientid: ID of the client from which to obtain the associated roles. @@ -3310,6 +3370,94 @@ class KeycloakAPI: return self.get_client_role_scope_from_client(clientid, clientscopeid, realm) + def update_clientscope_scope_mappings_client( + self, payload: list[dict], clientscope_id: str, client_id: str, realm: str = "master" + ): + """Update and fetch the client roles (scope-mappings) associated with the clientscope on the Keycloak server. + :param payload: List of client roles to be added to the scope. + :param clientscope_id: ID of the clientscope to update scope-mappings. + :param clientid: ID of the client from which to obtain the associated roles. + :param realm: Realm from which to obtain the client. + :return: The clientscope client scope-mappings. + """ + client_role_scope_url = URL_CLIENTSCOPE_SCOPE_MAPPINGS_CLIENT.format( + url=self.baseurl, realm=realm, id=clientscope_id, client=client_id + ) + try: + self._request(client_role_scope_url, method="POST", data=json.dumps(payload)) + + except Exception as e: + self.fail_request( + e, + msg=f"Could not update scope mappings for client-scope {client_id}.{clientscope_id} in realm {realm}: {e}", + ) + + return self.get_clientscope_scope_mappings_client(clientscope_id, client_id, realm) + + def update_clientscope_scope_mappings_realm(self, payload: list[dict], clientscope_id: str, realm: str = "master"): + """Update and fetch the realm roles (scope-mappings) associated with the clientscope on the Keycloak server. + :param payload: List of realm roles to be added to the scope. + :param clientscope_id: ID of the clientscope to update scope-mappings. + :param realm: Realm from which to obtain the roles. + :return: The clientscope realm scope-mappings. + """ + client_role_scope_url = URL_CLIENTSCOPE_SCOPE_MAPPINGS_REALM.format( + url=self.baseurl, realm=realm, id=clientscope_id + ) + try: + self._request(client_role_scope_url, method="POST", data=json.dumps(payload)) + + except Exception as e: + self.fail_request( + e, msg=f"Could not update scope mappings for client-scope {clientscope_id} in realm {realm}: {e}" + ) + + return self.get_clientscope_scope_mappings_realm(clientscope_id, realm) + + def delete_clientscope_scope_mappings_client( + self, payload: list[dict], clientscope_id: str, client_id: str, realm: str = "master" + ): + """Delete the client roles (scope_mappings) contained in the payload from the clientscope on the Keycloak server. + :param payload: List of roles to be deleted. + :param clientscope_id: ID of the clientscope to delete roles from scope-mappings. + :param clientid: ID of the client who owns the roles. + :param realm: Realm from which to obtain the client. + :return: The clientscope client scope-mappings. + """ + client_role_scope_url = URL_CLIENTSCOPE_SCOPE_MAPPINGS_CLIENT.format( + url=self.baseurl, realm=realm, id=clientscope_id, client=client_id + ) + try: + self._request(client_role_scope_url, method="DELETE", data=json.dumps(payload)) + + except Exception as e: + self.fail_request( + e, + msg=f"Could not delete scope mappings for client-scope {client_id}.{clientscope_id} in realm {realm}: {e}", + ) + + return self.get_clientscope_scope_mappings_client(clientscope_id, client_id, realm) + + def delete_clientscope_scope_mappings_realm(self, payload: list[dict], clientscope_id: str, realm: str = "master"): + """Delete the realm roles (scope_mappings) contained in the payload from the clientscope on the Keycloak server. + :param payload: List of roles to be deleted. + :param clientscope_id: ID of the clientscope to delete roles from scope-mappings. + :param realm: Realm from which to obtain the roles. + :return: The clientscope realm scope-mappings. + """ + client_role_scope_url = URL_CLIENTSCOPE_SCOPE_MAPPINGS_REALM.format( + url=self.baseurl, realm=realm, id=clientscope_id + ) + try: + self._request(client_role_scope_url, method="DELETE", data=json.dumps(payload)) + + except Exception as e: + self.fail_request( + e, msg=f"Could not delete scope mappings for client-scope {clientscope_id} in realm {realm}: {e}" + ) + + return self.get_clientscope_scope_mappings_realm(clientscope_id, realm) + def get_client_role_scope_from_realm(self, clientid, realm: str = "master"): """Fetch the realm roles from the client's scope on the Keycloak server. :param clientid: ID of the client from which to obtain the associated realm roles. diff --git a/plugins/modules/keycloak_authentication.py b/plugins/modules/keycloak_authentication.py index 4a45ad5..d545261 100644 --- a/plugins/modules/keycloak_authentication.py +++ b/plugins/modules/keycloak_authentication.py @@ -12,6 +12,7 @@ short_description: Configure authentication in Keycloak description: - This module actually can only make a copy of an existing authentication flow, add an execution to it and configure it. - It can also delete the flow. +# Originally added in community.general 3.3.0 version_added: "3.0.0" attributes: @@ -20,6 +21,7 @@ attributes: diff_mode: support: full action_group: + # Originally added in community.general 10.2.0 version_added: "3.0.0" options: diff --git a/plugins/modules/keycloak_authentication_required_actions.py b/plugins/modules/keycloak_authentication_required_actions.py index a333698..ad04583 100644 --- a/plugins/modules/keycloak_authentication_required_actions.py +++ b/plugins/modules/keycloak_authentication_required_actions.py @@ -14,6 +14,7 @@ short_description: Allows administration of Keycloak authentication required act description: - This module can register, update and delete required actions. - It also filters out any duplicate required actions by their alias. The first occurrence is preserved. +# Originally added in community.general 7.1.0 version_added: "3.0.0" attributes: @@ -22,6 +23,7 @@ attributes: diff_mode: support: full action_group: + # Originally added in community.general 10.2.0 version_added: "3.0.0" options: diff --git a/plugins/modules/keycloak_authentication_v2.py b/plugins/modules/keycloak_authentication_v2.py index e6234a1..789c9ae 100644 --- a/plugins/modules/keycloak_authentication_v2.py +++ b/plugins/modules/keycloak_authentication_v2.py @@ -9,6 +9,7 @@ DOCUMENTATION = r""" module: keycloak_authentication_v2 short_description: Configure authentication flows in Keycloak in an idempotent and safe manner. +# Originally added in community.general 12.5.0 version_added: "3.0.0" description: - This module allows the creation, deletion, and modification of Keycloak authentication flows using the Keycloak REST API. diff --git a/plugins/modules/keycloak_authz_authorization_scope.py b/plugins/modules/keycloak_authz_authorization_scope.py index 4c80149..13f00dc 100644 --- a/plugins/modules/keycloak_authz_authorization_scope.py +++ b/plugins/modules/keycloak_authz_authorization_scope.py @@ -11,6 +11,7 @@ module: keycloak_authz_authorization_scope short_description: Allows administration of Keycloak client authorization scopes using Keycloak API +# Originally added in community.general 6.6.0 version_added: "3.0.0" description: @@ -28,6 +29,7 @@ attributes: diff_mode: support: full action_group: + # Originally added in community.general 10.2.0 version_added: "3.0.0" options: diff --git a/plugins/modules/keycloak_authz_custom_policy.py b/plugins/modules/keycloak_authz_custom_policy.py index 1ca179f..a59b14d 100644 --- a/plugins/modules/keycloak_authz_custom_policy.py +++ b/plugins/modules/keycloak_authz_custom_policy.py @@ -11,6 +11,7 @@ module: keycloak_authz_custom_policy short_description: Allows administration of Keycloak client custom Javascript policies using Keycloak API +# Originally added in community.general 7.5.0 version_added: "3.0.0" description: @@ -29,6 +30,7 @@ attributes: diff_mode: support: none action_group: + # Originally added in community.general 10.2.0 version_added: "3.0.0" options: diff --git a/plugins/modules/keycloak_authz_permission.py b/plugins/modules/keycloak_authz_permission.py index 4e94d80..b6d8978 100644 --- a/plugins/modules/keycloak_authz_permission.py +++ b/plugins/modules/keycloak_authz_permission.py @@ -9,6 +9,7 @@ from __future__ import annotations DOCUMENTATION = r""" module: keycloak_authz_permission +# Originally added in community.general 7.2.0 version_added: "3.0.0" short_description: Allows administration of Keycloak client authorization permissions using Keycloak API @@ -34,6 +35,7 @@ attributes: diff_mode: support: none action_group: + # Originally added in community.general 10.2.0 version_added: "3.0.0" options: diff --git a/plugins/modules/keycloak_authz_permission_info.py b/plugins/modules/keycloak_authz_permission_info.py index 43e509f..f2ece6d 100644 --- a/plugins/modules/keycloak_authz_permission_info.py +++ b/plugins/modules/keycloak_authz_permission_info.py @@ -9,6 +9,7 @@ from __future__ import annotations DOCUMENTATION = r""" module: keycloak_authz_permission_info +# Originally added in community.general 7.2.0 version_added: "3.0.0" short_description: Query Keycloak client authorization permissions information @@ -24,6 +25,7 @@ description: U(https://www.puppeteers.net/blog/keycloak-authorization-services-rest-api-paths-and-payload/). attributes: action_group: + # Originally added in community.general 10.2.0 version_added: "3.0.0" options: diff --git a/plugins/modules/keycloak_client.py b/plugins/modules/keycloak_client.py index f475289..9a15881 100644 --- a/plugins/modules/keycloak_client.py +++ b/plugins/modules/keycloak_client.py @@ -30,6 +30,7 @@ attributes: diff_mode: support: full action_group: + # Originally added in community.general 10.2.0 version_added: "3.0.0" options: diff --git a/plugins/modules/keycloak_client_rolemapping.py b/plugins/modules/keycloak_client_rolemapping.py index 999739f..679684f 100644 --- a/plugins/modules/keycloak_client_rolemapping.py +++ b/plugins/modules/keycloak_client_rolemapping.py @@ -10,6 +10,7 @@ module: keycloak_client_rolemapping short_description: Allows administration of Keycloak client_rolemapping with the Keycloak API +# Originally added in community.general 3.5.0 version_added: "3.0.0" description: @@ -30,6 +31,7 @@ attributes: diff_mode: support: full action_group: + # Originally added in community.general 10.2.0 version_added: "3.0.0" options: diff --git a/plugins/modules/keycloak_client_rolescope.py b/plugins/modules/keycloak_client_rolescope.py index 0904730..2ea710a 100644 --- a/plugins/modules/keycloak_client_rolescope.py +++ b/plugins/modules/keycloak_client_rolescope.py @@ -11,6 +11,7 @@ module: keycloak_client_rolescope short_description: Allows administration of Keycloak client roles scope to restrict the usage of certain roles to a other specific client applications +# Originally added in community.general 8.6.0 version_added: "3.0.0" description: @@ -28,6 +29,7 @@ attributes: diff_mode: support: full action_group: + # Originally added in community.general 10.2.0 version_added: "3.0.0" options: diff --git a/plugins/modules/keycloak_client_scope.py b/plugins/modules/keycloak_client_scope.py index 5ec63fd..35723c9 100644 --- a/plugins/modules/keycloak_client_scope.py +++ b/plugins/modules/keycloak_client_scope.py @@ -14,6 +14,7 @@ module: keycloak_client_scope short_description: Allows administration of Keycloak client scopes via Keycloak API +# Originally added in community.general 3.4.0 as keycloak_clientscope version_added: "3.0.0" description: diff --git a/plugins/modules/keycloak_clientscope_rolemappings.py b/plugins/modules/keycloak_clientscope_rolemappings.py new file mode 100644 index 0000000..be89a07 --- /dev/null +++ b/plugins/modules/keycloak_clientscope_rolemappings.py @@ -0,0 +1,282 @@ + +# 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 + +from __future__ import annotations + +DOCUMENTATION = r""" +module: keycloak_clientscope_rolemappings + +short_description: Allows administration of Keycloak clientscope scope mappings to restrict the usage of certain roles to + specific clientscopes + +# Originally added in community.general 13.1.0 +version_added: "3.0.0" + +description: + - This module allows you to add or remove Keycloak roles from clientscopes using the Keycloak REST API. It requires access + to the REST API using OpenID Connect; the user connecting and the client being used must have the requisite access rights. + In a default Keycloak installation, C(admin-cli) and an admin user would work, as would a separate client definition with + the scope tailored to your needs and a user having the expected roles. + - Attributes are multi-valued in the Keycloak API. All attributes are lists of individual values and are returned that way + by this module. You may pass single values for attributes when calling the module, and this is translated into a list + suitable for the API. +attributes: + check_mode: + support: full + diff_mode: + support: full + action_group: + # Originally added in community.general 13.1.0 + version_added: "3.0.0" + +options: + state: + description: + - State of the role mapping. + - On V(present), all roles in O(role_names) are mapped if not exist yet. + - On V(absent), all roles mapping in O(role_names) are removed if they exist. + default: 'present' + type: str + choices: + - present + - absent + + realm: + type: str + description: + - The Keycloak realm under which clients resides. + default: 'master' + + clientscope_id: + required: true + type: str + description: + - Roles provided in O(role_names) will be added to this clientscope. + + client_id: + type: str + description: + - If the O(role_names) are client roles, the client ID under which it resides. + - If this parameter is absent, the roles are considered realm roles. + + role_names: + required: true + type: list + elements: str + description: + - Names of roles to add. + - If O(client_id) is present, all roles must be under this client. + - If O(client_id) is absent, all roles must be under the realm. + +extends_documentation_fragment: + - middleware_automation.keycloak.keycloak + - middleware_automation.keycloak.actiongroup_keycloak + - middleware_automation.keycloak.attributes + +author: + - Felix Grzelka (@felix-grzelka) + # This module was adapted from keycloak_client_rolescope, which was written by Andre Desrosiers (@desand01). +""" + +EXAMPLES = r""" +- name: Add roles to clientscope + middleware_automation.keycloak.keycloak_clientscope_rolemappings: + auth_keycloak_url: https://auth.example.com + auth_realm: master + auth_username: USERNAME + auth_password: PASSWORD + realm: MyCustomRealm + client_id: frontend-client-public + clientscope_id: frontend-clientscope + role_names: + - backend-role-admin + - backend-role-user + +- name: Remove roles from clientscope + middleware_automation.keycloak.keycloak_clientscope_rolemappings: + auth_keycloak_url: https://auth.example.com + auth_realm: master + auth_username: USERNAME + auth_password: PASSWORD + realm: MyCustomRealm + client_id: frontend-client-public + clientscope_id: frontend-clientscope + role_names: + - backend-role-admin + state: absent + +- name: Add realm roles to clientscope + middleware_automation.keycloak.keycloak_clientscope_rolemappings: + auth_keycloak_url: https://auth.example.com + auth_realm: master + auth_username: USERNAME + auth_password: PASSWORD + realm: MyCustomRealm + clientscope_id: frontend-clientscope + role_names: + - realm-role-admin + - realm-role-user +""" + +RETURN = r""" +end_state: + description: Representation of clientscope scope mappings after module execution. + returned: on success + type: list + elements: dict + sample: + [ + { + "clientRole": false, + "composite": false, + "containerId": "77f9bd4e-13a6-451e-9c72-ee6997299c1f", + "description": "User role", + "id": "9e155ef7-86f5-4def-b507-581ce7b87013", + "name": "realm-role-user" + }, + { + "clientRole": false, + "composite": false, + "containerId": "77f9bd4e-13a6-451e-9c72-ee6997299c1f", + "description": "Admin role", + "id": "9e155ef7-86f5-4def-b507-581ce7b87013", + "name": "realm-role-admin" + } + ] +""" + +import copy + +from ansible.module_utils.basic import AnsibleModule + +from ansible_collections.middleware_automation.keycloak.plugins.module_utils.identity.keycloak.keycloak import ( + KeycloakAPI, + KeycloakError, + get_token, + keycloak_argument_spec, +) + + +def main(): + argument_spec = keycloak_argument_spec() + + meta_args = dict( + client_id=dict(type="str"), + clientscope_id=dict(type="str", required=True), + realm=dict(type="str", default="master"), + role_names=dict(type="list", elements="str", required=True), + state=dict(type="str", default="present", choices=["present", "absent"]), + ) + + argument_spec.update(meta_args) + + module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True) + + result = dict(changed=False, msg="", diff={}, end_state={}) + + # Obtain access token, initialize API + try: + connection_header = get_token(module.params) + except KeycloakError as e: + module.fail_json(msg=str(e)) + + kc = KeycloakAPI(module, connection_header) + + realm = module.params["realm"] + client_id = module.params["client_id"] + clientscope_id = module.params["clientscope_id"] + role_names = module.params["role_names"] + state = module.params["state"] + + realm_object = kc.get_realm_by_id(realm) + if not realm_object: + module.fail_json(msg=f"Failed to retrieve realm '{realm}'") + + clientscope_object = kc.get_clientscope_by_name(clientscope_id, realm) + if not clientscope_object: + module.fail_json(msg=f"Failed to retrieve client-scope '{clientscope_id}'") + + if client_id: + # add client role + client_object = kc.get_client_by_clientid(client_id, realm) + if not client_object: + module.fail_json(msg=f"Failed to retrieve client '{realm}.{client_id}'") + if client_object["fullScopeAllowed"] and state == "present": + module.fail_json(msg=f"FullScopeAllowed is active for Client '{realm}.{client_id}'") + + before_roles = kc.get_clientscope_scope_mappings_client(clientscope_object["id"], client_object["id"], realm) + available_roles_by_name = kc.get_client_roles_by_id(client_object["id"], realm) + else: + # add realm role + before_roles = kc.get_clientscope_scope_mappings_realm(clientscope_object["id"], realm) + available_roles_by_name = kc.get_realm_roles(realm) + + # convert to indexed Dict by name + available_roles_by_name = {role["name"]: role for role in available_roles_by_name} + before_roles_by_name = {role["name"]: role for role in before_roles} + desired_roles = copy.deepcopy(before_roles) + changed_roles = [] + + if state == "present": + # update desired + for role_name in role_names: + if role_name not in available_roles_by_name: + if client_id: + module.fail_json(msg=f"Failed to retrieve role '{realm}.{client_id}.{role_name}'") + else: + module.fail_json(msg=f"Failed to retrieve role '{realm}.{role_name}'") + if role_name not in before_roles_by_name: + changed_roles.append(available_roles_by_name[role_name]) + desired_roles.append(available_roles_by_name[role_name]) + else: + # remove role if present + for role_name in role_names: + if role_name in before_roles_by_name: + changed_roles.append(before_roles_by_name[role_name]) + desired_roles.remove(available_roles_by_name[role_name]) + + before_roles = sorted(before_roles, key=lambda d: d["name"]) + desired_role_mapping = sorted(desired_roles, key=lambda d: d["name"]) + + result["changed"] = bool(changed_roles) + + if module._diff: + result["diff"] = dict(before={"roles": before_roles}, after={"roles": desired_role_mapping}) + + if not result["changed"]: + # no changes + result["end_state"] = before_roles + result["msg"] = f"No changes required for clientscope {clientscope_id}." + elif state == "present": + # doing update + if module.check_mode: + result["end_state"] = desired_role_mapping + elif client_id: + result["end_state"] = kc.update_clientscope_scope_mappings_client( + changed_roles, clientscope_object["id"], client_object["id"], realm + ) + else: + result["end_state"] = kc.update_clientscope_scope_mappings_realm( + changed_roles, clientscope_object["id"], realm + ) + result["msg"] = f"Clientscope scope mappings for {clientscope_id} have been updated" + else: + # doing delete + if module.check_mode: + result["end_state"] = desired_role_mapping + elif client_id: + result["end_state"] = kc.delete_clientscope_scope_mappings_client( + changed_roles, clientscope_object["id"], client_object["id"], realm + ) + else: + result["end_state"] = kc.delete_clientscope_scope_mappings_realm( + changed_roles, clientscope_object["id"], realm + ) + result["msg"] = f"Clientscope scope mappings for {clientscope_id} have been deleted" + module.exit_json(**result) + + +if __name__ == "__main__": + main() diff --git a/plugins/modules/keycloak_clientscope_type.py b/plugins/modules/keycloak_clientscope_type.py index f9cd07b..6c467a3 100644 --- a/plugins/modules/keycloak_clientscope_type.py +++ b/plugins/modules/keycloak_clientscope_type.py @@ -10,6 +10,7 @@ module: keycloak_clientscope_type short_description: Set the type of aclientscope in realm or client using Keycloak API +# Originally added in community.general 6.6.0 version_added: "3.0.0" description: @@ -23,6 +24,7 @@ attributes: diff_mode: support: full action_group: + # Originally added in community.general 10.2.0 version_added: "3.0.0" options: diff --git a/plugins/modules/keycloak_clientsecret_info.py b/plugins/modules/keycloak_clientsecret_info.py index a598fe7..d92f469 100644 --- a/plugins/modules/keycloak_clientsecret_info.py +++ b/plugins/modules/keycloak_clientsecret_info.py @@ -10,6 +10,7 @@ module: keycloak_clientsecret_info short_description: Retrieve client secret using Keycloak API +# Originally added in community.general 6.1.0 version_added: "3.0.0" description: @@ -23,6 +24,7 @@ description: the task.' attributes: action_group: + # Originally added in community.general 10.2.0 version_added: "3.0.0" options: diff --git a/plugins/modules/keycloak_clientsecret_regenerate.py b/plugins/modules/keycloak_clientsecret_regenerate.py index 4b96956..5e8b86b 100644 --- a/plugins/modules/keycloak_clientsecret_regenerate.py +++ b/plugins/modules/keycloak_clientsecret_regenerate.py @@ -10,6 +10,7 @@ module: keycloak_clientsecret_regenerate short_description: Regenerate Keycloak client secret using Keycloak API +# Originally added in community.general 6.1.0 version_added: "3.0.0" description: @@ -27,6 +28,7 @@ attributes: diff_mode: support: none action_group: + # Originally added in community.general 10.2.0 version_added: "3.0.0" options: diff --git a/plugins/modules/keycloak_clienttemplate.py b/plugins/modules/keycloak_clienttemplate.py index a764973..11334a3 100644 --- a/plugins/modules/keycloak_clienttemplate.py +++ b/plugins/modules/keycloak_clienttemplate.py @@ -28,6 +28,7 @@ attributes: diff_mode: support: full action_group: + # Originally added in community.general 10.2.0 version_added: "3.0.0" options: diff --git a/plugins/modules/keycloak_component.py b/plugins/modules/keycloak_component.py index 0993fc8..8176044 100644 --- a/plugins/modules/keycloak_component.py +++ b/plugins/modules/keycloak_component.py @@ -10,6 +10,7 @@ module: keycloak_component short_description: Allows administration of Keycloak components using Keycloak API +# Originally added in community.general 10.0.0 version_added: "3.0.0" description: @@ -26,6 +27,7 @@ attributes: diff_mode: support: full action_group: + # Originally added in community.general 10.2.0 version_added: "3.0.0" options: diff --git a/plugins/modules/keycloak_component_info.py b/plugins/modules/keycloak_component_info.py index a08c8fb..3934513 100644 --- a/plugins/modules/keycloak_component_info.py +++ b/plugins/modules/keycloak_component_info.py @@ -10,12 +10,14 @@ module: keycloak_component_info short_description: Retrieve component info in Keycloak +# Originally added in community.general 8.2.0 version_added: "3.0.0" description: - This module retrieve information on component from Keycloak. attributes: action_group: + # Originally added in community.general 10.2.0 version_added: "3.0.0" options: diff --git a/plugins/modules/keycloak_group.py b/plugins/modules/keycloak_group.py index bcf67bd..4f3a7e7 100644 --- a/plugins/modules/keycloak_group.py +++ b/plugins/modules/keycloak_group.py @@ -30,6 +30,7 @@ attributes: diff_mode: support: full action_group: + # Originally added in community.general 10.2.0 version_added: "3.0.0" options: diff --git a/plugins/modules/keycloak_identity_provider.py b/plugins/modules/keycloak_identity_provider.py index a7052c9..96eeb10 100644 --- a/plugins/modules/keycloak_identity_provider.py +++ b/plugins/modules/keycloak_identity_provider.py @@ -10,6 +10,7 @@ module: keycloak_identity_provider short_description: Allows administration of Keycloak identity providers using Keycloak API +# Originally added in community.general 3.6.0 version_added: "3.0.0" description: @@ -25,6 +26,7 @@ attributes: diff_mode: support: full action_group: + # Originally added in community.general 10.2.0 version_added: "3.0.0" options: diff --git a/plugins/modules/keycloak_realm.py b/plugins/modules/keycloak_realm.py index 2799aa4..9eb4070 100644 --- a/plugins/modules/keycloak_realm.py +++ b/plugins/modules/keycloak_realm.py @@ -11,6 +11,7 @@ module: keycloak_realm short_description: Allows administration of Keycloak realm using Keycloak API +# Originally added in community.general 3.0.0 version_added: "3.0.0" description: @@ -30,6 +31,7 @@ attributes: diff_mode: support: full action_group: + # Originally added in community.general 10.2.0 version_added: "3.0.0" options: diff --git a/plugins/modules/keycloak_realm_info.py b/plugins/modules/keycloak_realm_info.py index 2aaf2a3..4643283 100644 --- a/plugins/modules/keycloak_realm_info.py +++ b/plugins/modules/keycloak_realm_info.py @@ -10,6 +10,7 @@ module: keycloak_realm_info short_description: Allows obtaining Keycloak realm public information using Keycloak API +# Originally added in community.general 4.3.0 version_added: "3.0.0" description: diff --git a/plugins/modules/keycloak_realm_key.py b/plugins/modules/keycloak_realm_key.py index 297e5f5..8c9500a 100644 --- a/plugins/modules/keycloak_realm_key.py +++ b/plugins/modules/keycloak_realm_key.py @@ -11,6 +11,7 @@ module: keycloak_realm_key short_description: Allows administration of Keycloak realm keys using Keycloak API +# Originally added in community.general 7.5.0 version_added: "3.0.0" description: @@ -31,6 +32,7 @@ attributes: diff_mode: support: partial action_group: + # Originally added in community.general 10.2.0 version_added: "3.0.0" options: diff --git a/plugins/modules/keycloak_realm_keys_metadata_info.py b/plugins/modules/keycloak_realm_keys_metadata_info.py index 4c110e8..aafb478 100644 --- a/plugins/modules/keycloak_realm_keys_metadata_info.py +++ b/plugins/modules/keycloak_realm_keys_metadata_info.py @@ -10,6 +10,7 @@ module: keycloak_realm_keys_metadata_info short_description: Allows obtaining Keycloak realm keys metadata using Keycloak API +# Originally added in community.general 9.3.0 version_added: "3.0.0" description: @@ -18,6 +19,7 @@ description: at U(https://www.keycloak.org/docs-api/latest/rest-api/index.html). attributes: action_group: + # Originally added in community.general 10.2.0 version_added: "3.0.0" options: diff --git a/plugins/modules/keycloak_realm_localization.py b/plugins/modules/keycloak_realm_localization.py index b380590..45fb509 100644 --- a/plugins/modules/keycloak_realm_localization.py +++ b/plugins/modules/keycloak_realm_localization.py @@ -11,6 +11,7 @@ module: keycloak_realm_localization short_description: Allows management of Keycloak realm localization overrides via the Keycloak API +# Originally added in community.general 12.4.0 version_added: "3.0.0" description: diff --git a/plugins/modules/keycloak_realm_rolemapping.py b/plugins/modules/keycloak_realm_rolemapping.py index 473aca1..1f3a544 100644 --- a/plugins/modules/keycloak_realm_rolemapping.py +++ b/plugins/modules/keycloak_realm_rolemapping.py @@ -10,6 +10,7 @@ module: keycloak_realm_rolemapping short_description: Allows administration of Keycloak realm role mappings into groups with the Keycloak API +# Originally added in community.general 8.2.0 version_added: "3.0.0" description: @@ -30,6 +31,7 @@ attributes: diff_mode: support: full action_group: + # Originally added in community.general 10.2.0 version_added: "3.0.0" options: diff --git a/plugins/modules/keycloak_role.py b/plugins/modules/keycloak_role.py index 114c650..9787586 100644 --- a/plugins/modules/keycloak_role.py +++ b/plugins/modules/keycloak_role.py @@ -10,6 +10,7 @@ module: keycloak_role short_description: Allows administration of Keycloak roles using Keycloak API +# Originally added in community.general 3.4.0 version_added: "3.0.0" description: @@ -28,6 +29,7 @@ attributes: diff_mode: support: full action_group: + # Originally added in community.general 10.2.0 version_added: "3.0.0" options: diff --git a/plugins/modules/keycloak_user.py b/plugins/modules/keycloak_user.py index 67dcc04..b713bf9 100644 --- a/plugins/modules/keycloak_user.py +++ b/plugins/modules/keycloak_user.py @@ -10,6 +10,7 @@ module: keycloak_user short_description: Create and configure a user in Keycloak description: - This module creates, removes, or updates Keycloak users. +# Originally added in community.general 7.1.0 version_added: "3.0.0" options: auth_username: @@ -34,8 +35,9 @@ options: type: bool email_verified: description: - - Check the validity of user email. - default: false + - Set or reset the C(emailVerified) flag of the user. + - When O(email_verified_behavior=no_defaults), the default value of this option becomes C(null) and + that causes the module not to change any existing value for that attribute. type: bool aliases: - emailVerified @@ -133,8 +135,7 @@ options: default: false required_actions: description: - - RequiredActions user Auth. - default: [] + - Set or reset a user's required actions. type: list elements: str aliases: @@ -199,6 +200,20 @@ options: - If V(true), allows to remove user and recreate it. type: bool default: false + email_verified_behavior: + description: + - The O(email_verified) option used to have a default value. This caused problems when the + user expects different behavior from keycloak by default. + - The default value of this option is V(compatibility), which will ensure that the old default value + for O(email_verified) is used. + - When set to V(no_defaults), the module will not change existing values of O(email_verified) if no value is specified. + type: str + choices: + - compatibility + - no_defaults + default: compatibility + # Originally added in community.general 13.1.0 + version_added: "3.0.0" extends_documentation_fragment: - middleware_automation.keycloak.keycloak - middleware_automation.keycloak.actiongroup_keycloak @@ -209,6 +224,7 @@ attributes: diff_mode: support: full action_group: + # Originally added in community.general 10.2.0 version_added: "3.0.0" notes: - The module does not modify the user ID of an existing user. @@ -378,7 +394,7 @@ def main(): last_name=dict(type="str", aliases=["lastName"]), email=dict(type="str"), enabled=dict(type="bool"), - email_verified=dict(type="bool", default=False, aliases=["emailVerified"]), + email_verified=dict(type="bool", aliases=["emailVerified"]), federation_link=dict(type="str", aliases=["federationLink"]), service_account_client_id=dict(type="str", aliases=["serviceAccountClientId"]), attributes=dict(type="list", elements="dict", options=attributes_spec), @@ -387,7 +403,7 @@ def main(): disableable_credential_types=dict( type="list", default=[], aliases=["disableableCredentialTypes"], elements="str" ), - required_actions=dict(type="list", default=[], aliases=["requiredActions"], elements="str"), + required_actions=dict(type="list", aliases=["requiredActions"], elements="str"), credentials=dict(type="list", default=[], elements="dict", options=credential_spec), federated_identities=dict(type="list", default=[], aliases=["federatedIdentities"], elements="str"), client_consents=dict( @@ -396,6 +412,7 @@ def main(): origin=dict(type="str"), state=dict(choices=["absent", "present"], default="present"), force=dict(type="bool", default=False), + email_verified_behavior=dict(type="str", choices=["compatibility", "no_defaults"], default="compatibility"), ) argument_spec.update(meta_args) @@ -425,14 +442,21 @@ def main(): username = module.params.get("username") groups = module.params.get("groups") - # Filter and map the parameters names that apply to the user - user_params = [ - x - for x in module.params - if x not in list(keycloak_argument_spec().keys()) + ["state", "realm", "force", "groups"] - and module.params.get(x) is not None + # If there is no value for email_verified, check if we should to set the old default + if module.params["email_verified"] is None and module.params["email_verified_behavior"] == "compatibility": + module.params["email_verified"] = False + + ignored_arguments = list(keycloak_argument_spec().keys()) + [ + "state", + "realm", + "force", + "groups", + "email_verified_behavior", ] + # Filter and map the parameters names that apply to the user + user_params = [x for x in module.params if x not in ignored_arguments and module.params[x] is not None] + before_user = kc.get_user_by_username(username=username, realm=realm) if before_user is None: @@ -463,97 +487,109 @@ def main(): desired_user = copy.deepcopy(before_user) desired_user.update(changeset) + if before_user: + before_groups = kc.get_user_groups(user_id=before_user["id"], realm=realm) + before_user["groups"] = before_groups + else: + before_groups = [] + result["proposed"] = changeset result["existing"] = before_user # Default values for user_created result["user_created"] = False changed = False + after_user = {} + + user_compare_excludes = [ + "access", + "notBefore", + "createdTimestamp", + "totp", + "credentials", + "disableableCredentialTypes", + "groups", + "clientConsents", + "federatedIdentities", + ] - # Cater for when it doesn't exist (an empty dict) if state == "absent": if not before_user: # Do nothing and exit - if module._diff: - result["diff"] = dict(before="", after="") - result["changed"] = False - result["end_state"] = {} - result["msg"] = "Role does not exist, doing nothing." - module.exit_json(**result) + result["msg"] = "User does not exist, doing nothing." else: # Delete user - kc.delete_user(user_id=before_user["id"], realm=realm) + if not module.check_mode: + kc.delete_user(user_id=before_user["id"], realm=realm) result["msg"] = f"User {before_user['username']} deleted" changed = True - else: - after_user = {} - if force and before_user: # If the force option is set to true + if (not before_user or force) and username is None: + module.fail_json(msg="username must be specified when creating a new user") + + if force and before_user and not module.check_mode: # If the force option is set to true # Delete the existing user kc.delete_user(user_id=before_user["id"], realm=realm) if not before_user or force: - # Process a creation - changed = True + # Create a new user + if not module.check_mode: + # Create the user + after_user = kc.create_user(userrep=desired_user, realm=realm) + if after_user is None: + module.fail_json( + msg=f"User {desired_user['username']} was created in realm {realm} but could not be retrieved", + ) + # Add user ID to desired_user for group updates + desired_user["id"] = after_user["id"] + else: + after_user = desired_user - if username is None: - module.fail_json(msg="username must be specified when creating a new user") - - if module._diff: - result["diff"] = dict(before="", after=desired_user) - - if module.check_mode: - # Set user_created flag explicit for check_mode - # create_user could have failed, but we don't know for sure until we try to create the user.' - result["user_created"] = True - module.exit_json(**result) - - # Create the user - after_user = kc.create_user(userrep=desired_user, realm=realm) - if after_user is None: - module.fail_json( - msg=f"User {desired_user['username']} was created in realm {realm} but could not be retrieved", - ) result["msg"] = f"User {desired_user['username']} created" - # Add user ID to new representation - desired_user["id"] = after_user["id"] - # Set user_created flag result["user_created"] = True + changed = True else: - excludes = [ - "access", - "notBefore", - "createdTimestamp", - "totp", - "credentials", - "disableableCredentialTypes", - "groups", - "clientConsents", - "federatedIdentities", - "requiredActions", - ] + # Update an existing user # Add user ID to new representation desired_user["id"] = before_user["id"] # Compare users if not ( - is_struct_included(desired_user, before_user, excludes) - ): # If the new user does not introduce a change to the existing user + is_struct_included(desired_user, before_user, user_compare_excludes, empty_list_result=False) + ): # If the new user introduces a change to the existing user # Update the user - after_user = kc.update_user(userrep=desired_user, realm=realm) + if not module.check_mode: + after_user = kc.update_user(userrep=desired_user, realm=realm) changed = True + if not after_user: + # no change + after_user = desired_user + # set user groups - if kc.update_user_groups_membership(userrep=desired_user, groups=groups, realm=realm): - changed = True - # Get the user groups - after_user["groups"] = kc.get_user_groups(user_id=desired_user["id"], realm=realm) - result["end_state"] = after_user + if not module.check_mode: + changed |= kc.update_user_groups_membership(userrep=desired_user, groups=groups, realm=realm) + + present_groups = [g["name"] for g in groups if g["state"] == "present"] + absent_groups = [g["name"] for g in groups if g["state"] == "absent"] + + desired_user["groups"] = (set(before_groups) | set(present_groups)) - set(absent_groups) + + if module.check_mode: + # check if group meberships would have changed + changed |= not is_struct_included( + desired_user["groups"], before_groups, user_compare_excludes, empty_list_result=False + ) + else: + after_user["groups"] = kc.get_user_groups(user_id=desired_user["id"], realm=realm) + + if not result["msg"]: if changed: result["msg"] = f"User {desired_user['username']} updated" else: result["msg"] = f"No changes made for user {desired_user['username']}" - + result["end_state"] = after_user result["changed"] = changed + result["diff"] = dict(before=before_user, after=after_user) module.exit_json(**result) diff --git a/plugins/modules/keycloak_user_execute_actions_email.py b/plugins/modules/keycloak_user_execute_actions_email.py index 9a8765a..ef18fac 100644 --- a/plugins/modules/keycloak_user_execute_actions_email.py +++ b/plugins/modules/keycloak_user_execute_actions_email.py @@ -10,6 +10,7 @@ module: keycloak_user_execute_actions_email short_description: Send a Keycloak execute-actions email to a user +# Originally added in community.general 12.0.0 version_added: "3.0.0" description: diff --git a/plugins/modules/keycloak_user_federation.py b/plugins/modules/keycloak_user_federation.py index 8fbc7d1..ea8fb66 100644 --- a/plugins/modules/keycloak_user_federation.py +++ b/plugins/modules/keycloak_user_federation.py @@ -10,6 +10,7 @@ module: keycloak_user_federation short_description: Allows administration of Keycloak user federations using Keycloak API +# Originally added in community.general 3.7.0 version_added: "3.0.0" description: @@ -25,6 +26,7 @@ attributes: diff_mode: support: full action_group: + # Originally added in community.general 10.2.0 version_added: "3.0.0" options: diff --git a/plugins/modules/keycloak_user_rolemapping.py b/plugins/modules/keycloak_user_rolemapping.py index b4ee83c..cc20f42 100644 --- a/plugins/modules/keycloak_user_rolemapping.py +++ b/plugins/modules/keycloak_user_rolemapping.py @@ -9,6 +9,7 @@ module: keycloak_user_rolemapping short_description: Allows administration of Keycloak user_rolemapping with the Keycloak API +# Originally added in community.general 5.7.0 version_added: "3.0.0" description: @@ -29,6 +30,7 @@ attributes: diff_mode: support: full action_group: + # Originally added in community.general 10.2.0 version_added: "3.0.0" options: diff --git a/plugins/modules/keycloak_userprofile.py b/plugins/modules/keycloak_userprofile.py index d427db9..799e21d 100644 --- a/plugins/modules/keycloak_userprofile.py +++ b/plugins/modules/keycloak_userprofile.py @@ -16,6 +16,7 @@ description: - The names of module options are snake_cased versions of the camelCase ones found in the Keycloak API and its documentation at U(https://www.keycloak.org/docs-api/24.0.5/rest-api/index.html). For compatibility reasons, the module also accepts the camelCase versions of the options. +# Originally added in community.general 9.4.0 version_added: "3.0.0" attributes: @@ -24,6 +25,7 @@ attributes: diff_mode: support: full action_group: + # Originally added in community.general 10.2.0 version_added: "3.0.0" options: