mirror of
https://github.com/ansible-middleware/keycloak.git
synced 2026-05-06 21:33:05 +00:00
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
325 lines
11 KiB
Python
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()
|