1
0
Fork 0
mirror of https://github.com/ansible-collections/community.general.git synced 2024-09-14 20:13:21 +02:00

New Module: Keycloak User Rolemapping (#4898)

* keycloak_user_rolemapping: implement user role mapping

* keycloak_user_rolemapping: additional logging

* keycloak_user_rolemapping: move to getters, use names parameters

* keycloak_user_rolemapping: add service account user example

* Add keyring and keyring_info modules (#4764)

* keycloak_user_rolemapping: write tests, address ansibullbot concerns no.1

* keycloak_user_rolemapping: address felixfontein concerns no.1

* keycloak_user_rolemapping: remove rebase mistakes

* keycloak_user_rolemapping: address felixfontein concerns no.2

* keycloak_user_rolemapping: refactor duplicate username usage example

* keycloak_user_rolemapping: fix sanity check errors no.1

* keycloak_user_rolemapping: fix sanity check errors no.2

* keycloak_user_rolemapping: fix sanity check errors no.3

* keycloak_user_rolemapping: fix sanity check errors no.4

* keycloak_user_rolemapping: write tests, address ansibullbot concerns no.1

* keycloak_user_rolemapping: resolve rebase conflicts with origin/main branch

# Conflicts:
#	plugins/module_utils/identity/keycloak/keycloak.py

* keycloak_user_rolemapping: remove keycloak_role_composites from BOTMETA.yml

* keycloak_user_rolemapping: fix sanity check errors no.5

* keycloak_user_rolemapping: address felixfontein reviews concerns no.1

* keycloak_user_rolemapping: address felixfontein reviews concerns no.2

Co-authored-by: Dušan Markovič <dusan.markovic@better.care>
Co-authored-by: ahussey-redhat <93101976+ahussey-redhat@users.noreply.github.com>
This commit is contained in:
bratwurzt 2022-10-01 18:16:47 +02:00 committed by GitHub
parent 2eba5dc4e7
commit 2cac3ae879
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 916 additions and 90 deletions

2
.github/BOTMETA.yml vendored
View file

@ -589,6 +589,8 @@ files:
maintainers: Gaetan2907 maintainers: Gaetan2907
$modules/identity/keycloak/keycloak_client_rolemapping.py: $modules/identity/keycloak/keycloak_client_rolemapping.py:
maintainers: Gaetan2907 maintainers: Gaetan2907
$modules/identity/keycloak/keycloak_user_rolemapping.py:
maintainers: bratwurzt
$modules/identity/keycloak/keycloak_group.py: $modules/identity/keycloak/keycloak_group.py:
maintainers: adamgoossens maintainers: adamgoossens
$modules/identity/keycloak/keycloak_identity_provider.py: $modules/identity/keycloak/keycloak_identity_provider.py:

View file

@ -612,6 +612,8 @@ plugin_routing:
redirect: community.general.identity.keycloak.keycloak_role redirect: community.general.identity.keycloak.keycloak_role
keycloak_user_federation: keycloak_user_federation:
redirect: community.general.identity.keycloak.keycloak_user_federation redirect: community.general.identity.keycloak.keycloak_user_federation
keycloak_user_rolemapping:
redirect: community.general.identity.keycloak.keycloak_user_rolemapping
keyring: keyring:
redirect: community.general.system.keyring redirect: community.general.system.keyring
keyring_info: keyring_info:

View file

@ -29,8 +29,15 @@ URL_CLIENT_ROLE_COMPOSITES = "{url}/admin/realms/{realm}/clients/{id}/roles/{nam
URL_REALM_ROLES = "{url}/admin/realms/{realm}/roles" URL_REALM_ROLES = "{url}/admin/realms/{realm}/roles"
URL_REALM_ROLE = "{url}/admin/realms/{realm}/roles/{name}" URL_REALM_ROLE = "{url}/admin/realms/{realm}/roles/{name}"
URL_REALM_ROLEMAPPINGS = "{url}/admin/realms/{realm}/users/{id}/role-mappings/realm"
URL_REALM_ROLEMAPPINGS_AVAILABLE = "{url}/admin/realms/{realm}/users/{id}/role-mappings/realm/available"
URL_REALM_ROLEMAPPINGS_COMPOSITE = "{url}/admin/realms/{realm}/users/{id}/role-mappings/realm/composite"
URL_REALM_ROLE_COMPOSITES = "{url}/admin/realms/{realm}/roles/{name}/composites" URL_REALM_ROLE_COMPOSITES = "{url}/admin/realms/{realm}/roles/{name}/composites"
URL_ROLES_BY_ID = "{url}/admin/realms/{realm}/roles-by-id/{id}"
URL_ROLES_BY_ID_COMPOSITES_CLIENTS = "{url}/admin/realms/{realm}/roles-by-id/{id}/composites/clients/{cid}"
URL_ROLES_BY_ID_COMPOSITES = "{url}/admin/realms/{realm}/roles-by-id/{id}/composites"
URL_CLIENTTEMPLATE = "{url}/admin/realms/{realm}/client-templates/{id}" URL_CLIENTTEMPLATE = "{url}/admin/realms/{realm}/client-templates/{id}"
URL_CLIENTTEMPLATES = "{url}/admin/realms/{realm}/client-templates" URL_CLIENTTEMPLATES = "{url}/admin/realms/{realm}/client-templates"
URL_GROUPS = "{url}/admin/realms/{realm}/groups" URL_GROUPS = "{url}/admin/realms/{realm}/groups"
@ -41,9 +48,15 @@ URL_CLIENTSCOPE = "{url}/admin/realms/{realm}/client-scopes/{id}"
URL_CLIENTSCOPE_PROTOCOLMAPPERS = "{url}/admin/realms/{realm}/client-scopes/{id}/protocol-mappers/models" URL_CLIENTSCOPE_PROTOCOLMAPPERS = "{url}/admin/realms/{realm}/client-scopes/{id}/protocol-mappers/models"
URL_CLIENTSCOPE_PROTOCOLMAPPER = "{url}/admin/realms/{realm}/client-scopes/{id}/protocol-mappers/models/{mapper_id}" URL_CLIENTSCOPE_PROTOCOLMAPPER = "{url}/admin/realms/{realm}/client-scopes/{id}/protocol-mappers/models/{mapper_id}"
URL_CLIENT_ROLEMAPPINGS = "{url}/admin/realms/{realm}/groups/{id}/role-mappings/clients/{client}" URL_CLIENT_GROUP_ROLEMAPPINGS = "{url}/admin/realms/{realm}/groups/{id}/role-mappings/clients/{client}"
URL_CLIENT_ROLEMAPPINGS_AVAILABLE = "{url}/admin/realms/{realm}/groups/{id}/role-mappings/clients/{client}/available" URL_CLIENT_GROUP_ROLEMAPPINGS_AVAILABLE = "{url}/admin/realms/{realm}/groups/{id}/role-mappings/clients/{client}/available"
URL_CLIENT_ROLEMAPPINGS_COMPOSITE = "{url}/admin/realms/{realm}/groups/{id}/role-mappings/clients/{client}/composite" URL_CLIENT_GROUP_ROLEMAPPINGS_COMPOSITE = "{url}/admin/realms/{realm}/groups/{id}/role-mappings/clients/{client}/composite"
URL_USERS = "{url}/admin/realms/{realm}/users"
URL_CLIENT_SERVICE_ACCOUNT_USER = "{url}/admin/realms/{realm}/clients/{id}/service-account-user"
URL_CLIENT_USER_ROLEMAPPINGS = "{url}/admin/realms/{realm}/users/{id}/role-mappings/clients/{client}"
URL_CLIENT_USER_ROLEMAPPINGS_AVAILABLE = "{url}/admin/realms/{realm}/users/{id}/role-mappings/clients/{client}/available"
URL_CLIENT_USER_ROLEMAPPINGS_COMPOSITE = "{url}/admin/realms/{realm}/users/{id}/role-mappings/clients/{client}/composite"
URL_AUTHENTICATION_FLOWS = "{url}/admin/realms/{realm}/authentication/flows" URL_AUTHENTICATION_FLOWS = "{url}/admin/realms/{realm}/authentication/flows"
URL_AUTHENTICATION_FLOW = "{url}/admin/realms/{realm}/authentication/flows/{id}" URL_AUTHENTICATION_FLOW = "{url}/admin/realms/{realm}/authentication/flows/{id}"
@ -446,10 +459,9 @@ class KeycloakAPI(object):
self.module.fail_json(msg="Could not fetch rolemappings for client %s in realm %s: %s" self.module.fail_json(msg="Could not fetch rolemappings for client %s in realm %s: %s"
% (cid, realm, str(e))) % (cid, realm, str(e)))
def get_client_role_by_name(self, gid, cid, name, realm="master"): def get_client_role_id_by_name(self, cid, name, realm="master"):
""" Get the role ID of a client. """ Get the role ID of a client.
:param gid: ID of the group from which to obtain the rolemappings.
:param cid: ID of the client from which to obtain the rolemappings. :param cid: ID of the client from which to obtain the rolemappings.
:param name: Name of the role. :param name: Name of the role.
:param realm: Realm from which to obtain the rolemappings. :param realm: Realm from which to obtain the rolemappings.
@ -461,7 +473,7 @@ class KeycloakAPI(object):
return role['id'] return role['id']
return None return None
def get_client_rolemapping_by_id(self, gid, cid, rid, realm='master'): def get_client_group_rolemapping_by_id(self, gid, cid, rid, realm='master'):
""" Obtain client representation by id """ Obtain client representation by id
:param gid: ID of the group from which to obtain the rolemappings. :param gid: ID of the group from which to obtain the rolemappings.
@ -470,7 +482,7 @@ class KeycloakAPI(object):
:param realm: client from this realm :param realm: client from this realm
:return: dict of rolemapping representation or None if none matching exist :return: dict of rolemapping representation or None if none matching exist
""" """
rolemappings_url = URL_CLIENT_ROLEMAPPINGS.format(url=self.baseurl, realm=realm, id=gid, client=cid) rolemappings_url = URL_CLIENT_GROUP_ROLEMAPPINGS.format(url=self.baseurl, realm=realm, id=gid, client=cid)
try: try:
rolemappings = json.loads(to_native(open_url(rolemappings_url, method="GET", http_agent=self.http_agent, headers=self.restheaders, rolemappings = json.loads(to_native(open_url(rolemappings_url, method="GET", http_agent=self.http_agent, headers=self.restheaders,
timeout=self.connection_timeout, timeout=self.connection_timeout,
@ -483,7 +495,7 @@ class KeycloakAPI(object):
% (cid, gid, realm, str(e))) % (cid, gid, realm, str(e)))
return None return None
def get_client_available_rolemappings(self, gid, cid, realm="master"): def get_client_group_available_rolemappings(self, gid, cid, realm="master"):
""" Fetch the available role of a client in a specified goup on the Keycloak server. """ Fetch the available role of a client in a specified goup on the Keycloak server.
:param gid: ID of the group from which to obtain the rolemappings. :param gid: ID of the group from which to obtain the rolemappings.
@ -491,7 +503,7 @@ class KeycloakAPI(object):
:param realm: Realm from which to obtain the rolemappings. :param realm: Realm from which to obtain the rolemappings.
:return: The rollemappings of specified group and client of the realm (default "master"). :return: The rollemappings of specified group and client of the realm (default "master").
""" """
available_rolemappings_url = URL_CLIENT_ROLEMAPPINGS_AVAILABLE.format(url=self.baseurl, realm=realm, id=gid, client=cid) available_rolemappings_url = URL_CLIENT_GROUP_ROLEMAPPINGS_AVAILABLE.format(url=self.baseurl, realm=realm, id=gid, client=cid)
try: try:
return json.loads(to_native(open_url(available_rolemappings_url, method="GET", http_agent=self.http_agent, headers=self.restheaders, return json.loads(to_native(open_url(available_rolemappings_url, method="GET", http_agent=self.http_agent, headers=self.restheaders,
timeout=self.connection_timeout, timeout=self.connection_timeout,
@ -500,7 +512,7 @@ class KeycloakAPI(object):
self.module.fail_json(msg="Could not fetch available rolemappings for client %s in group %s, realm %s: %s" self.module.fail_json(msg="Could not fetch available rolemappings for client %s in group %s, realm %s: %s"
% (cid, gid, realm, str(e))) % (cid, gid, realm, str(e)))
def get_client_composite_rolemappings(self, gid, cid, realm="master"): def get_client_group_composite_rolemappings(self, gid, cid, realm="master"):
""" Fetch the composite role of a client in a specified group on the Keycloak server. """ Fetch the composite role of a client in a specified group on the Keycloak server.
:param gid: ID of the group from which to obtain the rolemappings. :param gid: ID of the group from which to obtain the rolemappings.
@ -508,15 +520,64 @@ class KeycloakAPI(object):
:param realm: Realm from which to obtain the rolemappings. :param realm: Realm from which to obtain the rolemappings.
:return: The rollemappings of specified group and client of the realm (default "master"). :return: The rollemappings of specified group and client of the realm (default "master").
""" """
available_rolemappings_url = URL_CLIENT_ROLEMAPPINGS_COMPOSITE.format(url=self.baseurl, realm=realm, id=gid, client=cid) composite_rolemappings_url = URL_CLIENT_GROUP_ROLEMAPPINGS_COMPOSITE.format(url=self.baseurl, realm=realm, id=gid, client=cid)
try: try:
return json.loads(to_native(open_url(available_rolemappings_url, method="GET", http_agent=self.http_agent, headers=self.restheaders, return json.loads(to_native(open_url(composite_rolemappings_url, method="GET", http_agent=self.http_agent, headers=self.restheaders,
timeout=self.connection_timeout, timeout=self.connection_timeout,
validate_certs=self.validate_certs).read())) validate_certs=self.validate_certs).read()))
except Exception as e: except Exception as e:
self.module.fail_json(msg="Could not fetch available rolemappings for client %s in group %s, realm %s: %s" self.module.fail_json(msg="Could not fetch available rolemappings for client %s in group %s, realm %s: %s"
% (cid, gid, realm, str(e))) % (cid, gid, realm, str(e)))
def get_role_by_id(self, rid, realm="master"):
""" Fetch a role by its id on the Keycloak server.
:param rid: ID of the role.
:param realm: Realm from which to obtain the rolemappings.
:return: The role.
"""
client_roles_url = URL_ROLES_BY_ID.format(url=self.baseurl, realm=realm, id=rid)
try:
return json.loads(to_native(open_url(client_roles_url, method="GET", http_agent=self.http_agent, headers=self.restheaders,
timeout=self.connection_timeout,
validate_certs=self.validate_certs).read()))
except Exception as e:
self.module.fail_json(msg="Could not fetch role for id %s in realm %s: %s"
% (rid, realm, str(e)))
def get_client_roles_by_id_composite_rolemappings(self, rid, cid, realm="master"):
""" Fetch a role by its id on the Keycloak server.
:param rid: ID of the composite role.
:param cid: ID of the client from which to obtain the rolemappings.
:param realm: Realm from which to obtain the rolemappings.
:return: The role.
"""
client_roles_url = URL_ROLES_BY_ID_COMPOSITES_CLIENTS.format(url=self.baseurl, realm=realm, id=rid, cid=cid)
try:
return json.loads(to_native(open_url(client_roles_url, method="GET", http_agent=self.http_agent, headers=self.restheaders,
timeout=self.connection_timeout,
validate_certs=self.validate_certs).read()))
except Exception as e:
self.module.fail_json(msg="Could not fetch role for id %s and cid %s in realm %s: %s"
% (rid, cid, realm, str(e)))
def add_client_roles_by_id_composite_rolemapping(self, rid, roles_rep, realm="master"):
""" Assign roles to composite role
:param rid: ID of the composite role.
:param roles_rep: Representation of the roles to assign.
:param realm: Realm from which to obtain the rolemappings.
:return: None.
"""
available_rolemappings_url = URL_ROLES_BY_ID_COMPOSITES.format(url=self.baseurl, realm=realm, id=rid)
try:
open_url(available_rolemappings_url, method="POST", http_agent=self.http_agent, headers=self.restheaders, data=json.dumps(roles_rep),
validate_certs=self.validate_certs, timeout=self.connection_timeout)
except Exception as e:
self.module.fail_json(msg="Could not assign roles to composite role %s and realm %s: %s"
% (rid, realm, str(e)))
def add_group_rolemapping(self, gid, cid, role_rep, realm="master"): def add_group_rolemapping(self, gid, cid, role_rep, realm="master"):
""" Fetch the composite role of a client in a specified goup on the Keycloak server. """ Fetch the composite role of a client in a specified goup on the Keycloak server.
@ -526,7 +587,7 @@ class KeycloakAPI(object):
:param realm: Realm from which to obtain the rolemappings. :param realm: Realm from which to obtain the rolemappings.
:return: None. :return: None.
""" """
available_rolemappings_url = URL_CLIENT_ROLEMAPPINGS.format(url=self.baseurl, realm=realm, id=gid, client=cid) available_rolemappings_url = URL_CLIENT_GROUP_ROLEMAPPINGS.format(url=self.baseurl, realm=realm, id=gid, client=cid)
try: try:
open_url(available_rolemappings_url, method="POST", http_agent=self.http_agent, headers=self.restheaders, data=json.dumps(role_rep), open_url(available_rolemappings_url, method="POST", http_agent=self.http_agent, headers=self.restheaders, data=json.dumps(role_rep),
validate_certs=self.validate_certs, timeout=self.connection_timeout) validate_certs=self.validate_certs, timeout=self.connection_timeout)
@ -543,7 +604,7 @@ class KeycloakAPI(object):
:param realm: Realm from which to obtain the rolemappings. :param realm: Realm from which to obtain the rolemappings.
:return: None. :return: None.
""" """
available_rolemappings_url = URL_CLIENT_ROLEMAPPINGS.format(url=self.baseurl, realm=realm, id=gid, client=cid) available_rolemappings_url = URL_CLIENT_GROUP_ROLEMAPPINGS.format(url=self.baseurl, realm=realm, id=gid, client=cid)
try: try:
open_url(available_rolemappings_url, method="DELETE", http_agent=self.http_agent, headers=self.restheaders, open_url(available_rolemappings_url, method="DELETE", http_agent=self.http_agent, headers=self.restheaders,
validate_certs=self.validate_certs, timeout=self.connection_timeout) validate_certs=self.validate_certs, timeout=self.connection_timeout)
@ -551,6 +612,206 @@ class KeycloakAPI(object):
self.module.fail_json(msg="Could not delete available rolemappings for client %s in group %s, realm %s: %s" self.module.fail_json(msg="Could not delete available rolemappings for client %s in group %s, realm %s: %s"
% (cid, gid, realm, str(e))) % (cid, gid, realm, str(e)))
def get_client_user_rolemapping_by_id(self, uid, cid, rid, realm='master'):
""" Obtain client representation by id
:param uid: ID of the user from which to obtain the rolemappings.
:param cid: ID of the client from which to obtain the rolemappings.
:param rid: ID of the role.
:param realm: client from this realm
:return: dict of rolemapping representation or None if none matching exist
"""
rolemappings_url = URL_CLIENT_USER_ROLEMAPPINGS.format(url=self.baseurl, realm=realm, id=uid, client=cid)
try:
rolemappings = json.loads(to_native(open_url(rolemappings_url, method="GET", http_agent=self.http_agent, headers=self.restheaders,
timeout=self.connection_timeout,
validate_certs=self.validate_certs).read()))
for role in rolemappings:
if rid == role['id']:
return role
except Exception as e:
self.module.fail_json(msg="Could not fetch rolemappings for client %s and user %s, realm %s: %s"
% (cid, uid, realm, str(e)))
return None
def get_client_user_available_rolemappings(self, uid, cid, realm="master"):
""" Fetch the available role of a client for a specified user on the Keycloak server.
:param uid: ID of the user from which to obtain the rolemappings.
:param cid: ID of the client from which to obtain the rolemappings.
:param realm: Realm from which to obtain the rolemappings.
:return: The effective rollemappings of specified client and user of the realm (default "master").
"""
available_rolemappings_url = URL_CLIENT_USER_ROLEMAPPINGS_AVAILABLE.format(url=self.baseurl, realm=realm, id=uid, client=cid)
try:
return json.loads(to_native(open_url(available_rolemappings_url, method="GET", http_agent=self.http_agent, headers=self.restheaders,
timeout=self.connection_timeout,
validate_certs=self.validate_certs).read()))
except Exception as e:
self.module.fail_json(msg="Could not fetch effective rolemappings for client %s and user %s, realm %s: %s"
% (cid, uid, realm, str(e)))
def get_client_user_composite_rolemappings(self, uid, cid, realm="master"):
""" Fetch the composite role of a client for a specified user on the Keycloak server.
:param uid: ID of the user from which to obtain the rolemappings.
:param cid: ID of the client from which to obtain the rolemappings.
:param realm: Realm from which to obtain the rolemappings.
:return: The rollemappings of specified group and client of the realm (default "master").
"""
composite_rolemappings_url = URL_CLIENT_USER_ROLEMAPPINGS_COMPOSITE.format(url=self.baseurl, realm=realm, id=uid, client=cid)
try:
return json.loads(to_native(open_url(composite_rolemappings_url, method="GET", http_agent=self.http_agent, headers=self.restheaders,
timeout=self.connection_timeout,
validate_certs=self.validate_certs).read()))
except Exception as e:
self.module.fail_json(msg="Could not fetch available rolemappings for user %s of realm %s: %s"
% (uid, realm, str(e)))
def get_realm_user_rolemapping_by_id(self, uid, rid, realm='master'):
""" Obtain role representation by id
:param uid: ID of the user from which to obtain the rolemappings.
:param rid: ID of the role.
:param realm: client from this realm
:return: dict of rolemapping representation or None if none matching exist
"""
rolemappings_url = URL_REALM_ROLEMAPPINGS.format(url=self.baseurl, realm=realm, id=uid)
try:
rolemappings = json.loads(to_native(open_url(rolemappings_url, method="GET", http_agent=self.http_agent, headers=self.restheaders,
timeout=self.connection_timeout,
validate_certs=self.validate_certs).read()))
for role in rolemappings:
if rid == role['id']:
return role
except Exception as e:
self.module.fail_json(msg="Could not fetch rolemappings for user %s, realm %s: %s"
% (uid, realm, str(e)))
return None
def get_realm_user_available_rolemappings(self, uid, realm="master"):
""" Fetch the available role of a realm for a specified user on the Keycloak server.
:param uid: ID of the user from which to obtain the rolemappings.
:param realm: Realm from which to obtain the rolemappings.
:return: The rollemappings of specified group and client of the realm (default "master").
"""
available_rolemappings_url = URL_REALM_ROLEMAPPINGS_AVAILABLE.format(url=self.baseurl, realm=realm, id=uid)
try:
return json.loads(to_native(open_url(available_rolemappings_url, method="GET", http_agent=self.http_agent, headers=self.restheaders,
timeout=self.connection_timeout,
validate_certs=self.validate_certs).read()))
except Exception as e:
self.module.fail_json(msg="Could not fetch available rolemappings for user %s of realm %s: %s"
% (uid, realm, str(e)))
def get_realm_user_composite_rolemappings(self, uid, realm="master"):
""" Fetch the composite role of a realm for a specified user on the Keycloak server.
:param uid: ID of the user from which to obtain the rolemappings.
:param realm: Realm from which to obtain the rolemappings.
:return: The effective rollemappings of specified client and user of the realm (default "master").
"""
composite_rolemappings_url = URL_REALM_ROLEMAPPINGS_COMPOSITE.format(url=self.baseurl, realm=realm, id=uid)
try:
return json.loads(to_native(open_url(composite_rolemappings_url, method="GET", http_agent=self.http_agent, headers=self.restheaders,
timeout=self.connection_timeout,
validate_certs=self.validate_certs).read()))
except Exception as e:
self.module.fail_json(msg="Could not fetch effective rolemappings for user %s, realm %s: %s"
% (uid, realm, str(e)))
def get_user_by_username(self, username, realm="master"):
""" Fetch a keycloak user within a realm based on its username.
If the user does not exist, None is returned.
:param username: Username of the user to fetch.
:param realm: Realm in which the user resides; default 'master'
"""
users_url = URL_USERS.format(url=self.baseurl, realm=realm)
users_url += '?username=%s&exact=true' % username
try:
return json.loads(to_native(open_url(users_url, method='GET', headers=self.restheaders, timeout=self.connection_timeout,
validate_certs=self.validate_certs).read()))
except ValueError as e:
self.module.fail_json(msg='API returned incorrect JSON when trying to obtain the user for realm %s and username %s: %s'
% (realm, username, str(e)))
except Exception as e:
self.module.fail_json(msg='Could not obtain the user for realm %s and username %s: %s'
% (realm, username, str(e)))
def get_service_account_user_by_client_id(self, client_id, realm="master"):
""" Fetch a keycloak service account user within a realm based on its client_id.
If the user does not exist, None is returned.
:param client_id: clientId of the service account user to fetch.
:param realm: Realm in which the user resides; default 'master'
"""
cid = self.get_client_id(client_id, realm=realm)
service_account_user_url = URL_CLIENT_SERVICE_ACCOUNT_USER.format(url=self.baseurl, realm=realm, id=cid)
try:
return json.loads(to_native(open_url(service_account_user_url, method='GET', headers=self.restheaders, timeout=self.connection_timeout,
validate_certs=self.validate_certs).read()))
except ValueError as e:
self.module.fail_json(msg='API returned incorrect JSON when trying to obtain the service-account-user for realm %s and client_id %s: %s'
% (realm, client_id, str(e)))
except Exception as e:
self.module.fail_json(msg='Could not obtain the service-account-user for realm %s and client_id %s: %s'
% (realm, client_id, str(e)))
def add_user_rolemapping(self, uid, cid, role_rep, realm="master"):
""" Assign a realm or client role to a specified user on the Keycloak server.
:param uid: ID of the user roles are assigned to.
:param cid: ID of the client from which to obtain the rolemappings. If empty, roles are from the realm
:param role_rep: Representation of the role to assign.
:param realm: Realm from which to obtain the rolemappings.
:return: None.
"""
if cid is None:
user_realm_rolemappings_url = URL_REALM_ROLEMAPPINGS.format(url=self.baseurl, realm=realm, id=uid)
try:
open_url(user_realm_rolemappings_url, method="POST", http_agent=self.http_agent, headers=self.restheaders, data=json.dumps(role_rep),
validate_certs=self.validate_certs, timeout=self.connection_timeout)
except Exception as e:
self.module.fail_json(msg="Could not map roles to userId %s for realm %s and roles %s: %s"
% (uid, realm, json.dumps(role_rep), str(e)))
else:
user_client_rolemappings_url = URL_CLIENT_USER_ROLEMAPPINGS.format(url=self.baseurl, realm=realm, id=uid, client=cid)
try:
open_url(user_client_rolemappings_url, method="POST", http_agent=self.http_agent, headers=self.restheaders, data=json.dumps(role_rep),
validate_certs=self.validate_certs, timeout=self.connection_timeout)
except Exception as e:
self.module.fail_json(msg="Could not map roles to userId %s for client %s, realm %s and roles %s: %s"
% (cid, uid, realm, json.dumps(role_rep), str(e)))
def delete_user_rolemapping(self, uid, cid, role_rep, realm="master"):
""" Delete the rolemapping of a client in a specified user on the Keycloak server.
:param uid: ID of the user from which to remove the rolemappings.
:param cid: ID of the client from which to remove the rolemappings.
:param role_rep: Representation of the role to remove from rolemappings.
:param realm: Realm from which to remove the rolemappings.
:return: None.
"""
if cid is None:
user_realm_rolemappings_url = URL_REALM_ROLEMAPPINGS.format(url=self.baseurl, realm=realm, id=uid)
try:
open_url(user_realm_rolemappings_url, method="DELETE", http_agent=self.http_agent, headers=self.restheaders, data=json.dumps(role_rep),
validate_certs=self.validate_certs, timeout=self.connection_timeout)
except Exception as e:
self.module.fail_json(msg="Could not remove roles %s from userId %s, realm %s: %s"
% (json.dumps(role_rep), uid, realm, str(e)))
else:
user_client_rolemappings_url = URL_CLIENT_USER_ROLEMAPPINGS.format(url=self.baseurl, realm=realm, id=uid, client=cid)
try:
open_url(user_client_rolemappings_url, method="DELETE", http_agent=self.http_agent, headers=self.restheaders, data=json.dumps(role_rep),
validate_certs=self.validate_certs, timeout=self.connection_timeout)
except Exception as e:
self.module.fail_json(msg="Could not remove roles %s for client %s from userId %s, realm %s: %s"
% (json.dumps(role_rep), cid, uid, realm, str(e)))
def get_client_templates(self, realm='master'): def get_client_templates(self, realm='master'):
""" Obtains client template representations for client templates in a realm """ Obtains client template representations for client templates in a realm
@ -930,7 +1191,6 @@ class KeycloakAPI(object):
return json.loads(to_native(open_url(groups_url, method="GET", http_agent=self.http_agent, headers=self.restheaders, return json.loads(to_native(open_url(groups_url, method="GET", http_agent=self.http_agent, headers=self.restheaders,
timeout=self.connection_timeout, timeout=self.connection_timeout,
validate_certs=self.validate_certs).read())) validate_certs=self.validate_certs).read()))
except HTTPError as e: except HTTPError as e:
if e.code == 404: if e.code == 404:
return None return None

View file

@ -279,20 +279,20 @@ def main():
module.fail_json(msg='Either the `name` or `id` has to be specified on each role.') module.fail_json(msg='Either the `name` or `id` has to be specified on each role.')
# Fetch missing role_id # Fetch missing role_id
if role['id'] is None: if role['id'] is None:
role_id = kc.get_client_role_by_name(gid, cid, role['name'], realm=realm) role_id = kc.get_client_role_id_by_name(cid, role['name'], realm=realm)
if role_id is not None: if role_id is not None:
role['id'] = role_id role['id'] = role_id
else: else:
module.fail_json(msg='Could not fetch role %s:' % (role['name'])) module.fail_json(msg='Could not fetch role %s:' % (role['name']))
# Fetch missing role_name # Fetch missing role_name
else: else:
role['name'] = kc.get_client_rolemapping_by_id(gid, cid, role['id'], realm=realm)['name'] role['name'] = kc.get_client_group_rolemapping_by_id(gid, cid, role['id'], realm=realm)['name']
if role['name'] is None: if role['name'] is None:
module.fail_json(msg='Could not fetch role %s' % (role['id'])) module.fail_json(msg='Could not fetch role %s' % (role['id']))
# Get effective client-level role mappings # Get effective client-level role mappings
available_roles_before = kc.get_client_available_rolemappings(gid, cid, realm=realm) available_roles_before = kc.get_client_group_available_rolemappings(gid, cid, realm=realm)
assigned_roles_before = kc.get_client_composite_rolemappings(gid, cid, realm=realm) assigned_roles_before = kc.get_client_group_composite_rolemappings(gid, cid, realm=realm)
result['existing'] = assigned_roles_before result['existing'] = assigned_roles_before
result['proposed'] = roles result['proposed'] = roles
@ -326,7 +326,7 @@ def main():
module.exit_json(**result) module.exit_json(**result)
kc.add_group_rolemapping(gid, cid, update_roles, realm=realm) kc.add_group_rolemapping(gid, cid, update_roles, realm=realm)
result['msg'] = 'Roles %s assigned to group %s.' % (update_roles, group_name) result['msg'] = 'Roles %s assigned to group %s.' % (update_roles, group_name)
assigned_roles_after = kc.get_client_composite_rolemappings(gid, cid, realm=realm) assigned_roles_after = kc.get_client_group_composite_rolemappings(gid, cid, realm=realm)
result['end_state'] = assigned_roles_after result['end_state'] = assigned_roles_after
module.exit_json(**result) module.exit_json(**result)
else: else:
@ -338,7 +338,7 @@ def main():
module.exit_json(**result) module.exit_json(**result)
kc.delete_group_rolemapping(gid, cid, update_roles, realm=realm) kc.delete_group_rolemapping(gid, cid, update_roles, realm=realm)
result['msg'] = 'Roles %s removed from group %s.' % (update_roles, group_name) result['msg'] = 'Roles %s removed from group %s.' % (update_roles, group_name)
assigned_roles_after = kc.get_client_composite_rolemappings(gid, cid, realm=realm) assigned_roles_after = kc.get_client_group_composite_rolemappings(gid, cid, realm=realm)
result['end_state'] = assigned_roles_after result['end_state'] = assigned_roles_after
module.exit_json(**result) module.exit_json(**result)
# Do nothing # Do nothing

View file

@ -0,0 +1,401 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright (c) 2022, Dušan Marković (@bratwurzt)
# GNU General Public License v3.0+ (see COPYING 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_user_rolemapping
short_description: Allows administration of Keycloak user_rolemapping with the Keycloak API
version_added: 5.7.0
description:
- This module allows you to add, remove or modify Keycloak user_rolemapping with 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.
- When updating a user_rolemapping, where possible provide the role ID to the module. This removes a lookup
to the API to translate the name into the role ID.
options:
state:
description:
- State of the user_rolemapping.
- On C(present), the user_rolemapping will be created if it does not yet exist, or updated with the parameters you provide.
- On C(absent), the user_rolemapping will be removed if it exists.
default: 'present'
type: str
choices:
- present
- absent
realm:
type: str
description:
- They Keycloak realm under which this role_representation resides.
default: 'master'
target_username:
type: str
description:
- Username of the user roles are mapped to.
- This parameter is not required (can be replaced by uid for less API call).
uid:
type: str
description:
- ID of the user to be mapped.
- This parameter is not required for updating or deleting the rolemapping but
providing it will reduce the number of API calls required.
service_account_user_client_id:
type: str
description:
- Client ID of the service-account-user to be mapped.
- This parameter is not required for updating or deleting the rolemapping but
providing it will reduce the number of API calls required.
client_id:
type: str
description:
- Name of the client to be mapped (different than I(cid)).
- This parameter is required if I(cid) is not provided (can be replaced by I(cid)
to reduce the number of API calls that must be made).
cid:
type: str
description:
- ID of the client to be mapped.
- This parameter is not required for updating or deleting the rolemapping but
providing it will reduce the number of API calls required.
roles:
description:
- Roles to be mapped to the user.
type: list
elements: dict
suboptions:
name:
type: str
description:
- Name of the role representation.
- This parameter is required only when creating or updating the role_representation.
id:
type: str
description:
- The unique identifier for this role_representation.
- This parameter is not required for updating or deleting a role_representation but
providing it will reduce the number of API calls required.
extends_documentation_fragment:
- community.general.keycloak
author:
- Dušan Marković (@bratwurzt)
'''
EXAMPLES = '''
- name: Map a client role to a user, authentication with credentials
community.general.keycloak_user_rolemapping:
realm: MyCustomRealm
auth_client_id: admin-cli
auth_keycloak_url: https://auth.example.com/auth
auth_realm: master
auth_username: USERNAME
auth_password: PASSWORD
state: present
client_id: client1
user_id: user1Id
roles:
- name: role_name1
id: role_id1
- name: role_name2
id: role_id2
delegate_to: localhost
- name: Map a client role to a service account user for a client, authentication with credentials
community.general.keycloak_user_rolemapping:
realm: MyCustomRealm
auth_client_id: admin-cli
auth_keycloak_url: https://auth.example.com/auth
auth_realm: master
auth_username: USERNAME
auth_password: PASSWORD
state: present
client_id: client1
service_account_user_client_id: clientIdOfServiceAccount
roles:
- name: role_name1
id: role_id1
- name: role_name2
id: role_id2
delegate_to: localhost
- name: Map a client role to a user, authentication with token
community.general.keycloak_user_rolemapping:
realm: MyCustomRealm
auth_client_id: admin-cli
auth_keycloak_url: https://auth.example.com/auth
token: TOKEN
state: present
client_id: client1
target_username: user1
roles:
- name: role_name1
id: role_id1
- name: role_name2
id: role_id2
delegate_to: localhost
- name: Unmap client role from a user
community.general.keycloak_user_rolemapping:
realm: MyCustomRealm
auth_client_id: admin-cli
auth_keycloak_url: https://auth.example.com/auth
auth_realm: master
auth_username: USERNAME
auth_password: PASSWORD
state: absent
client_id: client1
uid: 70e3ae72-96b6-11e6-9056-9737fd4d0764
roles:
- name: role_name1
id: role_id1
- name: role_name2
id: role_id2
delegate_to: localhost
'''
RETURN = '''
msg:
description: Message as to what action was taken.
returned: always
type: str
sample: "Role role1 assigned to user user1."
proposed:
description: Representation of proposed client role mapping.
returned: always
type: dict
sample: {
clientId: "test"
}
existing:
description:
- Representation of existing client role mapping.
- The sample is truncated.
returned: always
type: dict
sample: {
"adminUrl": "http://www.example.com/admin_url",
"attributes": {
"request.object.signature.alg": "RS256",
}
}
end_state:
description:
- Representation of client role mapping after module execution.
- The sample is truncated.
returned: on success
type: dict
sample: {
"adminUrl": "http://www.example.com/admin_url",
"attributes": {
"request.object.signature.alg": "RS256",
}
}
'''
from ansible_collections.community.general.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
def main():
"""
Module execution
:return:
"""
argument_spec = keycloak_argument_spec()
roles_spec = dict(
name=dict(type='str'),
id=dict(type='str'),
)
meta_args = dict(
state=dict(default='present', choices=['present', 'absent']),
realm=dict(default='master'),
uid=dict(type='str'),
target_username=dict(type='str'),
service_account_user_client_id=dict(type='str'),
cid=dict(type='str'),
client_id=dict(type='str'),
roles=dict(type='list', elements='dict', options=roles_spec),
)
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'],
['uid', 'target_username', 'service_account_user_client_id']]),
required_together=([['auth_realm', 'auth_username', 'auth_password']]))
result = dict(changed=False, msg='', diff={}, proposed={}, existing={}, 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')
state = module.params.get('state')
cid = module.params.get('cid')
client_id = module.params.get('client_id')
uid = module.params.get('uid')
target_username = module.params.get('target_username')
service_account_user_client_id = module.params.get('service_account_user_client_id')
roles = module.params.get('roles')
# Check the parameters
if uid is None and target_username is None and service_account_user_client_id is None:
module.fail_json(msg='Either the `target_username`, `uid` or `service_account_user_client_id` has to be specified.')
# Get the potential missing parameters
if uid is None and service_account_user_client_id is None:
user_rep = kc.get_user_by_username(username=target_username, realm=realm)
if user_rep is not None:
uid = user_rep.get('id')
else:
module.fail_json(msg='Could not fetch user for username %s:' % target_username)
else:
if uid is None and target_username is None:
user_rep = kc.get_service_account_user_by_client_id(client_id=service_account_user_client_id, realm=realm)
if user_rep is not None:
uid = user_rep['id']
else:
module.fail_json(msg='Could not fetch service-account-user for client_id %s:' % target_username)
if cid is None and client_id is not None:
cid = kc.get_client_id(client_id=client_id, realm=realm)
if cid is None:
module.fail_json(msg='Could not fetch client %s:' % client_id)
if roles is None:
module.exit_json(msg="Nothing to do (no roles specified).")
else:
for role_index, role in enumerate(roles, start=0):
if role.get('name') is None and role.get('id') is None:
module.fail_json(msg='Either the `name` or `id` has to be specified on each role.')
# Fetch missing role_id
if role.get('id') is None:
if cid is None:
role_id = kc.get_realm_role(name=role.get('name'), realm=realm)['id']
else:
role_id = kc.get_client_role_id_by_name(cid=cid, name=role.get('name'), realm=realm)
if role_id is not None:
role['id'] = role_id
else:
module.fail_json(msg='Could not fetch role %s for client_id %s or realm %s' % (role.get('name'), client_id, realm))
# Fetch missing role_name
else:
if cid is None:
role['name'] = kc.get_realm_user_rolemapping_by_id(uid=uid, rid=role.get('id'), realm=realm)['name']
else:
role['name'] = kc.get_client_user_rolemapping_by_id(uid=uid, cid=cid, rid=role.get('id'), realm=realm)['name']
if role.get('name') is None:
module.fail_json(msg='Could not fetch role %s for client_id %s or realm %s' % (role.get('id'), client_id, realm))
# Get effective role mappings
if cid is None:
available_roles_before = kc.get_realm_user_available_rolemappings(uid=uid, realm=realm)
assigned_roles_before = kc.get_realm_user_composite_rolemappings(uid=uid, realm=realm)
else:
available_roles_before = kc.get_client_user_available_rolemappings(uid=uid, cid=cid, realm=realm)
assigned_roles_before = kc.get_client_user_composite_rolemappings(uid=uid, cid=cid, realm=realm)
result['existing'] = assigned_roles_before
result['proposed'] = roles
update_roles = []
for role_index, role in enumerate(roles, start=0):
# Fetch roles to assign if state present
if state == 'present':
for available_role in available_roles_before:
if role.get('name') == available_role.get('name'):
update_roles.append({
'id': role.get('id'),
'name': role.get('name'),
})
# Fetch roles to remove if state absent
else:
for assigned_role in assigned_roles_before:
if role.get('name') == assigned_role.get('name'):
update_roles.append({
'id': role.get('id'),
'name': role.get('name'),
})
if len(update_roles):
if state == 'present':
# Assign roles
result['changed'] = True
if module._diff:
result['diff'] = dict(before=assigned_roles_before, after=update_roles)
if module.check_mode:
module.exit_json(**result)
kc.add_user_rolemapping(uid=uid, cid=cid, role_rep=update_roles, realm=realm)
result['msg'] = 'Roles %s assigned to userId %s.' % (update_roles, uid)
if cid is None:
assigned_roles_after = kc.get_realm_user_composite_rolemappings(uid=uid, realm=realm)
else:
assigned_roles_after = kc.get_client_user_composite_rolemappings(uid=uid, cid=cid, realm=realm)
result['end_state'] = assigned_roles_after
module.exit_json(**result)
else:
# Remove mapping of role
result['changed'] = True
if module._diff:
result['diff'] = dict(before=assigned_roles_before, after=update_roles)
if module.check_mode:
module.exit_json(**result)
kc.delete_user_rolemapping(uid=uid, cid=cid, role_rep=update_roles, realm=realm)
result['msg'] = 'Roles %s removed from userId %s.' % (update_roles, uid)
if cid is None:
assigned_roles_after = kc.get_realm_user_composite_rolemappings(uid=uid, realm=realm)
else:
assigned_roles_after = kc.get_client_user_composite_rolemappings(uid=uid, cid=cid, realm=realm)
result['end_state'] = assigned_roles_after
module.exit_json(**result)
# Do nothing
else:
result['changed'] = False
result['msg'] = 'Nothing to do, roles %s are correctly mapped to user for username %s.' % (roles, target_username)
module.exit_json(**result)
if __name__ == '__main__':
main()

View file

@ -0,0 +1,4 @@
# Copyright (c) 2022, Dušan Marković (@bratwurzt)
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later
unsupported

View file

@ -0,0 +1,143 @@
# Copyright (c) 2022, Dušan Marković (@bratwurzt)
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later
- name: Create realm
community.general.keycloak_realm:
auth_keycloak_url: "{{ url }}"
auth_realm: "{{ admin_realm }}"
auth_username: "{{ admin_user }}"
auth_password: "{{ admin_password }}"
id: "{{ realm }}"
realm: "{{ realm }}"
state: present
- name: Create client
community.general.keycloak_client:
auth_keycloak_url: "{{ url }}"
auth_realm: "{{ admin_realm }}"
auth_username: "{{ admin_user }}"
auth_password: "{{ admin_password }}"
realm: "{{ realm }}"
client_id: "{{ client_id }}"
service_accounts_enabled: True
state: present
register: client
- name: Create new realm role
community.general.keycloak_role:
auth_keycloak_url: "{{ url }}"
auth_realm: "{{ admin_realm }}"
auth_username: "{{ admin_user }}"
auth_password: "{{ admin_password }}"
realm: "{{ realm }}"
name: "{{ role }}"
description: "{{ description_1 }}"
state: present
- name: Map a realm role to client service account
vars:
- roles: [ {'name': '{{ role }}'} ]
community.general.keycloak_user_rolemapping:
auth_keycloak_url: "{{ url }}"
auth_realm: "{{ admin_realm }}"
auth_username: "{{ admin_user }}"
auth_password: "{{ admin_password }}"
realm: "{{ realm }}"
service_account_user_client_id: "{{ client_id }}"
roles: "{{ roles }}"
state: present
register: result
- name: Assert realm role is assigned
assert:
that:
- result is changed
- result.end_state | selectattr("clientRole", "eq", false) | selectattr("name", "eq", "{{role}}") | list | count > 0
- name: Unmap a realm role from client service account
vars:
- roles: [ {'name': '{{ role }}'} ]
community.general.keycloak_user_rolemapping:
auth_keycloak_url: "{{ url }}"
auth_realm: "{{ admin_realm }}"
auth_username: "{{ admin_user }}"
auth_password: "{{ admin_password }}"
realm: "{{ realm }}"
service_account_user_client_id: "{{ client_id }}"
roles: "{{ roles }}"
state: absent
register: result
- name: Assert realm role is unassigned
assert:
that:
- result is changed
- (result.end_state | length) == (result.existing | length) - 1
- result.existing | selectattr("clientRole", "eq", false) | selectattr("name", "eq", "{{role}}") | list | count > 0
- result.end_state | selectattr("clientRole", "eq", false) | selectattr("name", "eq", "{{role}}") | list | count == 0
- name: Delete existing realm role
community.general.keycloak_role:
auth_keycloak_url: "{{ url }}"
auth_realm: "{{ admin_realm }}"
auth_username: "{{ admin_user }}"
auth_password: "{{ admin_password }}"
realm: "{{ realm }}"
name: "{{ role }}"
state: absent
- name: Create new client role
community.general.keycloak_role:
auth_keycloak_url: "{{ url }}"
auth_realm: "{{ admin_realm }}"
auth_username: "{{ admin_user }}"
auth_password: "{{ admin_password }}"
realm: "{{ realm }}"
client_id: "{{ client_id }}"
name: "{{ role }}"
description: "{{ description_1 }}"
state: present
- name: Map a client role to client service account
vars:
- roles: [ {'name': '{{ role }}'} ]
community.general.keycloak_user_rolemapping:
auth_keycloak_url: "{{ url }}"
auth_realm: "{{ admin_realm }}"
auth_username: "{{ admin_user }}"
auth_password: "{{ admin_password }}"
realm: "{{ realm }}"
client_id: "{{ client_id }}"
service_account_user_client_id: "{{ client_id }}"
roles: "{{ roles }}"
state: present
register: result
- name: Assert client role is assigned
assert:
that:
- result is changed
- result.end_state | selectattr("clientRole", "eq", true) | selectattr("name", "eq", "{{role}}") | list | count > 0
- name: Unmap a client role from client service account
vars:
- roles: [ {'name': '{{ role }}'} ]
community.general.keycloak_user_rolemapping:
auth_keycloak_url: "{{ url }}"
auth_realm: "{{ admin_realm }}"
auth_username: "{{ admin_user }}"
auth_password: "{{ admin_password }}"
realm: "{{ realm }}"
client_id: "{{ client_id }}"
service_account_user_client_id: "{{ client_id }}"
roles: "{{ roles }}"
state: absent
register: result
- name: Assert client role is unassigned
assert:
that:
- result is changed
- result.end_state == []
- result.existing | selectattr("clientRole", "eq", true) | selectattr("name", "eq", "{{role}}") | list | count > 0

View file

@ -0,0 +1,14 @@
---
# Copyright (c) 2022, Dušan Marković (@bratwurzt)
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later
url: http://localhost:8080/auth
admin_realm: master
admin_user: admin
admin_password: password
realm: myrealm
client_id: myclient
role: myrole
description_1: desc 1
description_2: desc 2

View file

@ -21,9 +21,9 @@ from ansible.module_utils.six import StringIO
@contextmanager @contextmanager
def patch_keycloak_api(get_group_by_name=None, get_client_id=None, get_client_role_by_name=None, def patch_keycloak_api(get_group_by_name=None, get_client_id=None, get_client_role_id_by_name=None,
get_client_rolemapping_by_id=None, get_client_available_rolemappings=None, get_client_group_rolemapping_by_id=None, get_client_group_available_rolemappings=None,
get_client_composite_rolemappings=None, add_group_rolemapping=None, get_client_group_composite_rolemappings=None, add_group_rolemapping=None,
delete_group_rolemapping=None): delete_group_rolemapping=None):
"""Mock context manager for patching the methods in PwPolicyIPAClient that contact the IPA server """Mock context manager for patching the methods in PwPolicyIPAClient that contact the IPA server
@ -44,21 +44,21 @@ def patch_keycloak_api(get_group_by_name=None, get_client_id=None, get_client_ro
side_effect=get_group_by_name) as mock_get_group_by_name: side_effect=get_group_by_name) as mock_get_group_by_name:
with patch.object(obj, 'get_client_id', with patch.object(obj, 'get_client_id',
side_effect=get_client_id) as mock_get_client_id: side_effect=get_client_id) as mock_get_client_id:
with patch.object(obj, 'get_client_role_by_name', with patch.object(obj, 'get_client_role_id_by_name',
side_effect=get_client_role_by_name) as mock_get_client_role_by_name: side_effect=get_client_role_id_by_name) as mock_get_client_role_id_by_name:
with patch.object(obj, 'get_client_rolemapping_by_id', with patch.object(obj, 'get_client_group_rolemapping_by_id',
side_effect=get_client_rolemapping_by_id) as mock_get_client_rolemapping_by_id: side_effect=get_client_group_rolemapping_by_id) as mock_get_client_group_rolemapping_by_id:
with patch.object(obj, 'get_client_available_rolemappings', with patch.object(obj, 'get_client_group_available_rolemappings',
side_effect=get_client_available_rolemappings) as mock_get_client_available_rolemappings: side_effect=get_client_group_available_rolemappings) as mock_get_client_group_available_rolemappings:
with patch.object(obj, 'get_client_composite_rolemappings', with patch.object(obj, 'get_client_group_composite_rolemappings',
side_effect=get_client_composite_rolemappings) as mock_get_client_composite_rolemappings: side_effect=get_client_group_composite_rolemappings) as mock_get_client_group_composite_rolemappings:
with patch.object(obj, 'add_group_rolemapping', with patch.object(obj, 'add_group_rolemapping',
side_effect=add_group_rolemapping) as mock_add_group_rolemapping: side_effect=add_group_rolemapping) as mock_add_group_rolemapping:
with patch.object(obj, 'delete_group_rolemapping', with patch.object(obj, 'delete_group_rolemapping',
side_effect=delete_group_rolemapping) as mock_delete_group_rolemapping: side_effect=delete_group_rolemapping) as mock_delete_group_rolemapping:
yield mock_get_group_by_name, mock_get_client_id, mock_get_client_role_by_name, mock_add_group_rolemapping, \ yield mock_get_group_by_name, mock_get_client_id, mock_get_client_role_id_by_name, mock_add_group_rolemapping, \
mock_get_client_rolemapping_by_id, mock_get_client_available_rolemappings, mock_get_client_composite_rolemappings, \ mock_get_client_group_rolemapping_by_id, mock_get_client_group_available_rolemappings, \
mock_delete_group_rolemapping mock_get_client_group_composite_rolemappings, mock_delete_group_rolemapping
def get_response(object_with_future_response, method, get_id_call_count): def get_response(object_with_future_response, method, get_id_call_count):
@ -144,8 +144,8 @@ class TestKeycloakRealm(ModuleTestCase):
"subGroups": "[]" "subGroups": "[]"
}] }]
return_value_get_client_id = "c0f8490c-b224-4737-a567-20223e4c1727" return_value_get_client_id = "c0f8490c-b224-4737-a567-20223e4c1727"
return_value_get_client_role_by_name = "e91af074-cfd5-40ee-8ef5-ae0ae1ce69fe" return_value_get_client_role_id_by_name = "e91af074-cfd5-40ee-8ef5-ae0ae1ce69fe"
return_value_get_client_available_rolemappings = [[ return_value_get_client_group_available_rolemappings = [[
{ {
"clientRole": "true", "clientRole": "true",
"composite": "false", "composite": "false",
@ -161,7 +161,7 @@ class TestKeycloakRealm(ModuleTestCase):
"name": "test_role1" "name": "test_role1"
} }
]] ]]
return_value_get_client_composite_rolemappings = [ return_value_get_client_group_composite_rolemappings = [
None, None,
[ [
{ {
@ -189,11 +189,11 @@ class TestKeycloakRealm(ModuleTestCase):
with mock_good_connection(): with mock_good_connection():
with patch_keycloak_api(get_group_by_name=return_value_get_group_by_name, get_client_id=return_value_get_client_id, with patch_keycloak_api(get_group_by_name=return_value_get_group_by_name, get_client_id=return_value_get_client_id,
get_client_role_by_name=return_value_get_client_role_by_name, get_client_role_id_by_name=return_value_get_client_role_id_by_name,
get_client_available_rolemappings=return_value_get_client_available_rolemappings, get_client_group_available_rolemappings=return_value_get_client_group_available_rolemappings,
get_client_composite_rolemappings=return_value_get_client_composite_rolemappings) \ get_client_group_composite_rolemappings=return_value_get_client_group_composite_rolemappings) \
as (mock_get_group_by_name, mock_get_client_id, mock_get_client_role_by_name, mock_add_group_rolemapping, as (mock_get_group_by_name, mock_get_client_id, mock_get_client_role_id_by_name, mock_add_group_rolemapping,
mock_get_client_rolemapping_by_id, mock_get_client_available_rolemappings, mock_get_client_composite_rolemappings, mock_get_client_group_rolemapping_by_id, mock_get_client_group_available_rolemappings, mock_get_client_group_composite_rolemappings,
mock_delete_group_rolemapping): mock_delete_group_rolemapping):
with self.assertRaises(AnsibleExitJson) as exec_info: with self.assertRaises(AnsibleExitJson) as exec_info:
self.module.main() self.module.main()
@ -201,9 +201,9 @@ class TestKeycloakRealm(ModuleTestCase):
self.assertEqual(mock_get_group_by_name.call_count, 1) self.assertEqual(mock_get_group_by_name.call_count, 1)
self.assertEqual(mock_get_client_id.call_count, 1) self.assertEqual(mock_get_client_id.call_count, 1)
self.assertEqual(mock_add_group_rolemapping.call_count, 1) self.assertEqual(mock_add_group_rolemapping.call_count, 1)
self.assertEqual(mock_get_client_rolemapping_by_id.call_count, 0) self.assertEqual(mock_get_client_group_rolemapping_by_id.call_count, 0)
self.assertEqual(mock_get_client_available_rolemappings.call_count, 1) self.assertEqual(mock_get_client_group_available_rolemappings.call_count, 1)
self.assertEqual(mock_get_client_composite_rolemappings.call_count, 2) self.assertEqual(mock_get_client_group_composite_rolemappings.call_count, 2)
self.assertEqual(mock_delete_group_rolemapping.call_count, 0) self.assertEqual(mock_delete_group_rolemapping.call_count, 0)
# Verify that the module's changed status matches what is expected # Verify that the module's changed status matches what is expected
@ -246,9 +246,9 @@ class TestKeycloakRealm(ModuleTestCase):
"subGroups": "[]" "subGroups": "[]"
}] }]
return_value_get_client_id = "c0f8490c-b224-4737-a567-20223e4c1727" return_value_get_client_id = "c0f8490c-b224-4737-a567-20223e4c1727"
return_value_get_client_role_by_name = "e91af074-cfd5-40ee-8ef5-ae0ae1ce69fe" return_value_get_client_role_id_by_name = "e91af074-cfd5-40ee-8ef5-ae0ae1ce69fe"
return_value_get_client_available_rolemappings = [[]] return_value_get_client_group_available_rolemappings = [[]]
return_value_get_client_composite_rolemappings = [[ return_value_get_client_group_composite_rolemappings = [[
{ {
"clientRole": "true", "clientRole": "true",
"composite": "false", "composite": "false",
@ -273,11 +273,11 @@ class TestKeycloakRealm(ModuleTestCase):
with mock_good_connection(): with mock_good_connection():
with patch_keycloak_api(get_group_by_name=return_value_get_group_by_name, get_client_id=return_value_get_client_id, with patch_keycloak_api(get_group_by_name=return_value_get_group_by_name, get_client_id=return_value_get_client_id,
get_client_role_by_name=return_value_get_client_role_by_name, get_client_role_id_by_name=return_value_get_client_role_id_by_name,
get_client_available_rolemappings=return_value_get_client_available_rolemappings, get_client_group_available_rolemappings=return_value_get_client_group_available_rolemappings,
get_client_composite_rolemappings=return_value_get_client_composite_rolemappings) \ get_client_group_composite_rolemappings=return_value_get_client_group_composite_rolemappings) \
as (mock_get_group_by_name, mock_get_client_id, mock_get_client_role_by_name, mock_add_group_rolemapping, as (mock_get_group_by_name, mock_get_client_id, mock_get_client_role_id_by_name, mock_add_group_rolemapping,
mock_get_client_rolemapping_by_id, mock_get_client_available_rolemappings, mock_get_client_composite_rolemappings, mock_get_client_group_rolemapping_by_id, mock_get_client_group_available_rolemappings, mock_get_client_group_composite_rolemappings,
mock_delete_group_rolemapping): mock_delete_group_rolemapping):
with self.assertRaises(AnsibleExitJson) as exec_info: with self.assertRaises(AnsibleExitJson) as exec_info:
self.module.main() self.module.main()
@ -285,9 +285,9 @@ class TestKeycloakRealm(ModuleTestCase):
self.assertEqual(mock_get_group_by_name.call_count, 1) self.assertEqual(mock_get_group_by_name.call_count, 1)
self.assertEqual(mock_get_client_id.call_count, 1) self.assertEqual(mock_get_client_id.call_count, 1)
self.assertEqual(mock_add_group_rolemapping.call_count, 0) self.assertEqual(mock_add_group_rolemapping.call_count, 0)
self.assertEqual(mock_get_client_rolemapping_by_id.call_count, 0) self.assertEqual(mock_get_client_group_rolemapping_by_id.call_count, 0)
self.assertEqual(mock_get_client_available_rolemappings.call_count, 1) self.assertEqual(mock_get_client_group_available_rolemappings.call_count, 1)
self.assertEqual(mock_get_client_composite_rolemappings.call_count, 1) self.assertEqual(mock_get_client_group_composite_rolemappings.call_count, 1)
self.assertEqual(mock_delete_group_rolemapping.call_count, 0) self.assertEqual(mock_delete_group_rolemapping.call_count, 0)
# Verify that the module's changed status matches what is expected # Verify that the module's changed status matches what is expected
@ -330,8 +330,8 @@ class TestKeycloakRealm(ModuleTestCase):
"subGroups": "[]" "subGroups": "[]"
}] }]
return_value_get_client_id = "c0f8490c-b224-4737-a567-20223e4c1727" return_value_get_client_id = "c0f8490c-b224-4737-a567-20223e4c1727"
return_value_get_client_role_by_name = "e91af074-cfd5-40ee-8ef5-ae0ae1ce69fe" return_value_get_client_role_id_by_name = "e91af074-cfd5-40ee-8ef5-ae0ae1ce69fe"
return_value_get_client_available_rolemappings = [[ return_value_get_client_group_available_rolemappings = [[
{ {
"clientRole": "true", "clientRole": "true",
"composite": "false", "composite": "false",
@ -347,7 +347,7 @@ class TestKeycloakRealm(ModuleTestCase):
"name": "test_role1" "name": "test_role1"
} }
]] ]]
return_value_get_client_composite_rolemappings = [ return_value_get_client_group_composite_rolemappings = [
None, None,
[ [
{ {
@ -375,11 +375,11 @@ class TestKeycloakRealm(ModuleTestCase):
with mock_good_connection(): with mock_good_connection():
with patch_keycloak_api(get_group_by_name=return_value_get_group_by_name, get_client_id=return_value_get_client_id, with patch_keycloak_api(get_group_by_name=return_value_get_group_by_name, get_client_id=return_value_get_client_id,
get_client_role_by_name=return_value_get_client_role_by_name, get_client_role_id_by_name=return_value_get_client_role_id_by_name,
get_client_available_rolemappings=return_value_get_client_available_rolemappings, get_client_group_available_rolemappings=return_value_get_client_group_available_rolemappings,
get_client_composite_rolemappings=return_value_get_client_composite_rolemappings) \ get_client_group_composite_rolemappings=return_value_get_client_group_composite_rolemappings) \
as (mock_get_group_by_name, mock_get_client_id, mock_get_client_role_by_name, mock_add_group_rolemapping, as (mock_get_group_by_name, mock_get_client_id, mock_get_client_role_id_by_name, mock_add_group_rolemapping,
mock_get_client_rolemapping_by_id, mock_get_client_available_rolemappings, mock_get_client_composite_rolemappings, mock_get_client_group_rolemapping_by_id, mock_get_client_group_available_rolemappings, mock_get_client_group_composite_rolemappings,
mock_delete_group_rolemapping): mock_delete_group_rolemapping):
with self.assertRaises(AnsibleExitJson) as exec_info: with self.assertRaises(AnsibleExitJson) as exec_info:
self.module.main() self.module.main()
@ -387,9 +387,9 @@ class TestKeycloakRealm(ModuleTestCase):
self.assertEqual(mock_get_group_by_name.call_count, 0) self.assertEqual(mock_get_group_by_name.call_count, 0)
self.assertEqual(mock_get_client_id.call_count, 0) self.assertEqual(mock_get_client_id.call_count, 0)
self.assertEqual(mock_add_group_rolemapping.call_count, 1) self.assertEqual(mock_add_group_rolemapping.call_count, 1)
self.assertEqual(mock_get_client_rolemapping_by_id.call_count, 0) self.assertEqual(mock_get_client_group_rolemapping_by_id.call_count, 0)
self.assertEqual(mock_get_client_available_rolemappings.call_count, 1) self.assertEqual(mock_get_client_group_available_rolemappings.call_count, 1)
self.assertEqual(mock_get_client_composite_rolemappings.call_count, 2) self.assertEqual(mock_get_client_group_composite_rolemappings.call_count, 2)
self.assertEqual(mock_delete_group_rolemapping.call_count, 0) self.assertEqual(mock_delete_group_rolemapping.call_count, 0)
# Verify that the module's changed status matches what is expected # Verify that the module's changed status matches what is expected
@ -432,9 +432,9 @@ class TestKeycloakRealm(ModuleTestCase):
"subGroups": "[]" "subGroups": "[]"
}] }]
return_value_get_client_id = "c0f8490c-b224-4737-a567-20223e4c1727" return_value_get_client_id = "c0f8490c-b224-4737-a567-20223e4c1727"
return_value_get_client_role_by_name = "e91af074-cfd5-40ee-8ef5-ae0ae1ce69fe" return_value_get_client_role_id_by_name = "e91af074-cfd5-40ee-8ef5-ae0ae1ce69fe"
return_value_get_client_available_rolemappings = [[]] return_value_get_client_group_available_rolemappings = [[]]
return_value_get_client_composite_rolemappings = [ return_value_get_client_group_composite_rolemappings = [
[ [
{ {
"clientRole": "true", "clientRole": "true",
@ -462,11 +462,11 @@ class TestKeycloakRealm(ModuleTestCase):
with mock_good_connection(): with mock_good_connection():
with patch_keycloak_api(get_group_by_name=return_value_get_group_by_name, get_client_id=return_value_get_client_id, with patch_keycloak_api(get_group_by_name=return_value_get_group_by_name, get_client_id=return_value_get_client_id,
get_client_role_by_name=return_value_get_client_role_by_name, get_client_role_id_by_name=return_value_get_client_role_id_by_name,
get_client_available_rolemappings=return_value_get_client_available_rolemappings, get_client_group_available_rolemappings=return_value_get_client_group_available_rolemappings,
get_client_composite_rolemappings=return_value_get_client_composite_rolemappings) \ get_client_group_composite_rolemappings=return_value_get_client_group_composite_rolemappings) \
as (mock_get_group_by_name, mock_get_client_id, mock_get_client_role_by_name, mock_add_group_rolemapping, as (mock_get_group_by_name, mock_get_client_id, mock_get_client_role_id_by_name, mock_add_group_rolemapping,
mock_get_client_rolemapping_by_id, mock_get_client_available_rolemappings, mock_get_client_composite_rolemappings, mock_get_client_group_rolemapping_by_id, mock_get_client_group_available_rolemappings, mock_get_client_group_composite_rolemappings,
mock_delete_group_rolemapping): mock_delete_group_rolemapping):
with self.assertRaises(AnsibleExitJson) as exec_info: with self.assertRaises(AnsibleExitJson) as exec_info:
self.module.main() self.module.main()
@ -474,9 +474,9 @@ class TestKeycloakRealm(ModuleTestCase):
self.assertEqual(mock_get_group_by_name.call_count, 1) self.assertEqual(mock_get_group_by_name.call_count, 1)
self.assertEqual(mock_get_client_id.call_count, 1) self.assertEqual(mock_get_client_id.call_count, 1)
self.assertEqual(mock_add_group_rolemapping.call_count, 0) self.assertEqual(mock_add_group_rolemapping.call_count, 0)
self.assertEqual(mock_get_client_rolemapping_by_id.call_count, 0) self.assertEqual(mock_get_client_group_rolemapping_by_id.call_count, 0)
self.assertEqual(mock_get_client_available_rolemappings.call_count, 1) self.assertEqual(mock_get_client_group_available_rolemappings.call_count, 1)
self.assertEqual(mock_get_client_composite_rolemappings.call_count, 2) self.assertEqual(mock_get_client_group_composite_rolemappings.call_count, 2)
self.assertEqual(mock_delete_group_rolemapping.call_count, 1) self.assertEqual(mock_delete_group_rolemapping.call_count, 1)
# Verify that the module's changed status matches what is expected # Verify that the module's changed status matches what is expected
@ -519,8 +519,8 @@ class TestKeycloakRealm(ModuleTestCase):
"subGroups": "[]" "subGroups": "[]"
}] }]
return_value_get_client_id = "c0f8490c-b224-4737-a567-20223e4c1727" return_value_get_client_id = "c0f8490c-b224-4737-a567-20223e4c1727"
return_value_get_client_role_by_name = "e91af074-cfd5-40ee-8ef5-ae0ae1ce69fe" return_value_get_client_role_id_by_name = "e91af074-cfd5-40ee-8ef5-ae0ae1ce69fe"
return_value_get_client_available_rolemappings = [ return_value_get_client_group_available_rolemappings = [
[ [
{ {
"clientRole": "true", "clientRole": "true",
@ -538,7 +538,7 @@ class TestKeycloakRealm(ModuleTestCase):
} }
] ]
] ]
return_value_get_client_composite_rolemappings = [[]] return_value_get_client_group_composite_rolemappings = [[]]
changed = False changed = False
@ -548,11 +548,11 @@ class TestKeycloakRealm(ModuleTestCase):
with mock_good_connection(): with mock_good_connection():
with patch_keycloak_api(get_group_by_name=return_value_get_group_by_name, get_client_id=return_value_get_client_id, with patch_keycloak_api(get_group_by_name=return_value_get_group_by_name, get_client_id=return_value_get_client_id,
get_client_role_by_name=return_value_get_client_role_by_name, get_client_role_id_by_name=return_value_get_client_role_id_by_name,
get_client_available_rolemappings=return_value_get_client_available_rolemappings, get_client_group_available_rolemappings=return_value_get_client_group_available_rolemappings,
get_client_composite_rolemappings=return_value_get_client_composite_rolemappings) \ get_client_group_composite_rolemappings=return_value_get_client_group_composite_rolemappings) \
as (mock_get_group_by_name, mock_get_client_id, mock_get_client_role_by_name, mock_add_group_rolemapping, as (mock_get_group_by_name, mock_get_client_id, mock_get_client_role_id_by_name, mock_add_group_rolemapping,
mock_get_client_rolemapping_by_id, mock_get_client_available_rolemappings, mock_get_client_composite_rolemappings, mock_get_client_group_rolemapping_by_id, mock_get_client_group_available_rolemappings, mock_get_client_group_composite_rolemappings,
mock_delete_group_rolemapping): mock_delete_group_rolemapping):
with self.assertRaises(AnsibleExitJson) as exec_info: with self.assertRaises(AnsibleExitJson) as exec_info:
self.module.main() self.module.main()
@ -560,9 +560,9 @@ class TestKeycloakRealm(ModuleTestCase):
self.assertEqual(mock_get_group_by_name.call_count, 1) self.assertEqual(mock_get_group_by_name.call_count, 1)
self.assertEqual(mock_get_client_id.call_count, 1) self.assertEqual(mock_get_client_id.call_count, 1)
self.assertEqual(mock_add_group_rolemapping.call_count, 0) self.assertEqual(mock_add_group_rolemapping.call_count, 0)
self.assertEqual(mock_get_client_rolemapping_by_id.call_count, 0) self.assertEqual(mock_get_client_group_rolemapping_by_id.call_count, 0)
self.assertEqual(mock_get_client_available_rolemappings.call_count, 1) self.assertEqual(mock_get_client_group_available_rolemappings.call_count, 1)
self.assertEqual(mock_get_client_composite_rolemappings.call_count, 1) self.assertEqual(mock_get_client_group_composite_rolemappings.call_count, 1)
self.assertEqual(mock_delete_group_rolemapping.call_count, 0) self.assertEqual(mock_delete_group_rolemapping.call_count, 0)
# Verify that the module's changed status matches what is expected # Verify that the module's changed status matches what is expected