Add a module to set the keycloak client scope type (#6322)

The module keycloak_clientscope_type allows to set the client scope
types (optional/default) either on realm or client level.
This commit is contained in:
Simon Pahl 2023-04-22 22:55:46 +02:00 committed by GitHub
commit 1f2c7b1731
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 629 additions and 0 deletions

View file

@ -0,0 +1,285 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# 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 absolute_import, division, print_function
__metaclass__ = type
DOCUMENTATION = '''
---
module: keycloak_clientscope_type
short_description: Set the type of aclientscope in realm or client via Keycloak API
version_added: 6.6.0
description:
- This module allows you to set the type (optional, default) of clientscopes
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.
attributes:
check_mode:
support: full
diff_mode:
support: full
options:
realm:
type: str
description:
- The Keycloak realm.
default: 'master'
client_id:
description:
- The I(client_id) of the client. If not set the clientscop types are set as a default for the realm.
aliases:
- clientId
type: str
default_clientscopes:
description:
- Client scopes that should be of type default.
type: list
elements: str
optional_clientscopes:
description:
- Client scopes that should be of type optional.
type: list
elements: str
extends_documentation_fragment:
- community.general.keycloak
- community.general.attributes
author:
- Simon Pahl (@simonpahl)
'''
EXAMPLES = '''
- name: Set default client scopes on realm level
community.general.keycloak_clientsecret_info:
auth_client_id: admin-cli
auth_keycloak_url: https://auth.example.com/auth
auth_realm: master
auth_username: USERNAME
auth_password: PASSWORD
realm: "MyCustomRealm"
default_clientscopes: ['profile', 'roles']
delegate_to: localhost
- name: Set default and optional client scopes on client level with token auth
community.general.keycloak_clientsecret_info:
auth_client_id: admin-cli
auth_keycloak_url: https://auth.example.com/auth
token: TOKEN
realm: "MyCustomRealm"
client_id: "MyCustomClient"
default_clientscopes: ['profile', 'roles']
optional_clientscopes: ['phone']
delegate_to: localhost
'''
RETURN = '''
msg:
description: Message as to what action was taken.
returned: always
type: str
sample: ""
proposed:
description: Representation of proposed client-scope types mapping.
returned: always
type: dict
sample: {
default_clientscopes: ["profile", "role"],
optional_clientscopes: []
}
existing:
description:
- Representation of client scopes before module execution.
returned: always
type: dict
sample: {
default_clientscopes: ["profile", "role"],
optional_clientscopes: ["phone"]
}
end_state:
description:
- Representation of client scopes after module execution.
- The sample is truncated.
returned: on success
type: dict
sample: {
default_clientscopes: ["profile", "role"],
optional_clientscopes: []
}
'''
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.general.plugins.module_utils.identity.keycloak.keycloak import (
KeycloakAPI, KeycloakError, get_token)
from ansible_collections.community.general.plugins.module_utils.identity.keycloak.keycloak import \
keycloak_argument_spec
def keycloak_clientscope_type_module():
"""
Returns an AnsibleModule definition.
:return: argument_spec dict
"""
argument_spec = keycloak_argument_spec()
meta_args = dict(
realm=dict(default='master'),
client_id=dict(type='str', aliases=['clientId']),
default_clientscopes=dict(type='list', elements='str'),
optional_clientscopes=dict(type='list', elements='str'),
)
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'],
['default_clientscopes', 'optional_clientscopes']
]),
required_together=([['auth_realm', 'auth_username', 'auth_password']]),
mutually_exclusive=[
['token', 'auth_realm'],
['token', 'auth_username'],
['token', 'auth_password']
])
return module
def clientscopes_to_add(existing, proposed):
to_add = []
existing_clientscope_ids = extract_field(existing, 'id')
for clientscope in proposed:
if not clientscope['id'] in existing_clientscope_ids:
to_add.append(clientscope)
return to_add
def clientscopes_to_delete(existing, proposed):
to_delete = []
proposed_clientscope_ids = extract_field(proposed, 'id')
for clientscope in existing:
if not clientscope['id'] in proposed_clientscope_ids:
to_delete.append(clientscope)
return to_delete
def extract_field(dictionary, field='name'):
return [cs[field] for cs in dictionary]
def main():
"""
Module keycloak_clientscope_type
:return:
"""
module = keycloak_clientscope_type_module()
# 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.get('realm')
client_id = module.params.get('client_id')
default_clientscopes = module.params.get('default_clientscopes')
optional_clientscopes = module.params.get('optional_clientscopes')
result = dict(changed=False, msg='', proposed={}, existing={}, end_state={})
all_clientscopes = kc.get_clientscopes(realm)
default_clientscopes_real = []
optional_clientscopes_real = []
for client_scope in all_clientscopes:
if default_clientscopes is not None and client_scope["name"] in default_clientscopes:
default_clientscopes_real.append(client_scope)
if optional_clientscopes is not None and client_scope["name"] in optional_clientscopes:
optional_clientscopes_real.append(client_scope)
if default_clientscopes is not None and len(default_clientscopes_real) != len(default_clientscopes):
module.fail_json(msg='At least one of the default_clientscopes does not exist!')
if optional_clientscopes is not None and len(optional_clientscopes_real) != len(optional_clientscopes):
module.fail_json(msg='At least one of the optional_clientscopes does not exist!')
result['proposed'].update({
'default_clientscopes': 'no-change' if default_clientscopes is None else default_clientscopes,
'optional_clientscopes': 'no-change' if optional_clientscopes is None else optional_clientscopes
})
default_clientscopes_existing = kc.get_default_clientscopes(realm, client_id)
optional_clientscopes_existing = kc.get_optional_clientscopes(realm, client_id)
result['existing'].update({
'default_clientscopes': extract_field(default_clientscopes_existing),
'optional_clientscopes': extract_field(optional_clientscopes_existing)
})
if module._diff:
result['diff'] = dict(before=result['existing'], after=result['proposed'])
if module.check_mode:
module.exit_json(**result)
default_clientscopes_add = clientscopes_to_add(default_clientscopes_existing, default_clientscopes_real)
optional_clientscopes_add = clientscopes_to_add(optional_clientscopes_existing, optional_clientscopes_real)
default_clientscopes_delete = clientscopes_to_delete(default_clientscopes_existing, default_clientscopes_real)
optional_clientscopes_delete = clientscopes_to_delete(optional_clientscopes_existing, optional_clientscopes_real)
# first delete so clientscopes can change type
for clientscope in default_clientscopes_delete:
kc.delete_default_clientscope(clientscope['id'], realm, client_id)
for clientscope in optional_clientscopes_delete:
kc.delete_optional_clientscope(clientscope['id'], realm, client_id)
for clientscope in default_clientscopes_add:
kc.add_default_clientscope(clientscope['id'], realm, client_id)
for clientscope in optional_clientscopes_add:
kc.add_optional_clientscope(clientscope['id'], realm, client_id)
result["changed"] = (
len(default_clientscopes_add) > 0
or len(optional_clientscopes_add) > 0
or len(default_clientscopes_delete) > 0
or len(optional_clientscopes_delete) > 0
)
result['end_state'].update({
'default_clientscopes': extract_field(kc.get_default_clientscopes(realm, client_id)),
'optional_clientscopes': extract_field(kc.get_optional_clientscopes(realm, client_id))
})
module.exit_json(**result)
if __name__ == '__main__':
main()