Move community.general keycloak modules into keycloak collection

This commit is contained in:
Harsha Cherukuri
2026-05-27 14:47:21 -04:00
parent 7495385ccb
commit bdc090de64
45 changed files with 16390 additions and 3764 deletions

View File

@@ -1,135 +1,124 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Adam Goossens <adam.goossens@gmail.com>
# 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 absolute_import, division, print_function
__metaclass__ = type
from __future__ import annotations
DOCUMENTATION = '''
---
DOCUMENTATION = r"""
module: keycloak_role
short_description: Allows administration of Keycloak roles via Keycloak API
short_description: Allows administration of Keycloak roles using Keycloak API
version_added: 3.4.0
version_added: "3.0.0"
description:
- This module allows you to add, remove or modify Keycloak roles via the Keycloak REST API.
It requires access to the REST API via OpenID Connect; the user connecting and the client being
used must have the requisite access rights. In a default Keycloak installation, 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.
- 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/8.0/rest-api/index.html).
- Attributes are multi-valued in the Keycloak API. All attributes are lists of individual values and will
be returned that way by this module. You may pass single values for attributes when calling the module,
and this will be translated into a list suitable for the API.
- This module allows you to add, remove or modify Keycloak roles 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, 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.
- 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/latest/rest-api/index.html).
- 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
check_mode:
support: full
diff_mode:
support: full
action_group:
version_added: "3.0.0"
options:
state:
description:
- State of the role.
- On V(present), the role will be created if it does not yet exist, or updated with the parameters you provide.
- On V(absent), the role will be removed if it exists.
default: 'present'
type: str
choices:
- present
- absent
state:
description:
- State of the role.
- On V(present), the role is created if it does not yet exist, or updated with the parameters you provide.
- On V(absent), the role is removed if it exists.
default: 'present'
type: str
choices:
- present
- absent
name:
name:
type: str
required: true
description:
- Name of the role.
- This parameter is required.
description:
type: str
description:
- The role description.
realm:
type: str
description:
- The Keycloak realm under which this role resides.
default: 'master'
client_id:
type: str
description:
- If the role is a client role, the client ID under which it resides.
- If this parameter is absent, the role is considered a realm role.
attributes:
type: dict
description:
- A dict of key/value pairs to set as custom attributes for the role.
- Values may be single values (for example a string) or a list of strings.
composite:
description:
- If V(true), the role is a composition of other realm and/or client role.
default: false
type: bool
composites:
description:
- List of roles to include to the composite realm role.
- If the composite role is a client role, the C(clientId) (not ID of the client) must be specified.
default: []
type: list
elements: dict
suboptions:
name:
description:
- Name of the role. This can be the name of a REALM role or a client role.
type: str
required: true
client_id:
description:
- Name of the role.
- This parameter is required.
description:
- Client ID if the role is a client role. Do not include this option for a REALM role.
- Use the client ID you can see in the Keycloak console, not the technical ID of the client.
type: str
aliases:
- clientId
state:
description:
- The role description.
realm:
- Create the composite if present, remove it if absent.
type: str
description:
- The Keycloak realm under which this role resides.
default: 'master'
client_id:
type: str
description:
- If the role is a client role, the client id under which it resides.
- If this parameter is absent, the role is considered a realm role.
attributes:
type: dict
description:
- A dict of key/value pairs to set as custom attributes for the role.
- Values may be single values (e.g. a string) or a list of strings.
composite:
description:
- If V(true), the role is a composition of other realm and/or client role.
default: false
type: bool
version_added: 7.1.0
composites:
description:
- List of roles to include to the composite realm role.
- If the composite role is a client role, the C(clientId) (not ID of the client) must be specified.
default: []
type: list
elements: dict
version_added: 7.1.0
suboptions:
name:
description:
- Name of the role. This can be the name of a REALM role or a client role.
type: str
required: true
client_id:
description:
- Client ID if the role is a client role. Do not include this option for a REALM role.
- Use the client ID you can see in the Keycloak console, not the technical ID of the client.
type: str
required: false
aliases:
- clientId
state:
description:
- Create the composite if present, remove it if absent.
type: str
choices:
- present
- absent
default: present
choices:
- present
- absent
default: present
extends_documentation_fragment:
- middleware_automation.keycloak.keycloak
- middleware_automation.keycloak.attributes
- middleware_automation.keycloak.keycloak
- middleware_automation.keycloak.actiongroup_keycloak
- middleware_automation.keycloak.attributes
author:
- Laurent Paumier (@laurpaum)
'''
- Laurent Paumier (@laurpaum)
"""
EXAMPLES = '''
EXAMPLES = r"""
- name: Create a Keycloak realm role, authentication with credentials
middleware_automation.keycloak.keycloak_role:
name: my-new-kc-role
realm: MyCustomRealm
state: present
auth_client_id: admin-cli
auth_keycloak_url: https://auth.example.com/auth
auth_keycloak_url: https://auth.example.com
auth_realm: master
auth_username: USERNAME
auth_password: PASSWORD
@@ -141,7 +130,7 @@ EXAMPLES = '''
realm: MyCustomRealm
state: present
auth_client_id: admin-cli
auth_keycloak_url: https://auth.example.com/auth
auth_keycloak_url: https://auth.example.com
token: TOKEN
delegate_to: localhost
@@ -152,7 +141,7 @@ EXAMPLES = '''
client_id: MyClient
state: present
auth_client_id: admin-cli
auth_keycloak_url: https://auth.example.com/auth
auth_keycloak_url: https://auth.example.com
auth_realm: master
auth_username: USERNAME
auth_password: PASSWORD
@@ -163,7 +152,7 @@ EXAMPLES = '''
name: my-role-for-deletion
state: absent
auth_client_id: admin-cli
auth_keycloak_url: https://auth.example.com/auth
auth_keycloak_url: https://auth.example.com
auth_realm: master
auth_username: USERNAME
auth_password: PASSWORD
@@ -172,72 +161,80 @@ EXAMPLES = '''
- name: Create a keycloak role with some custom attributes
middleware_automation.keycloak.keycloak_role:
auth_client_id: admin-cli
auth_keycloak_url: https://auth.example.com/auth
auth_keycloak_url: https://auth.example.com
auth_realm: master
auth_username: USERNAME
auth_password: PASSWORD
name: my-new-role
attributes:
attrib1: value1
attrib2: value2
attrib3:
- with
- numerous
- individual
- list
- items
attrib1: value1
attrib2: value2
attrib3:
- with
- numerous
- individual
- list
- items
delegate_to: localhost
'''
"""
RETURN = '''
RETURN = r"""
msg:
description: Message as to what action was taken.
returned: always
type: str
sample: "Role myrole has been updated"
description: Message as to what action was taken.
returned: always
type: str
sample: "Role myrole has been updated"
proposed:
description: Representation of proposed role.
returned: always
type: dict
sample: {
"description": "My updated test description"
}
description: Representation of proposed role.
returned: always
type: dict
sample: {"description": "My updated test description"}
existing:
description: Representation of existing role.
returned: always
type: dict
sample: {
"attributes": {},
"clientRole": true,
"composite": false,
"containerId": "9f03eb61-a826-4771-a9fd-930e06d2d36a",
"description": "My client test role",
"id": "561703dd-0f38-45ff-9a5a-0c978f794547",
"name": "myrole"
description: Representation of existing role.
returned: always
type: dict
sample:
{
"attributes": {},
"clientRole": true,
"composite": false,
"containerId": "9f03eb61-a826-4771-a9fd-930e06d2d36a",
"description": "My client test role",
"id": "561703dd-0f38-45ff-9a5a-0c978f794547",
"name": "myrole"
}
end_state:
description: Representation of role after module execution (sample is truncated).
returned: on success
type: dict
sample: {
"attributes": {},
"clientRole": true,
"composite": false,
"containerId": "9f03eb61-a826-4771-a9fd-930e06d2d36a",
"description": "My updated client test role",
"id": "561703dd-0f38-45ff-9a5a-0c978f794547",
"name": "myrole"
description: Representation of role after module execution (sample is truncated).
returned: on success
type: dict
sample:
{
"attributes": {},
"clientRole": true,
"composite": false,
"containerId": "9f03eb61-a826-4771-a9fd-930e06d2d36a",
"description": "My updated client test role",
"id": "561703dd-0f38-45ff-9a5a-0c978f794547",
"name": "myrole"
}
'''
"""
from ansible_collections.middleware_automation.keycloak.plugins.module_utils.identity.keycloak.keycloak import KeycloakAPI, camel, \
keycloak_argument_spec, get_token, KeycloakError, is_struct_included
from ansible.module_utils.basic import AnsibleModule
import copy
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.middleware_automation.keycloak.plugins.module_utils.identity.keycloak.keycloak import (
KeycloakAPI,
KeycloakError,
camel,
get_token,
is_struct_included,
keycloak_argument_spec,
)
def main():
"""
@@ -248,30 +245,35 @@ def main():
argument_spec = keycloak_argument_spec()
composites_spec = dict(
name=dict(type='str', required=True),
client_id=dict(type='str', aliases=['clientId'], required=False),
state=dict(type='str', default='present', choices=['present', 'absent'])
name=dict(type="str", required=True),
client_id=dict(type="str", aliases=["clientId"]),
state=dict(type="str", default="present", choices=["present", "absent"]),
)
meta_args = dict(
state=dict(type='str', default='present', choices=['present', 'absent']),
name=dict(type='str', required=True),
description=dict(type='str'),
realm=dict(type='str', default='master'),
client_id=dict(type='str'),
attributes=dict(type='dict'),
composites=dict(type='list', default=[], options=composites_spec, elements='dict'),
composite=dict(type='bool', default=False),
state=dict(type="str", default="present", choices=["present", "absent"]),
name=dict(type="str", required=True),
description=dict(type="str"),
realm=dict(type="str", default="master"),
client_id=dict(type="str"),
attributes=dict(type="dict"),
composites=dict(type="list", default=[], options=composites_spec, elements="dict"),
composite=dict(type="bool", default=False),
)
argument_spec.update(meta_args)
module = AnsibleModule(argument_spec=argument_spec,
supports_check_mode=True,
required_one_of=([['token', 'auth_realm', 'auth_username', 'auth_password']]),
required_together=([['auth_realm', 'auth_username', 'auth_password']]))
module = AnsibleModule(
argument_spec=argument_spec,
supports_check_mode=True,
required_one_of=(
[["token", "auth_realm", "auth_username", "auth_password", "auth_client_id", "auth_client_secret"]]
),
required_together=([["auth_username", "auth_password"]]),
required_by={"refresh_token": "auth_realm"},
)
result = dict(changed=False, msg='', diff={}, proposed={}, existing={}, end_state={})
result = dict(changed=False, msg="", diff={}, proposed={}, existing={}, end_state={})
# Obtain access token, initialize API
try:
@@ -281,22 +283,25 @@ def main():
kc = KeycloakAPI(module, connection_header)
realm = module.params.get('realm')
clientid = module.params.get('client_id')
name = module.params.get('name')
state = module.params.get('state')
realm = module.params.get("realm")
clientid = module.params.get("client_id")
name = module.params.get("name")
state = module.params.get("state")
# attributes in Keycloak have their values returned as lists
# via the API. attributes is a dict, so we'll transparently convert
# using the API. attributes is a dict, so we'll transparently convert
# the values to lists.
if module.params.get('attributes') is not None:
for key, val in module.params['attributes'].items():
module.params['attributes'][key] = [val] if not isinstance(val, list) else val
if module.params.get("attributes") is not None:
for key, val in module.params["attributes"].items():
module.params["attributes"][key] = [val] if not isinstance(val, list) else val
# Filter and map the parameters names that apply to the role
role_params = [x for x in module.params
if x not in list(keycloak_argument_spec().keys()) + ['state', 'realm', 'client_id'] and
module.params.get(x) is not None]
role_params = [
x
for x in module.params
if x not in list(keycloak_argument_spec().keys()) + ["state", "realm", "client_id"]
and module.params.get(x) is not None
]
# See if it already exists in Keycloak
if clientid is None:
@@ -320,28 +325,28 @@ def main():
desired_role = copy.deepcopy(before_role)
desired_role.update(changeset)
result['proposed'] = changeset
result['existing'] = before_role
result["proposed"] = changeset
result["existing"] = before_role
# Cater for when it doesn't exist (an empty dict)
if not before_role:
if state == 'absent':
if state == "absent":
# 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.'
result["diff"] = dict(before="", after="")
result["changed"] = False
result["end_state"] = {}
result["msg"] = "Role does not exist, doing nothing."
module.exit_json(**result)
# Process a creation
result['changed'] = True
result["changed"] = True
if name is None:
module.fail_json(msg='name must be specified when creating a new role')
module.fail_json(msg="name must be specified when creating a new role")
if module._diff:
result['diff'] = dict(before='', after=desired_role)
result["diff"] = dict(before="", after=desired_role)
if module.check_mode:
module.exit_json(**result)
@@ -354,45 +359,49 @@ def main():
kc.create_client_role(desired_role, clientid, realm)
after_role = kc.get_client_role(name, clientid, realm)
if after_role['composite']:
after_role['composites'] = kc.get_role_composites(rolerep=after_role, clientid=clientid, realm=realm)
if after_role["composite"]:
after_role["composites"] = kc.get_role_composites(rolerep=after_role, clientid=clientid, realm=realm)
result['end_state'] = after_role
result["end_state"] = after_role
result['msg'] = 'Role {name} has been created'.format(name=name)
result["msg"] = f"Role {name} has been created"
module.exit_json(**result)
else:
if state == 'present':
compare_exclude = []
if 'composites' in desired_role and isinstance(desired_role['composites'], list) and len(desired_role['composites']) > 0:
if state == "present":
compare_exclude = ["clientId"]
if (
"composites" in desired_role
and isinstance(desired_role["composites"], list)
and len(desired_role["composites"]) > 0
):
composites = kc.get_role_composites(rolerep=before_role, clientid=clientid, realm=realm)
before_role['composites'] = []
before_role["composites"] = []
for composite in composites:
before_composite = {}
if composite['clientRole']:
composite_client = kc.get_client_by_id(id=composite['containerId'], realm=realm)
before_composite['client_id'] = composite_client['clientId']
if composite["clientRole"]:
composite_client = kc.get_client_by_id(id=composite["containerId"], realm=realm)
before_composite["client_id"] = composite_client["clientId"]
else:
before_composite['client_id'] = None
before_composite['name'] = composite['name']
before_composite['state'] = 'present'
before_role['composites'].append(before_composite)
before_composite["client_id"] = None
before_composite["name"] = composite["name"]
before_composite["state"] = "present"
before_role["composites"].append(before_composite)
else:
compare_exclude.append('composites')
compare_exclude.append("composites")
# Process an update
# no changes
if is_struct_included(desired_role, before_role, exclude=compare_exclude):
result['changed'] = False
result['end_state'] = desired_role
result['msg'] = "No changes required to role {name}.".format(name=name)
result["changed"] = False
result["end_state"] = desired_role
result["msg"] = f"No changes required to role {name}."
module.exit_json(**result)
# doing an update
result['changed'] = True
result["changed"] = True
if module._diff:
result['diff'] = dict(before=before_role, after=desired_role)
result["diff"] = dict(before=before_role, after=desired_role)
if module.check_mode:
module.exit_json(**result)
@@ -404,20 +413,20 @@ def main():
else:
kc.update_client_role(desired_role, clientid, realm)
after_role = kc.get_client_role(name, clientid, realm)
if after_role['composite']:
after_role['composites'] = kc.get_role_composites(rolerep=after_role, clientid=clientid, realm=realm)
if after_role["composite"]:
after_role["composites"] = kc.get_role_composites(rolerep=after_role, clientid=clientid, realm=realm)
result['end_state'] = after_role
result["end_state"] = after_role
result['msg'] = "Role {name} has been updated".format(name=name)
result["msg"] = f"Role {name} has been updated"
module.exit_json(**result)
else:
# Process a deletion (because state was not 'present')
result['changed'] = True
result["changed"] = True
if module._diff:
result['diff'] = dict(before=before_role, after='')
result["diff"] = dict(before=before_role, after="")
if module.check_mode:
module.exit_json(**result)
@@ -428,12 +437,12 @@ def main():
else:
kc.delete_client_role(name, clientid, realm)
result['end_state'] = {}
result["end_state"] = {}
result['msg'] = "Role {name} has been deleted".format(name=name)
result["msg"] = f"Role {name} has been deleted"
module.exit_json(**result)
if __name__ == '__main__':
if __name__ == "__main__":
main()