diff --git a/.github/BOTMETA.yml b/.github/BOTMETA.yml index 1e2fc66a199..0d35e1b5fdd 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/changelogs/fragments/5147-terraform-init-no-color.yml b/changelogs/fragments/5147-terraform-init-no-color.yml new file mode 100644 index 00000000000..6f0e805ea3b --- /dev/null +++ b/changelogs/fragments/5147-terraform-init-no-color.yml @@ -0,0 +1,2 @@ +minor_changes: + - terraform - run ``terraform init`` with ``-no-color`` not to mess up the stdout of the task (https://github.com/ansible-collections/community.general/pull/5147). diff --git a/changelogs/fragments/5274-proxmox-snap-container-with-mountpoints.yml b/changelogs/fragments/5274-proxmox-snap-container-with-mountpoints.yml new file mode 100644 index 00000000000..9e64e376637 --- /dev/null +++ b/changelogs/fragments/5274-proxmox-snap-container-with-mountpoints.yml @@ -0,0 +1,3 @@ +minor_changes: + - proxmox_snap - add ``unbind`` param to support snapshotting containers with configured mountpoints (https://github.com/ansible-collections/community.general/pull/5274). + - proxmox module utils, the proxmox* modules - add ``api_task_ok`` helper to standardize API task status checks across all proxmox modules (https://github.com/ansible-collections/community.general/pull/5274). diff --git a/changelogs/fragments/5280-lxc_container-py3.yaml b/changelogs/fragments/5280-lxc_container-py3.yaml new file mode 100644 index 00000000000..0a377381227 --- /dev/null +++ b/changelogs/fragments/5280-lxc_container-py3.yaml @@ -0,0 +1,5 @@ +bugfixes: + - lxc_container - the module has been updated to support Python 3 (https://github.com/ansible-collections/community.general/pull/5304). + +deprecated_features: + - lxc_container - the module will no longer make any effort to support Python 2 (https://github.com/ansible-collections/community.general/pull/5304). diff --git a/changelogs/fragments/5291-fix-nmcli-error-when-setting-unset-mac-address.yaml b/changelogs/fragments/5291-fix-nmcli-error-when-setting-unset-mac-address.yaml new file mode 100644 index 00000000000..b58db74bcfa --- /dev/null +++ b/changelogs/fragments/5291-fix-nmcli-error-when-setting-unset-mac-address.yaml @@ -0,0 +1,2 @@ +bugfixes: + - "nmcli - fix error when setting previously unset MAC address, ``gsm.apn`` or ``vpn.data``: current values were being normalized without checking if they might be ``None`` (https://github.com/ansible-collections/community.general/pull/5291)." diff --git a/changelogs/fragments/5297-bitwarden-add-search-field.yml b/changelogs/fragments/5297-bitwarden-add-search-field.yml new file mode 100644 index 00000000000..9b5d147b024 --- /dev/null +++ b/changelogs/fragments/5297-bitwarden-add-search-field.yml @@ -0,0 +1,2 @@ +minor_changes: + - bitwarden lookup plugin - add option ``search`` to search for other attributes than name (https://github.com/ansible-collections/community.general/pull/5297). diff --git a/meta/runtime.yml b/meta/runtime.yml index c93f00d760f..2685df53a7f 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/lookup/bitwarden.py b/plugins/lookup/bitwarden.py index 124c139c783..1cc2e44c749 100644 --- a/plugins/lookup/bitwarden.py +++ b/plugins/lookup/bitwarden.py @@ -22,6 +22,11 @@ required: true type: list elements: str + search: + description: Field to retrieve, for example C(name) or C(id). + type: str + default: name + version_added: 5.7.0 field: description: Field to fetch; leave unset to fetch whole response. type: str @@ -33,6 +38,11 @@ msg: >- {{ lookup('community.general.bitwarden', 'a_test', field='password') }} +- name: "Get 'password' from Bitwarden record with id 'bafba515-af11-47e6-abe3-af1200cd18b2'" + ansible.builtin.debug: + msg: >- + {{ lookup('community.general.bitwarden', 'bafba515-af11-47e6-abe3-af1200cd18b2', search='id', field='password') }} + - name: "Get full Bitwarden record named 'a_test'" ansible.builtin.debug: msg: >- @@ -81,7 +91,7 @@ def _run(self, args, stdin=None, expected_rc=0): raise BitwardenException(err) return to_text(out, errors='surrogate_or_strict'), to_text(err, errors='surrogate_or_strict') - def _get_matches(self, search_value, search_field="name"): + def _get_matches(self, search_value, search_field): """Return matching records whose search_field is equal to key. """ out, err = self._run(['list', 'items', '--search', search_value]) @@ -97,7 +107,7 @@ def get_field(self, field, search_value, search_field="name"): If field is None, return the whole record for each match. """ - matches = self._get_matches(search_value) + matches = self._get_matches(search_value, search_field) if field: return [match['login'][field] for match in matches] @@ -110,10 +120,11 @@ class LookupModule(LookupBase): def run(self, terms, variables=None, **kwargs): self.set_options(var_options=variables, direct=kwargs) field = self.get_option('field') + search_field = self.get_option('search') if not _bitwarden.logged_in: raise AnsibleError("Not logged into Bitwarden. Run 'bw login'.") - return [_bitwarden.get_field(field, term) for term in terms] + return [_bitwarden.get_field(field, term, search_field) for term in terms] _bitwarden = Bitwarden() diff --git a/plugins/module_utils/identity/keycloak/keycloak.py b/plugins/module_utils/identity/keycloak/keycloak.py index 1769d6c48f7..078925ef71d 100644 --- a/plugins/module_utils/identity/keycloak/keycloak.py +++ b/plugins/module_utils/identity/keycloak/keycloak.py @@ -29,8 +29,15 @@ 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_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 @@ def get_client_roles_by_id(self, cid, realm="master"): 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 @@ def get_client_role_by_name(self, gid, cid, name, realm="master"): 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 @@ def get_client_rolemapping_by_id(self, gid, cid, rid, realm='master'): :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 @@ def get_client_rolemapping_by_id(self, gid, cid, rid, realm='master'): % (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 @@ def get_client_available_rolemappings(self, gid, cid, realm="master"): :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 @@ def get_client_available_rolemappings(self, gid, cid, realm="master"): 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 @@ def get_client_composite_rolemappings(self, gid, cid, realm="master"): :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 @@ def add_group_rolemapping(self, gid, cid, role_rep, realm="master"): :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 @@ def delete_group_rolemapping(self, gid, cid, role_rep, realm="master"): :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 @@ def delete_group_rolemapping(self, gid, cid, role_rep, realm="master"): 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 @@ def get_group_by_groupid(self, gid, realm="master"): 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/module_utils/proxmox.py b/plugins/module_utils/proxmox.py index 65e8eb47236..96a96c8b3c0 100644 --- a/plugins/module_utils/proxmox.py +++ b/plugins/module_utils/proxmox.py @@ -137,3 +137,7 @@ def get_vm(self, vmid, ignore_missing=False): return None self.module.fail_json(msg='VM with vmid %s does not exist in cluster' % vmid) + + def api_task_ok(self, node, taskid): + status = self.proxmox_api.nodes(node).tasks(taskid).status.get() + return status['status'] == 'stopped' and status['exitstatus'] == 'OK' diff --git a/plugins/modules/cloud/lxc/lxc_container.py b/plugins/modules/cloud/lxc/lxc_container.py index ddcee0c8ea7..9eeb0b65d50 100644 --- a/plugins/modules/cloud/lxc/lxc_container.py +++ b/plugins/modules/cloud/lxc/lxc_container.py @@ -164,9 +164,9 @@ type: list elements: str requirements: - - 'lxc >= 1.0 # OS package' - - 'python >= 2.6 # OS Package' - - 'lxc-python2 >= 0.1 # PIP Package from https://github.com/lxc/python2-lxc' + - 'lxc >= 2.0 # OS package' + - 'python3 >= 3.5 # OS Package' + - 'python3-lxc # OS Package' notes: - Containers must have a unique name. If you attempt to create a container with a name that already exists in the users namespace the module will @@ -184,10 +184,10 @@ tarball of the running container. The "archive" option supports LVM backed containers and will create a snapshot of the running container when creating the archive. - - If your distro does not have a package for "python2-lxc", which is a + - If your distro does not have a package for C(python3-lxc), which is a requirement for this module, it can be installed from source at - "https://github.com/lxc/python2-lxc" or installed via pip using the package - name lxc-python2. + U(https://github.com/lxc/python3-lxc) or installed via pip using the + package name C(lxc). ''' EXAMPLES = r""" @@ -434,7 +434,6 @@ from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.parsing.convert_bool import BOOLEANS_FALSE, BOOLEANS_TRUE -from ansible.module_utils.six.moves import xrange from ansible.module_utils.common.text.converters import to_text, to_bytes @@ -559,7 +558,7 @@ def create_script(command): """Write out a script onto a target. - This method should be backward compatible with Python 2.4+ when executing + This method should be backward compatible with Python when executing from within the container. :param command: command to run, this can be a script and can use spacing @@ -939,7 +938,7 @@ def _container_startup(self, timeout=60): """ self.container = self.get_container_bind() - for dummy in xrange(timeout): + for dummy in range(timeout): if self._get_state() != 'running': self.container.start() self.state_change = True @@ -992,7 +991,7 @@ def _destroyed(self, timeout=60): :type timeout: ``int`` """ - for dummy in xrange(timeout): + for dummy in range(timeout): if not self._container_exists(container_name=self.container_name, lxc_path=self.lxc_path): break diff --git a/plugins/modules/cloud/misc/proxmox.py b/plugins/modules/cloud/misc/proxmox.py index 406666f57bf..5a89ee7796c 100644 --- a/plugins/modules/cloud/misc/proxmox.py +++ b/plugins/modules/cloud/misc/proxmox.py @@ -482,8 +482,7 @@ def create_instance(self, vmid, node, disk, storage, cpus, memory, swap, timeout taskid = getattr(proxmox_node, VZ_TYPE).create(vmid=vmid, storage=storage, memory=memory, swap=swap, **kwargs) while timeout: - if (proxmox_node.tasks(taskid).status.get()['status'] == 'stopped' and - proxmox_node.tasks(taskid).status.get()['exitstatus'] == 'OK'): + if self.api_task_ok(node, taskid): return True timeout -= 1 if timeout == 0: @@ -496,8 +495,7 @@ def create_instance(self, vmid, node, disk, storage, cpus, memory, swap, timeout def start_instance(self, vm, vmid, timeout): taskid = getattr(self.proxmox_api.nodes(vm['node']), VZ_TYPE)(vmid).status.start.post() while timeout: - if (self.proxmox_api.nodes(vm['node']).tasks(taskid).status.get()['status'] == 'stopped' and - self.proxmox_api.nodes(vm['node']).tasks(taskid).status.get()['exitstatus'] == 'OK'): + if self.api_task_ok(vm['node'], taskid): return True timeout -= 1 if timeout == 0: @@ -513,8 +511,7 @@ def stop_instance(self, vm, vmid, timeout, force): else: taskid = getattr(self.proxmox_api.nodes(vm['node']), VZ_TYPE)(vmid).status.shutdown.post() while timeout: - if (self.proxmox_api.nodes(vm['node']).tasks(taskid).status.get()['status'] == 'stopped' and - self.proxmox_api.nodes(vm['node']).tasks(taskid).status.get()['exitstatus'] == 'OK'): + if self.api_task_ok(vm['node'], taskid): return True timeout -= 1 if timeout == 0: @@ -527,8 +524,7 @@ def stop_instance(self, vm, vmid, timeout, force): def umount_instance(self, vm, vmid, timeout): taskid = getattr(self.proxmox_api.nodes(vm['node']), VZ_TYPE)(vmid).status.umount.post() while timeout: - if (self.proxmox_api.nodes(vm['node']).tasks(taskid).status.get()['status'] == 'stopped' and - self.proxmox_api.nodes(vm['node']).tasks(taskid).status.get()['exitstatus'] == 'OK'): + if self.api_task_ok(vm['node'], taskid): return True timeout -= 1 if timeout == 0: @@ -775,8 +771,7 @@ def main(): taskid = getattr(proxmox.proxmox_api.nodes(vm['node']), VZ_TYPE).delete(vmid, **delete_params) while timeout: - task_status = proxmox.proxmox_api.nodes(vm['node']).tasks(taskid).status.get() - if (task_status['status'] == 'stopped' and task_status['exitstatus'] == 'OK'): + if proxmox.api_task_ok(vm['node'], taskid): module.exit_json(changed=True, msg="VM %s removed" % vmid) timeout -= 1 if timeout == 0: diff --git a/plugins/modules/cloud/misc/proxmox_kvm.py b/plugins/modules/cloud/misc/proxmox_kvm.py index dc2d6e5aaea..ba5b0d4ff39 100644 --- a/plugins/modules/cloud/misc/proxmox_kvm.py +++ b/plugins/modules/cloud/misc/proxmox_kvm.py @@ -866,8 +866,7 @@ def wait_for_task(self, node, taskid): timeout = self.module.params['timeout'] while timeout: - task = self.proxmox_api.nodes(node).tasks(taskid).status.get() - if task['status'] == 'stopped' and task['exitstatus'] == 'OK': + if self.api_task_ok(node, taskid): # Wait an extra second as the API can be a ahead of the hypervisor time.sleep(1) return True diff --git a/plugins/modules/cloud/misc/proxmox_snap.py b/plugins/modules/cloud/misc/proxmox_snap.py index 9a9be8dc4db..3bd7c4ee329 100644 --- a/plugins/modules/cloud/misc/proxmox_snap.py +++ b/plugins/modules/cloud/misc/proxmox_snap.py @@ -38,6 +38,17 @@ - For removal from config file, even if removing disk snapshot fails. default: false type: bool + unbind: + description: + - This option only applies to LXC containers. + - Allows to snapshot a container even if it has configured mountpoints. + - Temporarily disables all configured mountpoints, takes snapshot, and finally restores original configuration. + - If running, the container will be stopped and restarted to apply config changes. + - Due to restrictions in the Proxmox API this option can only be used authenticating as C(root@pam) with I(api_password), API tokens do not work either. + - See U(https://pve.proxmox.com/pve-docs/api-viewer/#/nodes/{node}/lxc/{vmid}/config) (PUT tab) for more details. + default: false + type: bool + version_added: 5.7.0 vmstate: description: - Snapshot includes RAM. @@ -78,6 +89,16 @@ state: present snapname: pre-updates +- name: Create new snapshot for a container with configured mountpoints + community.general.proxmox_snap: + api_user: root@pam + api_password: 1q2w3e + api_host: node1 + vmid: 100 + state: present + unbind: true # requires root@pam+password auth, API tokens are not supported + snapname: pre-updates + - name: Remove container snapshot community.general.proxmox_snap: api_user: root@pam @@ -110,17 +131,89 @@ class ProxmoxSnapAnsible(ProxmoxAnsible): def snapshot(self, vm, vmid): return getattr(self.proxmox_api.nodes(vm['node']), vm['type'])(vmid).snapshot - def snapshot_create(self, vm, vmid, timeout, snapname, description, vmstate): + def vmconfig(self, vm, vmid): + return getattr(self.proxmox_api.nodes(vm['node']), vm['type'])(vmid).config + + def vmstatus(self, vm, vmid): + return getattr(self.proxmox_api.nodes(vm['node']), vm['type'])(vmid).status + + def _container_mp_get(self, vm, vmid): + cfg = self.vmconfig(vm, vmid).get() + mountpoints = {} + for key, value in cfg.items(): + if key.startswith('mp'): + mountpoints[key] = value + return mountpoints + + def _container_mp_disable(self, vm, vmid, timeout, unbind, mountpoints, vmstatus): + # shutdown container if running + if vmstatus == 'running': + self.shutdown_instance(vm, vmid, timeout) + # delete all mountpoints configs + self.vmconfig(vm, vmid).put(delete=' '.join(mountpoints)) + + def _container_mp_restore(self, vm, vmid, timeout, unbind, mountpoints, vmstatus): + # NOTE: requires auth as `root@pam`, API tokens are not supported + # see https://pve.proxmox.com/pve-docs/api-viewer/#/nodes/{node}/lxc/{vmid}/config + # restore original config + self.vmconfig(vm, vmid).put(**mountpoints) + # start container (if was running before snap) + if vmstatus == 'running': + self.start_instance(vm, vmid, timeout) + + def start_instance(self, vm, vmid, timeout): + taskid = self.vmstatus(vm, vmid).start.post() + while timeout: + if self.api_task_ok(vm['node'], taskid): + return True + timeout -= 1 + if timeout == 0: + self.module.fail_json(msg='Reached timeout while waiting for VM to start. Last line in task before timeout: %s' % + self.proxmox_api.nodes(vm['node']).tasks(taskid).log.get()[:1]) + time.sleep(1) + return False + + def shutdown_instance(self, vm, vmid, timeout): + taskid = self.vmstatus(vm, vmid).shutdown.post() + while timeout: + if self.api_task_ok(vm['node'], taskid): + return True + timeout -= 1 + if timeout == 0: + self.module.fail_json(msg='Reached timeout while waiting for VM to stop. Last line in task before timeout: %s' % + self.proxmox_api.nodes(vm['node']).tasks(taskid).log.get()[:1]) + time.sleep(1) + return False + + def snapshot_create(self, vm, vmid, timeout, snapname, description, vmstate, unbind): if self.module.check_mode: return True if vm['type'] == 'lxc': + if unbind is True: + # check if credentials will work + # WARN: it is crucial this check runs here! + # The correct permissions are required only to reconfig mounts. + # Not checking now would allow to remove the configuration BUT + # fail later, leaving the container in a misconfigured state. + if ( + self.module.params['api_user'] != 'root@pam' + or not self.module.params['api_password'] + ): + self.module.fail_json(msg='`unbind=True` requires authentication as `root@pam` with `api_password`, API tokens are not supported.') + return False + mountpoints = self._container_mp_get(vm, vmid) + vmstatus = self.vmstatus(vm, vmid).current().get()['status'] + if mountpoints: + self._container_mp_disable(vm, vmid, timeout, unbind, mountpoints, vmstatus) taskid = self.snapshot(vm, vmid).post(snapname=snapname, description=description) else: taskid = self.snapshot(vm, vmid).post(snapname=snapname, description=description, vmstate=int(vmstate)) + while timeout: - status_data = self.proxmox_api.nodes(vm['node']).tasks(taskid).status.get() - if status_data['status'] == 'stopped' and status_data['exitstatus'] == 'OK': + if self.api_task_ok(vm['node'], taskid): + if vm['type'] == 'lxc' and unbind is True and mountpoints: + self._container_mp_restore(vm, vmid, timeout, unbind, mountpoints, vmstatus) return True if timeout == 0: self.module.fail_json(msg='Reached timeout while waiting for creating VM snapshot. Last line in task before timeout: %s' % @@ -128,6 +221,8 @@ def snapshot_create(self, vm, vmid, timeout, snapname, description, vmstate): time.sleep(1) timeout -= 1 + if vm['type'] == 'lxc' and unbind is True and mountpoints: + self._container_mp_restore(vm, vmid, timeout, unbind, mountpoints, vmstatus) return False def snapshot_remove(self, vm, vmid, timeout, snapname, force): @@ -136,8 +231,7 @@ def snapshot_remove(self, vm, vmid, timeout, snapname, force): taskid = self.snapshot(vm, vmid).delete(snapname, force=int(force)) while timeout: - status_data = self.proxmox_api.nodes(vm['node']).tasks(taskid).status.get() - if status_data['status'] == 'stopped' and status_data['exitstatus'] == 'OK': + if self.api_task_ok(vm['node'], taskid): return True if timeout == 0: self.module.fail_json(msg='Reached timeout while waiting for removing VM snapshot. Last line in task before timeout: %s' % @@ -153,8 +247,7 @@ def snapshot_rollback(self, vm, vmid, timeout, snapname): taskid = self.snapshot(vm, vmid)(snapname).post("rollback") while timeout: - status_data = self.proxmox_api.nodes(vm['node']).tasks(taskid).status.get() - if status_data['status'] == 'stopped' and status_data['exitstatus'] == 'OK': + if self.api_task_ok(vm['node'], taskid): return True if timeout == 0: self.module.fail_json(msg='Reached timeout while waiting for rolling back VM snapshot. Last line in task before timeout: %s' % @@ -175,6 +268,7 @@ def main(): description=dict(type='str'), snapname=dict(type='str', default='ansible_snap'), force=dict(type='bool', default=False), + unbind=dict(type='bool', default=False), vmstate=dict(type='bool', default=False), ) module_args.update(snap_args) @@ -193,6 +287,7 @@ def main(): snapname = module.params['snapname'] timeout = module.params['timeout'] force = module.params['force'] + unbind = module.params['unbind'] vmstate = module.params['vmstate'] # If hostname is set get the VM id from ProxmoxAPI @@ -209,7 +304,7 @@ def main(): if i['name'] == snapname: module.exit_json(changed=False, msg="Snapshot %s is already present" % snapname) - if proxmox.snapshot_create(vm, vmid, timeout, snapname, description, vmstate): + if proxmox.snapshot_create(vm, vmid, timeout, snapname, description, vmstate, unbind): if module.check_mode: module.exit_json(changed=False, msg="Snapshot %s would be created" % snapname) else: diff --git a/plugins/modules/cloud/misc/proxmox_template.py b/plugins/modules/cloud/misc/proxmox_template.py index ab6e2d88e8a..24a6c87d31f 100644 --- a/plugins/modules/cloud/misc/proxmox_template.py +++ b/plugins/modules/cloud/misc/proxmox_template.py @@ -131,8 +131,7 @@ def task_status(self, node, taskid, timeout): Check the task status and wait until the task is completed or the timeout is reached. """ while timeout: - task_status = self.proxmox_api.nodes(node).tasks(taskid).status.get() - if task_status['status'] == 'stopped' and task_status['exitstatus'] == 'OK': + if self.api_task_ok(node, taskid): return True timeout = timeout - 1 if timeout == 0: diff --git a/plugins/modules/cloud/misc/terraform.py b/plugins/modules/cloud/misc/terraform.py index c8b654eb124..e4da9cbbf09 100644 --- a/plugins/modules/cloud/misc/terraform.py +++ b/plugins/modules/cloud/misc/terraform.py @@ -273,7 +273,7 @@ def _state_args(state_file): def init_plugins(bin_path, project_path, backend_config, backend_config_files, init_reconfigure, provider_upgrade, plugin_paths): - command = [bin_path, 'init', '-input=false'] + command = [bin_path, 'init', '-input=false', '-no-color'] if backend_config: for key, val in backend_config.items(): command.extend([ diff --git a/plugins/modules/identity/keycloak/keycloak_client_rolemapping.py b/plugins/modules/identity/keycloak/keycloak_client_rolemapping.py index c4dde9bee15..4f1f9b0d0fc 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 00000000000..72d403c6377 --- /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/plugins/modules/net_tools/nmcli.py b/plugins/modules/net_tools/nmcli.py index f5a45b2f5b3..13adc8bbd24 100644 --- a/plugins/modules/net_tools/nmcli.py +++ b/plugins/modules/net_tools/nmcli.py @@ -2099,15 +2099,18 @@ def _compare_conn_params(self, conn_info, options): # MAC addresses are case insensitive, nmcli always reports them in uppercase value = value.upper() # ensure current_value is also converted to uppercase in case nmcli changes behaviour - current_value = current_value.upper() + if current_value: + current_value = current_value.upper() if key == 'gsm.apn': # Depending on version nmcli adds double-qoutes to gsm.apn # Need to strip them in order to compare both - current_value = current_value.strip('"') + if current_value: + current_value = current_value.strip('"') if key == self.mtu_setting and self.mtu is None: self.mtu = 0 if key == 'vpn.data': - current_value = sorted(re.sub(r'\s*=\s*', '=', part.strip(), count=1) for part in current_value.split(',')) + if current_value: + current_value = sorted(re.sub(r'\s*=\s*', '=', part.strip(), count=1) for part in current_value.split(',')) value = sorted(part.strip() for part in value.split(',')) else: # parameter does not exist diff --git a/tests/integration/targets/keycloak_user_rolemapping/aliases b/tests/integration/targets/keycloak_user_rolemapping/aliases new file mode 100644 index 00000000000..cdeae1417ab --- /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 00000000000..e4625cb06e6 --- /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 00000000000..385dbea44a4 --- /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 d3a3516660d..72fc7850d52 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 @@ @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 @@ def test_map_clientrole_to_group_with_name(self): "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 @@ def test_map_clientrole_to_group_with_name(self): "name": "test_role1" } ]] - return_value_get_client_composite_rolemappings = [ + return_value_get_client_group_composite_rolemappings = [ None, [ { @@ -189,11 +189,11 @@ def test_map_clientrole_to_group_with_name(self): 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 @@ def test_map_clientrole_to_group_with_name(self): 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 @@ def test_map_clientrole_to_group_with_name_idempotency(self): "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 @@ def test_map_clientrole_to_group_with_name_idempotency(self): 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 @@ def test_map_clientrole_to_group_with_name_idempotency(self): 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 @@ def test_map_clientrole_to_group_with_id(self): "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 @@ def test_map_clientrole_to_group_with_id(self): "name": "test_role1" } ]] - return_value_get_client_composite_rolemappings = [ + return_value_get_client_group_composite_rolemappings = [ None, [ { @@ -375,11 +375,11 @@ def test_map_clientrole_to_group_with_id(self): 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 @@ def test_map_clientrole_to_group_with_id(self): 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 @@ def test_remove_clientrole_from_group(self): "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 @@ def test_remove_clientrole_from_group(self): 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 @@ def test_remove_clientrole_from_group(self): 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 @@ def test_remove_clientrole_from_group_idempotency(self): "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 @@ def test_remove_clientrole_from_group_idempotency(self): } ] ] - return_value_get_client_composite_rolemappings = [[]] + return_value_get_client_group_composite_rolemappings = [[]] changed = False @@ -548,11 +548,11 @@ def test_remove_clientrole_from_group_idempotency(self): 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 @@ def test_remove_clientrole_from_group_idempotency(self): 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