From 2cac3ae879172c59149637a80c4597b1a2e156de Mon Sep 17 00:00:00 2001 From: bratwurzt Date: Sat, 1 Oct 2022 18:16:47 +0200 Subject: [PATCH] New Module: Keycloak User Rolemapping (#4898) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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č Co-authored-by: ahussey-redhat <93101976+ahussey-redhat@users.noreply.github.com> --- .github/BOTMETA.yml | 2 + meta/runtime.yml | 2 + .../identity/keycloak/keycloak.py | 290 ++++++++++++- .../keycloak/keycloak_client_rolemapping.py | 12 +- .../keycloak/keycloak_user_rolemapping.py | 401 ++++++++++++++++++ .../targets/keycloak_user_rolemapping/aliases | 4 + .../keycloak_user_rolemapping/tasks/main.yml | 143 +++++++ .../keycloak_user_rolemapping/vars/main.yml | 14 + .../test_keycloak_client_rolemapping.py | 138 +++--- 9 files changed, 916 insertions(+), 90 deletions(-) create mode 100644 plugins/modules/identity/keycloak/keycloak_user_rolemapping.py create mode 100644 tests/integration/targets/keycloak_user_rolemapping/aliases create mode 100644 tests/integration/targets/keycloak_user_rolemapping/tasks/main.yml create mode 100644 tests/integration/targets/keycloak_user_rolemapping/vars/main.yml diff --git a/.github/BOTMETA.yml b/.github/BOTMETA.yml index 1e2fc66a19..0d35e1b5fd 100644 --- a/.github/BOTMETA.yml +++ b/.github/BOTMETA.yml @@ -589,6 +589,8 @@ files: maintainers: Gaetan2907 $modules/identity/keycloak/keycloak_client_rolemapping.py: maintainers: Gaetan2907 + $modules/identity/keycloak/keycloak_user_rolemapping.py: + maintainers: bratwurzt $modules/identity/keycloak/keycloak_group.py: maintainers: adamgoossens $modules/identity/keycloak/keycloak_identity_provider.py: diff --git a/meta/runtime.yml b/meta/runtime.yml index c93f00d760..2685df53a7 100644 --- a/meta/runtime.yml +++ b/meta/runtime.yml @@ -612,6 +612,8 @@ plugin_routing: redirect: community.general.identity.keycloak.keycloak_role keycloak_user_federation: redirect: community.general.identity.keycloak.keycloak_user_federation + keycloak_user_rolemapping: + redirect: community.general.identity.keycloak.keycloak_user_rolemapping keyring: redirect: community.general.system.keyring keyring_info: diff --git a/plugins/module_utils/identity/keycloak/keycloak.py b/plugins/module_utils/identity/keycloak/keycloak.py index 1769d6c48f..078925ef71 100644 --- a/plugins/module_utils/identity/keycloak/keycloak.py +++ b/plugins/module_utils/identity/keycloak/keycloak.py @@ -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_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_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_CLIENTTEMPLATES = "{url}/admin/realms/{realm}/client-templates" 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_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_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 = "{url}/admin/realms/{realm}/groups/{id}/role-mappings/clients/{client}" +URL_CLIENT_GROUP_ROLEMAPPINGS_AVAILABLE = "{url}/admin/realms/{realm}/groups/{id}/role-mappings/clients/{client}/available" +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_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" % (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. - :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 name: Name of the role. :param realm: Realm from which to obtain the rolemappings. @@ -461,7 +473,7 @@ class KeycloakAPI(object): return role['id'] 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 :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 :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: rolemappings = json.loads(to_native(open_url(rolemappings_url, method="GET", http_agent=self.http_agent, headers=self.restheaders, timeout=self.connection_timeout, @@ -483,7 +495,7 @@ class KeycloakAPI(object): % (cid, gid, realm, str(e))) 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. :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. :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: return json.loads(to_native(open_url(available_rolemappings_url, method="GET", http_agent=self.http_agent, headers=self.restheaders, 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" % (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. :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. :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: - 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, validate_certs=self.validate_certs).read())) except Exception as e: self.module.fail_json(msg="Could not fetch available rolemappings for client %s in group %s, realm %s: %s" % (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"): """ 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. :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: 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) @@ -543,7 +604,7 @@ class KeycloakAPI(object): :param realm: Realm from which to obtain the rolemappings. :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: open_url(available_rolemappings_url, method="DELETE", http_agent=self.http_agent, headers=self.restheaders, 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" % (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'): """ 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, timeout=self.connection_timeout, validate_certs=self.validate_certs).read())) - except HTTPError as e: if e.code == 404: return None diff --git a/plugins/modules/identity/keycloak/keycloak_client_rolemapping.py b/plugins/modules/identity/keycloak/keycloak_client_rolemapping.py index c4dde9bee1..4f1f9b0d0f 100644 --- a/plugins/modules/identity/keycloak/keycloak_client_rolemapping.py +++ b/plugins/modules/identity/keycloak/keycloak_client_rolemapping.py @@ -279,20 +279,20 @@ def main(): module.fail_json(msg='Either the `name` or `id` has to be specified on each role.') # Fetch missing role_id 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: role['id'] = role_id else: module.fail_json(msg='Could not fetch role %s:' % (role['name'])) # Fetch missing role_name 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: module.fail_json(msg='Could not fetch role %s' % (role['id'])) # Get effective client-level role mappings - available_roles_before = kc.get_client_available_rolemappings(gid, cid, realm=realm) - assigned_roles_before = kc.get_client_composite_rolemappings(gid, cid, realm=realm) + available_roles_before = kc.get_client_group_available_rolemappings(gid, cid, realm=realm) + assigned_roles_before = kc.get_client_group_composite_rolemappings(gid, cid, realm=realm) result['existing'] = assigned_roles_before result['proposed'] = roles @@ -326,7 +326,7 @@ def main(): module.exit_json(**result) kc.add_group_rolemapping(gid, cid, update_roles, realm=realm) 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 module.exit_json(**result) else: @@ -338,7 +338,7 @@ def main(): module.exit_json(**result) kc.delete_group_rolemapping(gid, cid, update_roles, realm=realm) 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 module.exit_json(**result) # Do nothing diff --git a/plugins/modules/identity/keycloak/keycloak_user_rolemapping.py b/plugins/modules/identity/keycloak/keycloak_user_rolemapping.py new file mode 100644 index 0000000000..72d403c637 --- /dev/null +++ b/plugins/modules/identity/keycloak/keycloak_user_rolemapping.py @@ -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() diff --git a/tests/integration/targets/keycloak_user_rolemapping/aliases b/tests/integration/targets/keycloak_user_rolemapping/aliases new file mode 100644 index 0000000000..cdeae1417a --- /dev/null +++ b/tests/integration/targets/keycloak_user_rolemapping/aliases @@ -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 diff --git a/tests/integration/targets/keycloak_user_rolemapping/tasks/main.yml b/tests/integration/targets/keycloak_user_rolemapping/tasks/main.yml new file mode 100644 index 0000000000..e4625cb06e --- /dev/null +++ b/tests/integration/targets/keycloak_user_rolemapping/tasks/main.yml @@ -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 diff --git a/tests/integration/targets/keycloak_user_rolemapping/vars/main.yml b/tests/integration/targets/keycloak_user_rolemapping/vars/main.yml new file mode 100644 index 0000000000..385dbea44a --- /dev/null +++ b/tests/integration/targets/keycloak_user_rolemapping/vars/main.yml @@ -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 diff --git a/tests/unit/plugins/modules/identity/keycloak/test_keycloak_client_rolemapping.py b/tests/unit/plugins/modules/identity/keycloak/test_keycloak_client_rolemapping.py index d3a3516660..72fc7850d5 100644 --- a/tests/unit/plugins/modules/identity/keycloak/test_keycloak_client_rolemapping.py +++ b/tests/unit/plugins/modules/identity/keycloak/test_keycloak_client_rolemapping.py @@ -21,9 +21,9 @@ from ansible.module_utils.six import StringIO @contextmanager -def patch_keycloak_api(get_group_by_name=None, get_client_id=None, get_client_role_by_name=None, - get_client_rolemapping_by_id=None, get_client_available_rolemappings=None, - get_client_composite_rolemappings=None, add_group_rolemapping=None, +def patch_keycloak_api(get_group_by_name=None, get_client_id=None, get_client_role_id_by_name=None, + get_client_group_rolemapping_by_id=None, get_client_group_available_rolemappings=None, + get_client_group_composite_rolemappings=None, add_group_rolemapping=None, delete_group_rolemapping=None): """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: with patch.object(obj, 'get_client_id', side_effect=get_client_id) as mock_get_client_id: - with patch.object(obj, 'get_client_role_by_name', - side_effect=get_client_role_by_name) as mock_get_client_role_by_name: - with patch.object(obj, 'get_client_rolemapping_by_id', - side_effect=get_client_rolemapping_by_id) as mock_get_client_rolemapping_by_id: - with patch.object(obj, 'get_client_available_rolemappings', - side_effect=get_client_available_rolemappings) as mock_get_client_available_rolemappings: - with patch.object(obj, 'get_client_composite_rolemappings', - side_effect=get_client_composite_rolemappings) as mock_get_client_composite_rolemappings: + with patch.object(obj, 'get_client_role_id_by_name', + side_effect=get_client_role_id_by_name) as mock_get_client_role_id_by_name: + with patch.object(obj, 'get_client_group_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_group_available_rolemappings', + side_effect=get_client_group_available_rolemappings) as mock_get_client_group_available_rolemappings: + with patch.object(obj, 'get_client_group_composite_rolemappings', + side_effect=get_client_group_composite_rolemappings) as mock_get_client_group_composite_rolemappings: with patch.object(obj, 'add_group_rolemapping', side_effect=add_group_rolemapping) as mock_add_group_rolemapping: with patch.object(obj, '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, \ - mock_get_client_rolemapping_by_id, mock_get_client_available_rolemappings, mock_get_client_composite_rolemappings, \ - mock_delete_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_group_rolemapping_by_id, mock_get_client_group_available_rolemappings, \ + mock_get_client_group_composite_rolemappings, mock_delete_group_rolemapping def get_response(object_with_future_response, method, get_id_call_count): @@ -144,8 +144,8 @@ class TestKeycloakRealm(ModuleTestCase): "subGroups": "[]" }] 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_available_rolemappings = [[ + return_value_get_client_role_id_by_name = "e91af074-cfd5-40ee-8ef5-ae0ae1ce69fe" + return_value_get_client_group_available_rolemappings = [[ { "clientRole": "true", "composite": "false", @@ -161,7 +161,7 @@ class TestKeycloakRealm(ModuleTestCase): "name": "test_role1" } ]] - return_value_get_client_composite_rolemappings = [ + return_value_get_client_group_composite_rolemappings = [ None, [ { @@ -189,11 +189,11 @@ class TestKeycloakRealm(ModuleTestCase): 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, - get_client_role_by_name=return_value_get_client_role_by_name, - get_client_available_rolemappings=return_value_get_client_available_rolemappings, - get_client_composite_rolemappings=return_value_get_client_composite_rolemappings) \ - as (mock_get_group_by_name, mock_get_client_id, mock_get_client_role_by_name, mock_add_group_rolemapping, - mock_get_client_rolemapping_by_id, mock_get_client_available_rolemappings, mock_get_client_composite_rolemappings, + get_client_role_id_by_name=return_value_get_client_role_id_by_name, + get_client_group_available_rolemappings=return_value_get_client_group_available_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_id_by_name, mock_add_group_rolemapping, + mock_get_client_group_rolemapping_by_id, mock_get_client_group_available_rolemappings, mock_get_client_group_composite_rolemappings, mock_delete_group_rolemapping): with self.assertRaises(AnsibleExitJson) as exec_info: self.module.main() @@ -201,9 +201,9 @@ class TestKeycloakRealm(ModuleTestCase): self.assertEqual(mock_get_group_by_name.call_count, 1) self.assertEqual(mock_get_client_id.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_available_rolemappings.call_count, 1) - self.assertEqual(mock_get_client_composite_rolemappings.call_count, 2) + self.assertEqual(mock_get_client_group_rolemapping_by_id.call_count, 0) + self.assertEqual(mock_get_client_group_available_rolemappings.call_count, 1) + self.assertEqual(mock_get_client_group_composite_rolemappings.call_count, 2) self.assertEqual(mock_delete_group_rolemapping.call_count, 0) # Verify that the module's changed status matches what is expected @@ -246,9 +246,9 @@ class TestKeycloakRealm(ModuleTestCase): "subGroups": "[]" }] 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_available_rolemappings = [[]] - return_value_get_client_composite_rolemappings = [[ + return_value_get_client_role_id_by_name = "e91af074-cfd5-40ee-8ef5-ae0ae1ce69fe" + return_value_get_client_group_available_rolemappings = [[]] + return_value_get_client_group_composite_rolemappings = [[ { "clientRole": "true", "composite": "false", @@ -273,11 +273,11 @@ class TestKeycloakRealm(ModuleTestCase): 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, - get_client_role_by_name=return_value_get_client_role_by_name, - get_client_available_rolemappings=return_value_get_client_available_rolemappings, - get_client_composite_rolemappings=return_value_get_client_composite_rolemappings) \ - as (mock_get_group_by_name, mock_get_client_id, mock_get_client_role_by_name, mock_add_group_rolemapping, - mock_get_client_rolemapping_by_id, mock_get_client_available_rolemappings, mock_get_client_composite_rolemappings, + get_client_role_id_by_name=return_value_get_client_role_id_by_name, + get_client_group_available_rolemappings=return_value_get_client_group_available_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_id_by_name, mock_add_group_rolemapping, + mock_get_client_group_rolemapping_by_id, mock_get_client_group_available_rolemappings, mock_get_client_group_composite_rolemappings, mock_delete_group_rolemapping): with self.assertRaises(AnsibleExitJson) as exec_info: self.module.main() @@ -285,9 +285,9 @@ class TestKeycloakRealm(ModuleTestCase): self.assertEqual(mock_get_group_by_name.call_count, 1) self.assertEqual(mock_get_client_id.call_count, 1) 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_available_rolemappings.call_count, 1) - self.assertEqual(mock_get_client_composite_rolemappings.call_count, 1) + self.assertEqual(mock_get_client_group_rolemapping_by_id.call_count, 0) + self.assertEqual(mock_get_client_group_available_rolemappings.call_count, 1) + self.assertEqual(mock_get_client_group_composite_rolemappings.call_count, 1) self.assertEqual(mock_delete_group_rolemapping.call_count, 0) # Verify that the module's changed status matches what is expected @@ -330,8 +330,8 @@ class TestKeycloakRealm(ModuleTestCase): "subGroups": "[]" }] 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_available_rolemappings = [[ + return_value_get_client_role_id_by_name = "e91af074-cfd5-40ee-8ef5-ae0ae1ce69fe" + return_value_get_client_group_available_rolemappings = [[ { "clientRole": "true", "composite": "false", @@ -347,7 +347,7 @@ class TestKeycloakRealm(ModuleTestCase): "name": "test_role1" } ]] - return_value_get_client_composite_rolemappings = [ + return_value_get_client_group_composite_rolemappings = [ None, [ { @@ -375,11 +375,11 @@ class TestKeycloakRealm(ModuleTestCase): 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, - get_client_role_by_name=return_value_get_client_role_by_name, - get_client_available_rolemappings=return_value_get_client_available_rolemappings, - get_client_composite_rolemappings=return_value_get_client_composite_rolemappings) \ - as (mock_get_group_by_name, mock_get_client_id, mock_get_client_role_by_name, mock_add_group_rolemapping, - mock_get_client_rolemapping_by_id, mock_get_client_available_rolemappings, mock_get_client_composite_rolemappings, + get_client_role_id_by_name=return_value_get_client_role_id_by_name, + get_client_group_available_rolemappings=return_value_get_client_group_available_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_id_by_name, mock_add_group_rolemapping, + mock_get_client_group_rolemapping_by_id, mock_get_client_group_available_rolemappings, mock_get_client_group_composite_rolemappings, mock_delete_group_rolemapping): with self.assertRaises(AnsibleExitJson) as exec_info: self.module.main() @@ -387,9 +387,9 @@ class TestKeycloakRealm(ModuleTestCase): self.assertEqual(mock_get_group_by_name.call_count, 0) self.assertEqual(mock_get_client_id.call_count, 0) 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_available_rolemappings.call_count, 1) - self.assertEqual(mock_get_client_composite_rolemappings.call_count, 2) + self.assertEqual(mock_get_client_group_rolemapping_by_id.call_count, 0) + self.assertEqual(mock_get_client_group_available_rolemappings.call_count, 1) + self.assertEqual(mock_get_client_group_composite_rolemappings.call_count, 2) self.assertEqual(mock_delete_group_rolemapping.call_count, 0) # Verify that the module's changed status matches what is expected @@ -432,9 +432,9 @@ class TestKeycloakRealm(ModuleTestCase): "subGroups": "[]" }] 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_available_rolemappings = [[]] - return_value_get_client_composite_rolemappings = [ + return_value_get_client_role_id_by_name = "e91af074-cfd5-40ee-8ef5-ae0ae1ce69fe" + return_value_get_client_group_available_rolemappings = [[]] + return_value_get_client_group_composite_rolemappings = [ [ { "clientRole": "true", @@ -462,11 +462,11 @@ class TestKeycloakRealm(ModuleTestCase): 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, - get_client_role_by_name=return_value_get_client_role_by_name, - get_client_available_rolemappings=return_value_get_client_available_rolemappings, - get_client_composite_rolemappings=return_value_get_client_composite_rolemappings) \ - as (mock_get_group_by_name, mock_get_client_id, mock_get_client_role_by_name, mock_add_group_rolemapping, - mock_get_client_rolemapping_by_id, mock_get_client_available_rolemappings, mock_get_client_composite_rolemappings, + get_client_role_id_by_name=return_value_get_client_role_id_by_name, + get_client_group_available_rolemappings=return_value_get_client_group_available_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_id_by_name, mock_add_group_rolemapping, + mock_get_client_group_rolemapping_by_id, mock_get_client_group_available_rolemappings, mock_get_client_group_composite_rolemappings, mock_delete_group_rolemapping): with self.assertRaises(AnsibleExitJson) as exec_info: self.module.main() @@ -474,9 +474,9 @@ class TestKeycloakRealm(ModuleTestCase): self.assertEqual(mock_get_group_by_name.call_count, 1) self.assertEqual(mock_get_client_id.call_count, 1) 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_available_rolemappings.call_count, 1) - self.assertEqual(mock_get_client_composite_rolemappings.call_count, 2) + self.assertEqual(mock_get_client_group_rolemapping_by_id.call_count, 0) + self.assertEqual(mock_get_client_group_available_rolemappings.call_count, 1) + self.assertEqual(mock_get_client_group_composite_rolemappings.call_count, 2) self.assertEqual(mock_delete_group_rolemapping.call_count, 1) # Verify that the module's changed status matches what is expected @@ -519,8 +519,8 @@ class TestKeycloakRealm(ModuleTestCase): "subGroups": "[]" }] 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_available_rolemappings = [ + return_value_get_client_role_id_by_name = "e91af074-cfd5-40ee-8ef5-ae0ae1ce69fe" + return_value_get_client_group_available_rolemappings = [ [ { "clientRole": "true", @@ -538,7 +538,7 @@ class TestKeycloakRealm(ModuleTestCase): } ] ] - return_value_get_client_composite_rolemappings = [[]] + return_value_get_client_group_composite_rolemappings = [[]] changed = False @@ -548,11 +548,11 @@ class TestKeycloakRealm(ModuleTestCase): 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, - get_client_role_by_name=return_value_get_client_role_by_name, - get_client_available_rolemappings=return_value_get_client_available_rolemappings, - get_client_composite_rolemappings=return_value_get_client_composite_rolemappings) \ - as (mock_get_group_by_name, mock_get_client_id, mock_get_client_role_by_name, mock_add_group_rolemapping, - mock_get_client_rolemapping_by_id, mock_get_client_available_rolemappings, mock_get_client_composite_rolemappings, + get_client_role_id_by_name=return_value_get_client_role_id_by_name, + get_client_group_available_rolemappings=return_value_get_client_group_available_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_id_by_name, mock_add_group_rolemapping, + mock_get_client_group_rolemapping_by_id, mock_get_client_group_available_rolemappings, mock_get_client_group_composite_rolemappings, mock_delete_group_rolemapping): with self.assertRaises(AnsibleExitJson) as exec_info: self.module.main() @@ -560,9 +560,9 @@ class TestKeycloakRealm(ModuleTestCase): self.assertEqual(mock_get_group_by_name.call_count, 1) self.assertEqual(mock_get_client_id.call_count, 1) 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_available_rolemappings.call_count, 1) - self.assertEqual(mock_get_client_composite_rolemappings.call_count, 1) + self.assertEqual(mock_get_client_group_rolemapping_by_id.call_count, 0) + self.assertEqual(mock_get_client_group_available_rolemappings.call_count, 1) + self.assertEqual(mock_get_client_group_composite_rolemappings.call_count, 1) self.assertEqual(mock_delete_group_rolemapping.call_count, 0) # Verify that the module's changed status matches what is expected