Added PRs #11841 and #11749, and updated version references from community.general to Keycloak.

This commit is contained in:
Harsha Cherukuri
2026-05-30 09:16:56 -04:00
parent bdc090de64
commit 123906d739
36 changed files with 721 additions and 80 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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