From 7d3e6d1bb712cb15e62b9e20d16fdfe7e31cb59f Mon Sep 17 00:00:00 2001 From: morco Date: Sat, 25 Feb 2023 11:12:35 +0100 Subject: [PATCH] keycloak_group: support keycloak subgroups (#5814) * feat(module/keycloak_group): add support for ... ... handling subgroups * added changelog fragment and fixing sanity ... ... test issues * more sanity fixes * fix missing version and review issues * added missing licence header * fix docu * fix line beeing too long * replaced suboptimal string type prefixing ... ... with better subdict based approach * fix sanity issues * more sanity fixing * fixed more review issues * fix argument list too long * why is it failing? something wrong with the docu? * is it this line then? * undid group attribute removing, it does not ... ... belong into this PR * fix version_added for parents parameter --------- Co-authored-by: Mirko Wilhelmi --- .../5814-support-keycloak-subgroups.yml | 2 + .../identity/keycloak/keycloak.py | 138 ++++- plugins/modules/keycloak_group.py | 122 ++++- .../targets/keycloak_group/aliases | 5 + .../targets/keycloak_group/readme.adoc | 27 + .../targets/keycloak_group/tasks/main.yml | 501 ++++++++++++++++++ .../targets/keycloak_group/vars/main.yml | 10 + 7 files changed, 796 insertions(+), 9 deletions(-) create mode 100644 changelogs/fragments/5814-support-keycloak-subgroups.yml create mode 100644 tests/integration/targets/keycloak_group/aliases create mode 100644 tests/integration/targets/keycloak_group/readme.adoc create mode 100644 tests/integration/targets/keycloak_group/tasks/main.yml create mode 100644 tests/integration/targets/keycloak_group/vars/main.yml diff --git a/changelogs/fragments/5814-support-keycloak-subgroups.yml b/changelogs/fragments/5814-support-keycloak-subgroups.yml new file mode 100644 index 0000000000..a369db4422 --- /dev/null +++ b/changelogs/fragments/5814-support-keycloak-subgroups.yml @@ -0,0 +1,2 @@ +minor_changes: + - keycloak_group - add new optional module parameter ``parents`` to properly handle keycloak subgroups (https://github.com/ansible-collections/community.general/pull/5814). diff --git a/plugins/module_utils/identity/keycloak/keycloak.py b/plugins/module_utils/identity/keycloak/keycloak.py index 09b22b7561..15b665752d 100644 --- a/plugins/module_utils/identity/keycloak/keycloak.py +++ b/plugins/module_utils/identity/keycloak/keycloak.py @@ -42,6 +42,7 @@ URL_CLIENTTEMPLATE = "{url}/admin/realms/{realm}/client-templates/{id}" URL_CLIENTTEMPLATES = "{url}/admin/realms/{realm}/client-templates" URL_GROUPS = "{url}/admin/realms/{realm}/groups" URL_GROUP = "{url}/admin/realms/{realm}/groups/{groupid}" +URL_GROUP_CHILDREN = "{url}/admin/realms/{realm}/groups/{groupid}/children" URL_CLIENTSCOPES = "{url}/admin/realms/{realm}/client-scopes" URL_CLIENTSCOPE = "{url}/admin/realms/{realm}/client-scopes/{id}" @@ -1249,7 +1250,7 @@ class KeycloakAPI(object): self.module.fail_json(msg="Could not fetch group %s in realm %s: %s" % (gid, realm, str(e))) - def get_group_by_name(self, name, realm="master"): + def get_group_by_name(self, name, realm="master", parents=None): """ Fetch a keycloak group within a realm based on its name. The Keycloak API does not allow filtering of the Groups resource by name. @@ -1259,10 +1260,19 @@ class KeycloakAPI(object): If the group does not exist, None is returned. :param name: Name of the group to fetch. :param realm: Realm in which the group resides; default 'master' + :param parents: Optional list of parents when group to look for is a subgroup """ groups_url = URL_GROUPS.format(url=self.baseurl, realm=realm) try: - all_groups = self.get_groups(realm=realm) + if parents: + parent = self.get_subgroup_direct_parent(parents, realm) + + if not parent: + return None + + all_groups = parent['subGroups'] + else: + all_groups = self.get_groups(realm=realm) for group in all_groups: if group['name'] == name: @@ -1274,6 +1284,102 @@ class KeycloakAPI(object): self.module.fail_json(msg="Could not fetch group %s in realm %s: %s" % (name, realm, str(e))) + def _get_normed_group_parent(self, parent): + """ Converts parent dict information into a more easy to use form. + + :param parent: parent describing dict + """ + if parent['id']: + return (parent['id'], True) + + return (parent['name'], False) + + def get_subgroup_by_chain(self, name_chain, realm="master"): + """ Access a subgroup API object by walking down a given name/id chain. + + Groups can be given either as by name or by ID, the first element + must either be a toplvl group or given as ID, all parents must exist. + + If the group cannot be found, None is returned. + :param name_chain: Topdown ordered list of subgroup parent (ids or names) + its own name at the end + :param realm: Realm in which the group resides; default 'master' + """ + cp = name_chain[0] + + # for 1st parent in chain we must query the server + cp, is_id = self._get_normed_group_parent(cp) + + if is_id: + tmp = self.get_group_by_groupid(cp, realm=realm) + else: + # given as name, assume toplvl group + tmp = self.get_group_by_name(cp, realm=realm) + + if not tmp: + return None + + for p in name_chain[1:]: + for sg in tmp['subGroups']: + pv, is_id = self._get_normed_group_parent(p) + + if is_id: + cmpkey = "id" + else: + cmpkey = "name" + + if pv == sg[cmpkey]: + tmp = sg + break + + if not tmp: + return None + + return tmp + + def get_subgroup_direct_parent(self, parents, realm="master", children_to_resolve=None): + """ Get keycloak direct parent group API object for a given chain of parents. + + To succesfully work the API for subgroups we actually dont need + to "walk the whole tree" for nested groups but only need to know + the ID for the direct predecessor of current subgroup. This + method will guarantee us this information getting there with + as minimal work as possible. + + Note that given parent list can and might be incomplete at the + upper levels as long as it starts with an ID instead of a name + + If the group does not exist, None is returned. + :param parents: Topdown ordered list of subgroup parents + :param realm: Realm in which the group resides; default 'master' + """ + if children_to_resolve is None: + # start recursion by reversing parents (in optimal cases + # we dont need to walk the whole tree upwarts) + parents = list(reversed(parents)) + children_to_resolve = [] + + if not parents: + # walk complete parents list to the top, all names, no id's, + # try to resolve it assuming list is complete and 1st + # element is a toplvl group + return self.get_subgroup_by_chain(list(reversed(children_to_resolve)), realm=realm) + + cp = parents[0] + unused, is_id = self._get_normed_group_parent(cp) + + if is_id: + # current parent is given as ID, we can stop walking + # upwards searching for an entry point + return self.get_subgroup_by_chain([cp] + list(reversed(children_to_resolve)), realm=realm) + else: + # current parent is given as name, it must be resolved + # later, try next parent (recurse) + children_to_resolve.append(cp) + return self.get_subgroup_direct_parent( + parents[1:], + realm=realm, children_to_resolve=children_to_resolve + ) + def create_group(self, grouprep, realm="master"): """ Create a Keycloak group. @@ -1288,6 +1394,34 @@ class KeycloakAPI(object): self.module.fail_json(msg="Could not create group %s in realm %s: %s" % (grouprep['name'], realm, str(e))) + def create_subgroup(self, parents, grouprep, realm="master"): + """ Create a Keycloak subgroup. + + :param parents: list of one or more parent groups + :param grouprep: a GroupRepresentation of the group to be created. Must contain at minimum the field name. + :return: HTTPResponse object on success + """ + parent_id = "---UNDETERMINED---" + try: + parent_id = self.get_subgroup_direct_parent(parents, realm) + + if not parent_id: + raise Exception( + "Could not determine subgroup parent ID for given" + " parent chain {0}. Assure that all parents exist" + " already and the list is complete and properly" + " ordered, starts with an ID or starts at the" + " top level".format(parents) + ) + + parent_id = parent_id["id"] + url = URL_GROUP_CHILDREN.format(url=self.baseurl, realm=realm, groupid=parent_id) + return open_url(url, method='POST', http_agent=self.http_agent, headers=self.restheaders, timeout=self.connection_timeout, + data=json.dumps(grouprep), validate_certs=self.validate_certs) + except Exception as e: + self.module.fail_json(msg="Could not create subgroup %s for parent group %s in realm %s: %s" + % (grouprep['name'], parent_id, realm, str(e))) + def update_group(self, grouprep, realm="master"): """ Update an existing group. diff --git a/plugins/modules/keycloak_group.py b/plugins/modules/keycloak_group.py index 94d96543c8..399bc5b4fa 100644 --- a/plugins/modules/keycloak_group.py +++ b/plugins/modules/keycloak_group.py @@ -22,7 +22,7 @@ description: 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). + Keycloak API and its documentation at U(https://www.keycloak.org/docs-api/20.0.2/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, @@ -42,7 +42,9 @@ options: description: - State of the group. - On C(present), the group will be created if it does not yet exist, or updated with the parameters you provide. - - On C(absent), the group will be removed if it exists. + - >- + On C(absent), the group will be removed if it exists. Be aware that absenting + a group with subgroups will automatically delete all its subgroups too. default: 'present' type: str choices: @@ -74,6 +76,38 @@ options: - A dict of key/value pairs to set as custom attributes for the group. - Values may be single values (e.g. a string) or a list of strings. + parents: + version_added: "6.4.0" + type: list + description: + - List of parent groups for the group to handle sorted top to bottom. + - >- + Set this to create a group as a subgroup of another group or groups (parents) or + when accessing an existing subgroup by name. + - >- + Not necessary to set when accessing an existing subgroup by its C(ID) because in + that case the group can be directly queried without necessarily knowing its parent(s). + elements: dict + suboptions: + id: + type: str + description: + - Identify parent by ID. + - Needs less API calls than using I(name). + - A deep parent chain can be started at any point when first given parent is given as ID. + - Note that in principle both ID and name can be specified at the same time + but current implementation only always use just one of them, with ID + being preferred. + name: + type: str + description: + - Identify parent by name. + - Needs more internal API calls than using I(id) to map names to ID's under the hood. + - When giving a parent chain with only names it must be complete up to the top. + - Note that in principle both ID and name can be specified at the same time + but current implementation only always use just one of them, with ID + being preferred. + notes: - Presently, the I(realmRoles), I(clientRoles) and I(access) attributes returned by the Keycloak API are read-only for groups. This limitation will be removed in a later version of this module. @@ -97,6 +131,7 @@ EXAMPLES = ''' auth_realm: master auth_username: USERNAME auth_password: PASSWORD + register: result_new_kcgrp delegate_to: localhost - name: Create a Keycloak group, authentication with token @@ -162,6 +197,64 @@ EXAMPLES = ''' - list - items delegate_to: localhost + +- name: Create a Keycloak subgroup of a base group (using parent name) + community.general.keycloak_group: + name: my-new-kc-group-sub + realm: MyCustomRealm + state: present + auth_client_id: admin-cli + auth_keycloak_url: https://auth.example.com/auth + auth_realm: master + auth_username: USERNAME + auth_password: PASSWORD + parents: + - name: my-new-kc-group + register: result_new_kcgrp_sub + delegate_to: localhost + +- name: Create a Keycloak subgroup of a base group (using parent id) + community.general.keycloak_group: + name: my-new-kc-group-sub2 + realm: MyCustomRealm + state: present + auth_client_id: admin-cli + auth_keycloak_url: https://auth.example.com/auth + auth_realm: master + auth_username: USERNAME + auth_password: PASSWORD + parents: + - id: "{{ result_new_kcgrp.end_state.id }}" + delegate_to: localhost + +- name: Create a Keycloak subgroup of a subgroup (using parent names) + community.general.keycloak_group: + name: my-new-kc-group-sub-sub + realm: MyCustomRealm + state: present + auth_client_id: admin-cli + auth_keycloak_url: https://auth.example.com/auth + auth_realm: master + auth_username: USERNAME + auth_password: PASSWORD + parents: + - name: my-new-kc-group + - name: my-new-kc-group-sub + delegate_to: localhost + +- name: Create a Keycloak subgroup of a subgroup (using direct parent id) + community.general.keycloak_group: + name: my-new-kc-group-sub-sub + realm: MyCustomRealm + state: present + auth_client_id: admin-cli + auth_keycloak_url: https://auth.example.com/auth + auth_realm: master + auth_username: USERNAME + auth_password: PASSWORD + parents: + - id: "{{ result_new_kcgrp_sub.end_state.id }}" + delegate_to: localhost ''' RETURN = ''' @@ -240,6 +333,13 @@ def main(): id=dict(type='str'), name=dict(type='str'), attributes=dict(type='dict'), + parents=dict( + type='list', elements='dict', + options=dict( + id=dict(type='str'), + name=dict(type='str') + ), + ), ) argument_spec.update(meta_args) @@ -266,6 +366,8 @@ def main(): name = module.params.get('name') attributes = module.params.get('attributes') + parents = module.params.get('parents') + # attributes in Keycloak have their values returned as lists # via the API. attributes is a dict, so we'll transparently convert # the values to lists. @@ -275,12 +377,12 @@ def main(): # Filter and map the parameters names that apply to the group group_params = [x for x in module.params - if x not in list(keycloak_argument_spec().keys()) + ['state', 'realm'] and + if x not in list(keycloak_argument_spec().keys()) + ['state', 'realm', 'parents'] and module.params.get(x) is not None] # See if it already exists in Keycloak if gid is None: - before_group = kc.get_group_by_name(name, realm=realm) + before_group = kc.get_group_by_name(name, realm=realm, parents=parents) else: before_group = kc.get_group_by_groupid(gid, realm=realm) @@ -323,9 +425,15 @@ def main(): if module.check_mode: module.exit_json(**result) - # create it - kc.create_group(desired_group, realm=realm) - after_group = kc.get_group_by_name(name, realm) + # create it ... + if parents: + # ... as subgroup of another parent group + kc.create_subgroup(parents, desired_group, realm=realm) + else: + # ... as toplvl base group + kc.create_group(desired_group, realm=realm) + + after_group = kc.get_group_by_name(name, realm, parents=parents) result['end_state'] = after_group diff --git a/tests/integration/targets/keycloak_group/aliases b/tests/integration/targets/keycloak_group/aliases new file mode 100644 index 0000000000..bd1f024441 --- /dev/null +++ b/tests/integration/targets/keycloak_group/aliases @@ -0,0 +1,5 @@ +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +unsupported diff --git a/tests/integration/targets/keycloak_group/readme.adoc b/tests/integration/targets/keycloak_group/readme.adoc new file mode 100644 index 0000000000..1941e54efd --- /dev/null +++ b/tests/integration/targets/keycloak_group/readme.adoc @@ -0,0 +1,27 @@ +// Copyright (c) Ansible Project +// GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +// SPDX-License-Identifier: GPL-3.0-or-later + +To be able to run these integration tests a keycloak server must be +reachable under a specific url with a specific admin user and password. +The exact values expected for these parameters can be found in +'vars/main.yml' file. A simple way to do this is to use the official +keycloak docker images like this: + +---- +docker run --name mykeycloak -p 8080:8080 -e KC_HTTP_RELATIVE_PATH= -e KEYCLOAK_ADMIN= -e KEYCLOAK_ADMIN_PASSWORD= quay.io/keycloak/keycloak:20.0.2 start-dev +---- + +Example with concrete values inserted: + +---- +docker run --name mykeycloak -p 8080:8080 -e KC_HTTP_RELATIVE_PATH=/auth -e KEYCLOAK_ADMIN=admin -e KEYCLOAK_ADMIN_PASSWORD=password quay.io/keycloak/keycloak:20.0.2 start-dev +---- + +This test suite can run against a fresh unconfigured server instance +(no preconfiguration required) and cleans up after itself (undoes all +its config changes) as long as it runs through completly. While its active +it changes the server configuration in the following ways: + + * creating, modifying and deleting some keycloak groups + diff --git a/tests/integration/targets/keycloak_group/tasks/main.yml b/tests/integration/targets/keycloak_group/tasks/main.yml new file mode 100644 index 0000000000..50e61bab0f --- /dev/null +++ b/tests/integration/targets/keycloak_group/tasks/main.yml @@ -0,0 +1,501 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +- name: Create a keycloak group + community.general.keycloak_group: + auth_keycloak_url: "{{ url }}" + auth_realm: "{{ admin_realm }}" + auth_username: "{{ admin_user }}" + auth_password: "{{ admin_password }}" + realm: "{{ realm }}" + name: test-group + state: present + register: result + +- name: Assert group was created + assert: + that: + - result is changed + - result.end_state != {} + - result.end_state.name == "test-group" + - result.end_state.path == "/test-group" + - result.end_state.attributes == {} + - result.end_state.clientRoles == {} + - result.end_state.realmRoles == [] + - result.end_state.subGroups == [] + +- set_fact: + test_group_id: "{{ result.end_state.id }}" + +- name: Group creation rerun (test for idempotency) + community.general.keycloak_group: + auth_keycloak_url: "{{ url }}" + auth_realm: "{{ admin_realm }}" + auth_username: "{{ admin_user }}" + auth_password: "{{ admin_password }}" + realm: "{{ realm }}" + name: test-group + state: present + register: result + +- name: Assert that nothing has changed + assert: + that: + - result is not changed + - result.end_state != {} + - result.end_state.name == "test-group" + - result.end_state.path == "/test-group" + - result.end_state.attributes == {} + - result.end_state.clientRoles == {} + - result.end_state.realmRoles == [] + - result.end_state.subGroups == [] + +- name: Update the name of a keycloak group + community.general.keycloak_group: + auth_keycloak_url: "{{ url }}" + auth_realm: "{{ admin_realm }}" + auth_username: "{{ admin_user }}" + auth_password: "{{ admin_password }}" + realm: "{{ realm }}" + id: "{{ test_group_id }}" + name: new-test-group + state: present + register: result + +- name: Assert that group name was updated + assert: + that: + - result is changed + - result.end_state != {} + - result.end_state.name == "new-test-group" + - result.end_state.path == "/new-test-group" + - result.end_state.attributes == {} + - result.end_state.clientRoles == {} + - result.end_state.realmRoles == [] + - result.end_state.subGroups == [] + +- name: Delete a keycloak group by id + community.general.keycloak_group: + auth_keycloak_url: "{{ url }}" + auth_realm: "{{ admin_realm }}" + auth_username: "{{ admin_user }}" + auth_password: "{{ admin_password }}" + realm: "{{ realm }}" + id: "{{ test_group_id }}" + state: absent + register: result + +- name: Assert that group was deleted + assert: + that: + - result is changed + - result.end_state == {} + +- name: Redo group deletion (check for idempotency) + community.general.keycloak_group: + auth_keycloak_url: "{{ url }}" + auth_realm: "{{ admin_realm }}" + auth_username: "{{ admin_user }}" + auth_password: "{{ admin_password }}" + realm: "{{ realm }}" + id: "{{ test_group_id }}" + state: absent + register: result + +- name: Assert that nothing has changed + assert: + that: + - result is not changed + - result.end_state == {} + +- name: Create a keycloak group with some custom attributes + community.general.keycloak_group: + auth_keycloak_url: "{{ url }}" + auth_realm: "{{ admin_realm }}" + auth_username: "{{ admin_user }}" + auth_password: "{{ admin_password }}" + realm: "{{ realm }}" + name: my-new_group + attributes: + attrib1: value1 + attrib2: value2 + attrib3: + - item1 + - item2 + register: result + +- name: Assert that group was correctly created + assert: + that: + - result is changed + - result.end_state != {} + - result.end_state.name == "my-new_group" + - result.end_state.path == "/my-new_group" + - result.end_state.clientRoles == {} + - result.end_state.realmRoles == [] + - result.end_state.subGroups == [] + - result.end_state.attributes != {} + - result.end_state.attributes.attrib1 == ["value1"] + - result.end_state.attributes.attrib2 == ["value2"] + - result.end_state.attributes.attrib3 == ["item1", "item2"] + +- name: Delete a keycloak group based on name + community.general.keycloak_group: + auth_keycloak_url: "{{ url }}" + auth_realm: "{{ admin_realm }}" + auth_username: "{{ admin_user }}" + auth_password: "{{ admin_password }}" + realm: "{{ realm }}" + name: my-new_group + state: absent + register: result + +- name: Assert that group was deleted + assert: + that: + - result is changed + - result.end_state == {} + +## subgroup tests +## we already testet this so no asserts for this +- name: Create a new base group for subgroup testing (test setup) + community.general.keycloak_group: + auth_keycloak_url: "{{ url }}" + auth_realm: "{{ admin_realm }}" + auth_username: "{{ admin_user }}" + auth_password: "{{ admin_password }}" + realm: "{{ realm }}" + name: rootgrp + register: subgrp_basegrp_result + +- name: Create a subgroup using parent id + community.general.keycloak_group: + auth_keycloak_url: "{{ url }}" + auth_realm: "{{ admin_realm }}" + auth_username: "{{ admin_user }}" + auth_password: "{{ admin_password }}" + realm: "{{ realm }}" + name: subgrp1 + parents: + - id: "{{ subgrp_basegrp_result.end_state.id }}" + register: result + +- name: Assert that subgroup was correctly created + assert: + that: + - result is changed + - result.end_state != {} + - result.end_state.name == "subgrp1" + - result.end_state.path == "/rootgrp/subgrp1" + - result.end_state.attributes == {} + - result.end_state.clientRoles == {} + - result.end_state.realmRoles == [] + - result.end_state.subGroups == [] + +- name: Recreate a subgroup using parent id (test idempotency) + community.general.keycloak_group: + auth_keycloak_url: "{{ url }}" + auth_realm: "{{ admin_realm }}" + auth_username: "{{ admin_user }}" + auth_password: "{{ admin_password }}" + realm: "{{ realm }}" + name: subgrp1 + parents: + - id: "{{ subgrp_basegrp_result.end_state.id }}" + register: result + +- name: Assert that nothing has changed + assert: + that: + - result is not changed + - result.end_state != {} + - result.end_state.name == "subgrp1" + - result.end_state.path == "/rootgrp/subgrp1" + - result.end_state.attributes == {} + - result.end_state.clientRoles == {} + - result.end_state.realmRoles == [] + - result.end_state.subGroups == [] + +- name: Changing name of existing group + community.general.keycloak_group: + auth_keycloak_url: "{{ url }}" + auth_realm: "{{ admin_realm }}" + auth_username: "{{ admin_user }}" + auth_password: "{{ admin_password }}" + realm: "{{ realm }}" + id: "{{ result.end_state.id }}" + name: new-subgrp1 + parents: + - id: "{{ subgrp_basegrp_result.end_state.id }}" + register: result + +- name: Assert that subgroup name has changed correctly + assert: + that: + - result is changed + - result.end_state != {} + - result.end_state.name == "new-subgrp1" + - result.end_state.path == "/rootgrp/new-subgrp1" + - result.end_state.attributes == {} + - result.end_state.clientRoles == {} + - result.end_state.realmRoles == [] + - result.end_state.subGroups == [] + +- name: Create a subgroup using parent name + community.general.keycloak_group: + auth_keycloak_url: "{{ url }}" + auth_realm: "{{ admin_realm }}" + auth_username: "{{ admin_user }}" + auth_password: "{{ admin_password }}" + realm: "{{ realm }}" + name: subgrp2 + parents: + - name: rootgrp + register: result + +- name: Assert that subgroup was correctly created + assert: + that: + - result is changed + - result.end_state != {} + - result.end_state.name == "subgrp2" + - result.end_state.path == "/rootgrp/subgrp2" + - result.end_state.attributes == {} + - result.end_state.clientRoles == {} + - result.end_state.realmRoles == [] + - result.end_state.subGroups == [] + +- name: Recreate a subgroup using parent name (test idempotency) + community.general.keycloak_group: + auth_keycloak_url: "{{ url }}" + auth_realm: "{{ admin_realm }}" + auth_username: "{{ admin_user }}" + auth_password: "{{ admin_password }}" + realm: "{{ realm }}" + name: subgrp2 + parents: + - name: rootgrp + register: result + +- name: Assert that nothing has changed + assert: + that: + - result is not changed + - result.end_state != {} + - result.end_state.name == "subgrp2" + - result.end_state.path == "/rootgrp/subgrp2" + - result.end_state.attributes == {} + - result.end_state.clientRoles == {} + - result.end_state.realmRoles == [] + - result.end_state.subGroups == [] + +## subgroup of subgroup tests +- name: Create a subgroup of a subgroup using parent names (complete parent chain) + community.general.keycloak_group: + auth_keycloak_url: "{{ url }}" + auth_realm: "{{ admin_realm }}" + auth_username: "{{ admin_user }}" + auth_password: "{{ admin_password }}" + realm: "{{ realm }}" + name: subsubgrp + parents: + - name: rootgrp + - name: subgrp2 + register: result + +- name: Assert subgroup of subgroup was created + assert: + that: + - result is changed + - result.end_state != {} + - result.end_state.name == "subsubgrp" + - result.end_state.path == "/rootgrp/subgrp2/subsubgrp" + - result.end_state.attributes == {} + - result.end_state.clientRoles == {} + - result.end_state.realmRoles == [] + - result.end_state.subGroups == [] + +- name: ReCreate a subgroup of a subgroup using parent names (test idempotency) + community.general.keycloak_group: + auth_keycloak_url: "{{ url }}" + auth_realm: "{{ admin_realm }}" + auth_username: "{{ admin_user }}" + auth_password: "{{ admin_password }}" + realm: "{{ realm }}" + name: subsubgrp + parents: + - name: rootgrp + - name: subgrp2 + register: result_subsubgrp + +- name: Assert that nothing has changed + assert: + that: + - result_subsubgrp is not changed + - result_subsubgrp.end_state != {} + - result_subsubgrp.end_state.name == "subsubgrp" + - result_subsubgrp.end_state.path == "/rootgrp/subgrp2/subsubgrp" + - result_subsubgrp.end_state.attributes == {} + - result_subsubgrp.end_state.clientRoles == {} + - result_subsubgrp.end_state.realmRoles == [] + - result_subsubgrp.end_state.subGroups == [] + +- name: Create a subgroup of a subgroup using direct parent id (incomplete parent chain) + community.general.keycloak_group: + auth_keycloak_url: "{{ url }}" + auth_realm: "{{ admin_realm }}" + auth_username: "{{ admin_user }}" + auth_password: "{{ admin_password }}" + realm: "{{ realm }}" + name: subsubsubgrp + parents: + - id: "{{ result_subsubgrp.end_state.id }}" + register: result + +- name: Assert subgroup of subgroup was created + assert: + that: + - result is changed + - result.end_state != {} + - result.end_state.name == "subsubsubgrp" + - result.end_state.path == "/rootgrp/subgrp2/subsubgrp/subsubsubgrp" + - result.end_state.attributes == {} + - result.end_state.clientRoles == {} + - result.end_state.realmRoles == [] + - result.end_state.subGroups == [] + +- name: ReCreate a subgroup of a subgroup using direct parent id (test idempotency) + community.general.keycloak_group: + auth_keycloak_url: "{{ url }}" + auth_realm: "{{ admin_realm }}" + auth_username: "{{ admin_user }}" + auth_password: "{{ admin_password }}" + realm: "{{ realm }}" + name: subsubsubgrp + parents: + - id: "{{ result_subsubgrp.end_state.id }}" + register: result_subsubsubgrp + +- name: Assert that nothing changed + assert: + that: + - result_subsubsubgrp is not changed + - result_subsubsubgrp.end_state != {} + - result_subsubsubgrp.end_state.name == "subsubsubgrp" + - result_subsubsubgrp.end_state.path == "/rootgrp/subgrp2/subsubgrp/subsubsubgrp" + - result_subsubsubgrp.end_state.attributes == {} + - result_subsubsubgrp.end_state.clientRoles == {} + - result_subsubsubgrp.end_state.realmRoles == [] + - result_subsubsubgrp.end_state.subGroups == [] + +## subgroup deletion tests +## note: in principle we already have tested group deletion in general +## enough already, but what makes it interesting here again is to +## see it works also properly for subgroups and groups with subgroups +- name: Deleting a subgroup by id (no parents needed) + community.general.keycloak_group: + auth_keycloak_url: "{{ url }}" + auth_realm: "{{ admin_realm }}" + auth_username: "{{ admin_user }}" + auth_password: "{{ admin_password }}" + realm: "{{ realm }}" + id: "{{ result_subsubsubgrp.end_state.id }}" + state: absent + register: result + +- name: Assert that subgroup was deleted + assert: + that: + - result is changed + - result.end_state == {} + +- name: Redo subgroup deletion (idempotency test) + community.general.keycloak_group: + auth_keycloak_url: "{{ url }}" + auth_realm: "{{ admin_realm }}" + auth_username: "{{ admin_user }}" + auth_password: "{{ admin_password }}" + realm: "{{ realm }}" + id: "{{ result_subsubsubgrp.end_state.id }}" + state: absent + register: result + +- name: Assert that nothing changed + assert: + that: + - result is not changed + - result.end_state == {} + +- name: Deleting a subgroup by name + community.general.keycloak_group: + auth_keycloak_url: "{{ url }}" + auth_realm: "{{ admin_realm }}" + auth_username: "{{ admin_user }}" + auth_password: "{{ admin_password }}" + realm: "{{ realm }}" + name: new-subgrp1 + parents: + - name: rootgrp + state: absent + register: result + +- name: Assert that subgroup was deleted + assert: + that: + - result is changed + - result.end_state == {} + +- name: Redo deleting a subgroup by name (idempotency test) + community.general.keycloak_group: + auth_keycloak_url: "{{ url }}" + auth_realm: "{{ admin_realm }}" + auth_username: "{{ admin_user }}" + auth_password: "{{ admin_password }}" + realm: "{{ realm }}" + name: new-subgrp1 + parents: + - name: rootgrp + state: absent + register: result + +- name: Assert that nothing has changed + assert: + that: + - result is not changed + - result.end_state == {} + +- name: Delete keycloak group which has subgroups + community.general.keycloak_group: + auth_keycloak_url: "{{ url }}" + auth_realm: "{{ admin_realm }}" + auth_username: "{{ admin_user }}" + auth_password: "{{ admin_password }}" + realm: "{{ realm }}" + name: rootgrp + state: absent + register: result + +- name: Assert that group was deleted + assert: + that: + - result is changed + - result.end_state == {} + +- name: Redo delete keycloak group which has subgroups (idempotency test) + community.general.keycloak_group: + auth_keycloak_url: "{{ url }}" + auth_realm: "{{ admin_realm }}" + auth_username: "{{ admin_user }}" + auth_password: "{{ admin_password }}" + realm: "{{ realm }}" + name: rootgrp + state: absent + register: result + +- name: Assert that group was deleted + assert: + that: + - result is not changed + - result.end_state == {} diff --git a/tests/integration/targets/keycloak_group/vars/main.yml b/tests/integration/targets/keycloak_group/vars/main.yml new file mode 100644 index 0000000000..e8aeb4f3fd --- /dev/null +++ b/tests/integration/targets/keycloak_group/vars/main.yml @@ -0,0 +1,10 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +url: http://localhost:8080/auth +admin_realm: master +admin_user: admin +admin_password: password +realm: master