Files
ansible-middleware.keycloak/plugins/modules/keycloak_client_scope.py
pamenon c6189bfc51 Add keycloak_client_scope and keycloak_authentication_flow modules with example playbooks
The collection was missing modules for managing client scopes and
authentication flows, forcing users to write raw uri calls against
the Keycloak Admin REST API. This adds two new modules that leverage
the existing KeycloakAPI helper methods:

- keycloak_client_scope: create/update/delete client scopes with
  protocol mappers (supports check_mode and diff)
- keycloak_authentication_flow: create/delete authentication flows
  with execution steps, or copy existing flows (supports check_mode
  and diff)

Also adds three example playbooks using the new modules:
- keycloak_client_scope.yml
- keycloak_authentication_flow.yml
- keycloak_realm_client.yml

Made-with: Cursor
2026-04-23 12:53:03 +01:00

325 lines
11 KiB
Python

#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright (c) 2024, Contributors to the middleware_automation.keycloak collection
# 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
DOCUMENTATION = '''
---
module: keycloak_client_scope
short_description: Allows administration of Keycloak client scopes via Keycloak API
description:
- This module allows you to add, remove or modify Keycloak client scopes 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.
- This module also supports managing protocol mappers within a client scope.
attributes:
check_mode:
support: full
diff_mode:
support: full
options:
state:
description:
- State of the client scope.
- On V(present), the client scope will be created if it does not yet exist, or updated with the parameters you provide.
- On V(absent), the client scope will be removed if it exists.
default: 'present'
type: str
choices:
- present
- absent
name:
type: str
required: true
description:
- Name of the client scope.
description:
type: str
description:
- Description of the client scope.
realm:
type: str
description:
- The Keycloak realm under which this client scope resides.
default: 'master'
protocol:
type: str
description:
- The protocol associated with the client scope.
default: 'openid-connect'
choices:
- openid-connect
- saml
attributes:
type: dict
description:
- A dict of key/value pairs to set as attributes for the client scope.
protocol_mappers:
type: list
elements: dict
description:
- A list of protocol mappers to associate with the client scope.
- Each mapper is a dict with the keys C(name), C(protocol), C(protocolMapper), and C(config).
default: []
suboptions:
name:
type: str
required: true
description:
- Name of the protocol mapper.
protocol:
type: str
description:
- Protocol for the mapper.
default: 'openid-connect'
protocolMapper:
type: str
required: true
description:
- The mapper type (e.g. V(oidc-usermodel-attribute-mapper), V(oidc-audience-mapper)).
aliases:
- protocol_mapper_type
config:
type: dict
required: true
description:
- Configuration for the protocol mapper.
extends_documentation_fragment:
- middleware_automation.keycloak.keycloak
- middleware_automation.keycloak.attributes
author:
- Paulo Menon (@paulomenon)
'''
EXAMPLES = '''
- name: Create a client scope with protocol mappers
middleware_automation.keycloak.keycloak_client_scope:
auth_keycloak_url: http://localhost:8080
auth_realm: master
auth_username: admin
auth_password: password
realm: TestRealm
name: my-client-scope
description: "A custom client scope"
protocol: openid-connect
protocol_mappers:
- name: email
protocol: openid-connect
protocolMapper: oidc-usermodel-attribute-mapper
config:
user.attribute: email
claim.name: email
jsonType.label: String
id.token.claim: "true"
access.token.claim: "true"
userinfo.token.claim: "true"
state: present
delegate_to: localhost
- name: Create a client scope using token authentication
middleware_automation.keycloak.keycloak_client_scope:
auth_keycloak_url: http://localhost:8080
token: MY_TOKEN
realm: TestRealm
name: my-scope
state: present
delegate_to: localhost
- name: Delete a client scope
middleware_automation.keycloak.keycloak_client_scope:
auth_keycloak_url: http://localhost:8080
auth_realm: master
auth_username: admin
auth_password: password
realm: TestRealm
name: my-client-scope
state: absent
delegate_to: localhost
'''
RETURN = '''
msg:
description: Message as to what action was taken.
returned: always
type: str
sample: "Client scope my-scope has been created"
end_state:
description: Representation of the client scope after module execution.
returned: on success
type: dict
sample: {
"id": "uuid-here",
"name": "my-scope",
"protocol": "openid-connect",
"description": "A custom scope"
}
'''
from ansible_collections.middleware_automation.keycloak.plugins.module_utils.identity.keycloak.keycloak import KeycloakAPI, \
keycloak_argument_spec, get_token, KeycloakError
from ansible.module_utils.basic import AnsibleModule
import copy
def main():
argument_spec = keycloak_argument_spec()
mapper_spec = dict(
name=dict(type='str', required=True),
protocol=dict(type='str', default='openid-connect'),
protocolMapper=dict(type='str', required=True, aliases=['protocol_mapper_type']),
config=dict(type='dict', required=True),
)
meta_args = dict(
state=dict(type='str', default='present', choices=['present', 'absent']),
name=dict(type='str', required=True),
description=dict(type='str', default=''),
realm=dict(type='str', default='master'),
protocol=dict(type='str', default='openid-connect', choices=['openid-connect', 'saml']),
attributes=dict(type='dict'),
protocol_mappers=dict(type='list', default=[], options=mapper_spec, elements='dict'),
)
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']]))
result = dict(changed=False, msg='', diff={}, end_state={})
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.get('realm')
name = module.params.get('name')
state = module.params.get('state')
protocol = module.params.get('protocol')
description = module.params.get('description')
attributes = module.params.get('attributes')
protocol_mappers = module.params.get('protocol_mappers')
before_scope = kc.get_clientscope_by_name(name, realm=realm)
if state == 'absent':
if before_scope:
result['changed'] = True
if module._diff:
result['diff'] = dict(before=before_scope, after='')
if module.check_mode:
module.exit_json(**result)
kc.delete_clientscope(cid=before_scope['id'], realm=realm)
result['msg'] = "Client scope {name} has been deleted".format(name=name)
else:
result['msg'] = "Client scope {name} does not exist, doing nothing".format(name=name)
result['end_state'] = {}
module.exit_json(**result)
scope_rep = {
'name': name,
'protocol': protocol,
'description': description,
}
if attributes:
scope_rep['attributes'] = attributes
if not before_scope:
result['changed'] = True
if module._diff:
result['diff'] = dict(before='', after=scope_rep)
if module.check_mode:
module.exit_json(**result)
kc.create_clientscope(scope_rep, realm=realm)
after_scope = kc.get_clientscope_by_name(name, realm=realm)
if protocol_mappers:
for mapper in protocol_mappers:
mapper_rep = {
'name': mapper['name'],
'protocol': mapper.get('protocol', protocol),
'protocolMapper': mapper['protocolMapper'],
'config': mapper['config'],
}
kc.create_clientscope_protocolmapper(after_scope['id'], mapper_rep, realm=realm)
after_scope = kc.get_clientscope_by_name(name, realm=realm)
result['end_state'] = after_scope
result['msg'] = "Client scope {name} has been created".format(name=name)
module.exit_json(**result)
else:
changed = False
for key in ('protocol', 'description'):
if scope_rep.get(key) and scope_rep[key] != before_scope.get(key):
changed = True
break
if attributes and attributes != before_scope.get('attributes', {}):
changed = True
if changed:
result['changed'] = True
scope_rep['id'] = before_scope['id']
if module._diff:
result['diff'] = dict(before=before_scope, after=scope_rep)
if module.check_mode:
module.exit_json(**result)
kc.update_clientscope(scope_rep, realm=realm)
if protocol_mappers:
existing_mappers = kc.get_clientscope_protocolmappers(before_scope['id'], realm=realm)
existing_mapper_names = {m['name'] for m in existing_mappers}
for mapper in protocol_mappers:
if mapper['name'] not in existing_mapper_names:
result['changed'] = True
if not module.check_mode:
mapper_rep = {
'name': mapper['name'],
'protocol': mapper.get('protocol', protocol),
'protocolMapper': mapper['protocolMapper'],
'config': mapper['config'],
}
kc.create_clientscope_protocolmapper(before_scope['id'], mapper_rep, realm=realm)
after_scope = kc.get_clientscope_by_name(name, realm=realm)
result['end_state'] = after_scope
if result['changed']:
result['msg'] = "Client scope {name} has been updated".format(name=name)
else:
result['msg'] = "No changes required to client scope {name}".format(name=name)
module.exit_json(**result)
if __name__ == '__main__':
main()