1
0
Fork 0
mirror of https://github.com/ansible-collections/community.general.git synced 2024-09-14 20:13:21 +02:00
community.general/plugins/modules/keycloak_client_rolescope.py
patchback[bot] aa384be6fe
[PR #8252/a5697da2 backport][stable-8] Keycloak client role scope (#8266)
Keycloak client role scope (#8252)

* first commit

* minor update

* fixe Copyright

* fixe sanity

* Update plugins/modules/keycloak_client_rolescope.py

Co-authored-by: Felix Fontein <felix@fontein.de>

* fixe sanity 2

* Update plugins/modules/keycloak_client_rolescope.py

Co-authored-by: Felix Fontein <felix@fontein.de>

---------

Co-authored-by: Andre Desrosiers <andre.desrosiers@ssss.gouv.qc.ca>
Co-authored-by: Felix Fontein <felix@fontein.de>
(cherry picked from commit a5697da29c)

Co-authored-by: desand01 <desrosiers.a@hotmail.com>
2024-04-21 20:39:47 +02:00

280 lines
10 KiB
Python

#!/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_client_rolescope
short_description: Allows administration of Keycloak client roles scope to restrict the usage of certain roles to a other specific client applications.
version_added: 8.6.0
description:
- This module allows you to add or remove Keycloak roles from clients scope 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.
- Client O(client_id) must have O(community.general.keycloak_client#module:full_scope_allowed) set to V(false).
- 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.
attributes:
check_mode:
support: full
diff_mode:
support: full
options:
state:
description:
- State of the role mapping.
- On V(present), all roles in O(role_names) will be mapped if not exists yet.
- On V(absent), all roles mapping in O(role_names) will be removed if it exists.
default: 'present'
type: str
choices:
- present
- absent
realm:
type: str
description:
- The Keycloak realm under which clients resides.
default: 'master'
client_id:
type: str
required: true
description:
- Roles provided in O(role_names) while be added to this client scope.
client_scope_id:
type: str
description:
- If the O(role_names) are client role, the client ID under which it resides.
- If this parameter is absent, the roles are considered a realm role.
role_names:
required: true
type: list
elements: str
description:
- Names of roles to manipulate.
- If O(client_scope_id) is present, all roles must be under this client.
- If O(client_scope_id) is absent, all roles must be under the realm.
extends_documentation_fragment:
- community.general.keycloak
- community.general.attributes
author:
- Andre Desrosiers (@desand01)
'''
EXAMPLES = '''
- name: Add roles to public client scope
community.general.keycloak_client_rolescope:
auth_keycloak_url: https://auth.example.com/auth
auth_realm: master
auth_username: USERNAME
auth_password: PASSWORD
realm: MyCustomRealm
client_id: frontend-client-public
client_scope_id: backend-client-private
role_names:
- backend-role-admin
- backend-role-user
- name: Remove roles from public client scope
community.general.keycloak_client_rolescope:
auth_keycloak_url: https://auth.example.com/auth
auth_realm: master
auth_username: USERNAME
auth_password: PASSWORD
realm: MyCustomRealm
client_id: frontend-client-public
client_scope_id: backend-client-private
role_names:
- backend-role-admin
state: absent
- name: Add realm roles to public client scope
community.general.keycloak_client_rolescope:
auth_keycloak_url: https://auth.example.com/auth
auth_realm: master
auth_username: USERNAME
auth_password: PASSWORD
realm: MyCustomRealm
client_id: frontend-client-public
role_names:
- realm-role-admin
- realm-role-user
'''
RETURN = '''
msg:
description: Message as to what action was taken.
returned: always
type: str
sample: "Client role scope for frontend-client-public has been updated"
end_state:
description: Representation of role role scope after module execution.
returned: on success
type: list
elements: dict
sample: [
{
"clientRole": false,
"composite": false,
"containerId": "MyCustomRealm",
"id": "47293104-59a6-46f0-b460-2e9e3c9c424c",
"name": "backend-role-admin"
},
{
"clientRole": false,
"composite": false,
"containerId": "MyCustomRealm",
"id": "39c62a6d-542c-4715-92d2-41021eb33967",
"name": "backend-role-user"
}
]
'''
from ansible_collections.community.general.plugins.module_utils.identity.keycloak.keycloak import KeycloakAPI, \
keycloak_argument_spec, get_token, KeycloakError
from ansible.module_utils.basic import AnsibleModule
def main():
"""
Module execution
:return:
"""
argument_spec = keycloak_argument_spec()
meta_args = dict(
client_id=dict(type='str', required=True),
client_scope_id=dict(type='str'),
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.get('realm')
clientid = module.params.get('client_id')
client_scope_id = module.params.get('client_scope_id')
role_names = module.params.get('role_names')
state = module.params.get('state')
objRealm = kc.get_realm_by_id(realm)
if not objRealm:
module.fail_json(msg="Failed to retrive realm '{realm}'".format(realm=realm))
objClient = kc.get_client_by_clientid(clientid, realm)
if not objClient:
module.fail_json(msg="Failed to retrive client '{realm}.{clientid}'".format(realm=realm, clientid=clientid))
if objClient["fullScopeAllowed"] and state == "present":
module.fail_json(msg="FullScopeAllowed is active for Client '{realm}.{clientid}'".format(realm=realm, clientid=clientid))
if client_scope_id:
objClientScope = kc.get_client_by_clientid(client_scope_id, realm)
if not objClientScope:
module.fail_json(msg="Failed to retrive client '{realm}.{client_scope_id}'".format(realm=realm, client_scope_id=client_scope_id))
before_role_mapping = kc.get_client_role_scope_from_client(objClient["id"], objClientScope["id"], realm)
else:
before_role_mapping = kc.get_client_role_scope_from_realm(objClient["id"], realm)
if client_scope_id:
# retrive all role from client_scope
client_scope_roles_by_name = kc.get_client_roles_by_id(objClientScope["id"], realm)
else:
# retrive all role from realm
client_scope_roles_by_name = kc.get_realm_roles(realm)
# convert to indexed Dict by name
client_scope_roles_by_name = {role["name"]: role for role in client_scope_roles_by_name}
role_mapping_by_name = {role["name"]: role for role in before_role_mapping}
role_mapping_to_manipulate = []
if state == "present":
# update desired
for role_name in role_names:
if role_name not in client_scope_roles_by_name:
if client_scope_id:
module.fail_json(msg="Failed to retrive role '{realm}.{client_scope_id}.{role_name}'"
.format(realm=realm, client_scope_id=client_scope_id, role_name=role_name))
else:
module.fail_json(msg="Failed to retrive role '{realm}.{role_name}'".format(realm=realm, role_name=role_name))
if role_name not in role_mapping_by_name:
role_mapping_to_manipulate.append(client_scope_roles_by_name[role_name])
role_mapping_by_name[role_name] = client_scope_roles_by_name[role_name]
else:
# remove role if present
for role_name in role_names:
if role_name in role_mapping_by_name:
role_mapping_to_manipulate.append(role_mapping_by_name[role_name])
del role_mapping_by_name[role_name]
before_role_mapping = sorted(before_role_mapping, key=lambda d: d['name'])
desired_role_mapping = sorted(role_mapping_by_name.values(), key=lambda d: d['name'])
result['changed'] = len(role_mapping_to_manipulate) > 0
if result['changed']:
result['diff'] = dict(before=before_role_mapping, after=desired_role_mapping)
if not result['changed']:
# no changes
result['end_state'] = before_role_mapping
result['msg'] = "No changes required for client role scope {name}.".format(name=clientid)
elif state == "present":
# doing update
if module.check_mode:
result['end_state'] = desired_role_mapping
elif client_scope_id:
result['end_state'] = kc.update_client_role_scope_from_client(role_mapping_to_manipulate, objClient["id"], objClientScope["id"], realm)
else:
result['end_state'] = kc.update_client_role_scope_from_realm(role_mapping_to_manipulate, objClient["id"], realm)
result['msg'] = "Client role scope for {name} has been updated".format(name=clientid)
else:
# doing delete
if module.check_mode:
result['end_state'] = desired_role_mapping
elif client_scope_id:
result['end_state'] = kc.delete_client_role_scope_from_client(role_mapping_to_manipulate, objClient["id"], objClientScope["id"], realm)
else:
result['end_state'] = kc.delete_client_role_scope_from_realm(role_mapping_to_manipulate, objClient["id"], realm)
result['msg'] = "Client role scope for {name} has been deleted".format(name=clientid)
module.exit_json(**result)
if __name__ == '__main__':
main()