From 1f2c7b17313929ed807a2b3ff553a18f5f705495 Mon Sep 17 00:00:00 2001 From: Simon Pahl Date: Sat, 22 Apr 2023 22:55:46 +0200 Subject: [PATCH] Add a module to set the keycloak client scope type (#6322) The module keycloak_clientscope_type allows to set the client scope types (optional/default) either on realm or client level. --- .github/BOTMETA.yml | 2 + .../identity/keycloak/keycloak.py | 135 +++++++++ plugins/modules/keycloak_clientscope_type.py | 285 ++++++++++++++++++ .../keycloak_clientscope_type/README.md | 16 + .../docker-compose.yml | 16 + .../keycloak_clientscope_type/tasks/main.yml | 164 ++++++++++ .../keycloak_clientscope_type/vars/main.yml | 11 + 7 files changed, 629 insertions(+) create mode 100644 plugins/modules/keycloak_clientscope_type.py create mode 100644 tests/integration/targets/keycloak_clientscope_type/README.md create mode 100644 tests/integration/targets/keycloak_clientscope_type/docker-compose.yml create mode 100644 tests/integration/targets/keycloak_clientscope_type/tasks/main.yml create mode 100644 tests/integration/targets/keycloak_clientscope_type/vars/main.yml diff --git a/.github/BOTMETA.yml b/.github/BOTMETA.yml index 98df26fb8e..504bfbc969 100644 --- a/.github/BOTMETA.yml +++ b/.github/BOTMETA.yml @@ -682,6 +682,8 @@ files: maintainers: Gaetan2907 $modules/keycloak_clientscope.py: maintainers: Gaetan2907 + $modules/keycloak_clientscope_type.py: + maintainers: simonpahl $modules/keycloak_clientsecret_info.py: maintainers: fynncfchen johncant $modules/keycloak_clientsecret_regenerate.py: diff --git a/plugins/module_utils/identity/keycloak/keycloak.py b/plugins/module_utils/identity/keycloak/keycloak.py index 15b665752d..c22ed21cd5 100644 --- a/plugins/module_utils/identity/keycloak/keycloak.py +++ b/plugins/module_utils/identity/keycloak/keycloak.py @@ -49,6 +49,16 @@ URL_CLIENTSCOPE = "{url}/admin/realms/{realm}/client-scopes/{id}" URL_CLIENTSCOPE_PROTOCOLMAPPERS = "{url}/admin/realms/{realm}/client-scopes/{id}/protocol-mappers/models" URL_CLIENTSCOPE_PROTOCOLMAPPER = "{url}/admin/realms/{realm}/client-scopes/{id}/protocol-mappers/models/{mapper_id}" +URL_DEFAULT_CLIENTSCOPES = "{url}/admin/realms/{realm}/default-default-client-scopes" +URL_DEFAULT_CLIENTSCOPE = "{url}/admin/realms/{realm}/default-default-client-scopes/{id}" +URL_OPTIONAL_CLIENTSCOPES = "{url}/admin/realms/{realm}/default-optional-client-scopes" +URL_OPTIONAL_CLIENTSCOPE = "{url}/admin/realms/{realm}/default-optional-client-scopes/{id}" + +URL_CLIENT_DEFAULT_CLIENTSCOPES = "{url}/admin/realms/{realm}/clients/{cid}/default-client-scopes" +URL_CLIENT_DEFAULT_CLIENTSCOPE = "{url}/admin/realms/{realm}/clients/{cid}/default-client-scopes/{id}" +URL_CLIENT_OPTIONAL_CLIENTSCOPES = "{url}/admin/realms/{realm}/clients/{cid}/optional-client-scopes" +URL_CLIENT_OPTIONAL_CLIENTSCOPE = "{url}/admin/realms/{realm}/clients/{cid}/optional-client-scopes/{id}" + 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" @@ -1163,6 +1173,131 @@ class KeycloakAPI(object): self.module.fail_json(msg='Could not update protocolmappers for clientscope %s in realm %s: %s' % (mapper_rep, realm, str(e))) + def get_default_clientscopes(self, realm, client_id=None): + """Fetch the name and ID of all clientscopes on the Keycloak server. + + To fetch the full data of the client scope, make a subsequent call to + get_clientscope_by_clientscopeid, passing in the ID of the client scope you wish to return. + + :param realm: Realm in which the clientscope resides. + :param client_id: The client in which the clientscope resides. + :return The default clientscopes of this realm or client + """ + url = URL_DEFAULT_CLIENTSCOPES if client_id is None else URL_CLIENT_DEFAULT_CLIENTSCOPES + return self._get_clientscopes_of_type(realm, url, 'default', client_id) + + def get_optional_clientscopes(self, realm, client_id=None): + """Fetch the name and ID of all clientscopes on the Keycloak server. + + To fetch the full data of the client scope, make a subsequent call to + get_clientscope_by_clientscopeid, passing in the ID of the client scope you wish to return. + + :param realm: Realm in which the clientscope resides. + :param client_id: The client in which the clientscope resides. + :return The optinal clientscopes of this realm or client + """ + url = URL_OPTIONAL_CLIENTSCOPES if client_id is None else URL_CLIENT_OPTIONAL_CLIENTSCOPES + return self._get_clientscopes_of_type(realm, url, 'optional', client_id) + + def _get_clientscopes_of_type(self, realm, url_template, scope_type, client_id=None): + """Fetch the name and ID of all clientscopes on the Keycloak server. + + To fetch the full data of the client scope, make a subsequent call to + get_clientscope_by_clientscopeid, passing in the ID of the client scope you wish to return. + + :param realm: Realm in which the clientscope resides. + :param url_template the template for the right type + :param scope_type this can be either optinal or default + :param client_id: The client in which the clientscope resides. + :return The clientscopes of the specified type of this realm + """ + if client_id is None: + clientscopes_url = url_template.format(url=self.baseurl, realm=realm) + try: + return json.loads(to_native(open_url(clientscopes_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 list of %s clientscopes in realm %s: %s" % (scope_type, realm, str(e))) + else: + cid = self.get_client_id(client_id=client_id, realm=realm) + clientscopes_url = url_template.format(url=self.baseurl, realm=realm, cid=cid) + try: + return json.loads(to_native(open_url(clientscopes_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 list of %s clientscopes in client %s: %s" % (scope_type, client_id, clientscopes_url)) + + def _decide_url_type_clientscope(self, client_id=None, scope_type="default"): + """Decides which url to use. + :param scope_type this can be either optinal or default + :param client_id: The client in which the clientscope resides. + """ + if client_id is None: + if scope_type == "default": + return URL_DEFAULT_CLIENTSCOPE + if scope_type == "optional": + return URL_OPTIONAL_CLIENTSCOPE + else: + if scope_type == "default": + return URL_CLIENT_DEFAULT_CLIENTSCOPE + if scope_type == "optional": + return URL_CLIENT_OPTIONAL_CLIENTSCOPE + + def add_default_clientscope(self, id, realm="master", client_id=None): + """Add a client scope as default either on realm or client level. + + :param id: Client scope Id. + :param realm: Realm in which the clientscope resides. + :param client_id: The client in which the clientscope resides. + """ + self._action_type_clientscope(id, client_id, "default", realm, 'add') + + def add_optional_clientscope(self, id, realm="master", client_id=None): + """Add a client scope as optional either on realm or client level. + + :param id: Client scope Id. + :param realm: Realm in which the clientscope resides. + :param client_id: The client in which the clientscope resides. + """ + self._action_type_clientscope(id, client_id, "optional", realm, 'add') + + def delete_default_clientscope(self, id, realm="master", client_id=None): + """Remove a client scope as default either on realm or client level. + + :param id: Client scope Id. + :param realm: Realm in which the clientscope resides. + :param client_id: The client in which the clientscope resides. + """ + self._action_type_clientscope(id, client_id, "default", realm, 'delete') + + def delete_optional_clientscope(self, id, realm="master", client_id=None): + """Remove a client scope as optional either on realm or client level. + + :param id: Client scope Id. + :param realm: Realm in which the clientscope resides. + :param client_id: The client in which the clientscope resides. + """ + self._action_type_clientscope(id, client_id, "optional", realm, 'delete') + + def _action_type_clientscope(self, id=None, client_id=None, scope_type="default", realm="master", action='add'): + """ Delete or add a clientscope of type. + :param name: The name of the clientscope. A lookup will be performed to retrieve the clientscope ID. + :param client_id: The ID of the clientscope (preferred to name). + :param scope_type 'default' or 'optional' + :param realm: The realm in which this group resides, default "master". + """ + cid = None if client_id is None else self.get_client_id(client_id=client_id, realm=realm) + # should have a good cid by here. + clientscope_type_url = self._decide_url_type_clientscope(client_id, scope_type).format(realm=realm, id=id, cid=cid, url=self.baseurl) + try: + method = 'PUT' if action == "add" else 'DELETE' + return open_url(clientscope_type_url, method=method, http_agent=self.http_agent, headers=self.restheaders, timeout=self.connection_timeout, + validate_certs=self.validate_certs) + + except Exception as e: + place = 'realm' if client_id is None else 'client ' + client_id + self.module.fail_json(msg="Unable to %s %s clientscope %s @ %s : %s" % (action, scope_type, id, place, str(e))) + def create_clientsecret(self, id, realm="master"): """ Generate a new client secret by id diff --git a/plugins/modules/keycloak_clientscope_type.py b/plugins/modules/keycloak_clientscope_type.py new file mode 100644 index 0000000000..facf02aa47 --- /dev/null +++ b/plugins/modules/keycloak_clientscope_type.py @@ -0,0 +1,285 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright (c) Ansible project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +DOCUMENTATION = ''' +--- +module: keycloak_clientscope_type + +short_description: Set the type of aclientscope in realm or client via Keycloak API + +version_added: 6.6.0 + +description: + - This module allows you to set the type (optional, default) of clientscopes + via the Keycloak REST API. It requires access to the REST API via OpenID + Connect; the user connecting and the client being used must have the + requisite access rights. In a default Keycloak installation, admin-cli and + an admin user would work, as would a separate client definition with the + scope tailored to your needs and a user having the expected roles. + +attributes: + check_mode: + support: full + diff_mode: + support: full + +options: + realm: + type: str + description: + - The Keycloak realm. + default: 'master' + + client_id: + description: + - The I(client_id) of the client. If not set the clientscop types are set as a default for the realm. + aliases: + - clientId + type: str + + default_clientscopes: + description: + - Client scopes that should be of type default. + type: list + elements: str + + optional_clientscopes: + description: + - Client scopes that should be of type optional. + type: list + elements: str + +extends_documentation_fragment: + - community.general.keycloak + - community.general.attributes + +author: + - Simon Pahl (@simonpahl) +''' + +EXAMPLES = ''' +- name: Set default client scopes on realm level + community.general.keycloak_clientsecret_info: + auth_client_id: admin-cli + auth_keycloak_url: https://auth.example.com/auth + auth_realm: master + auth_username: USERNAME + auth_password: PASSWORD + realm: "MyCustomRealm" + default_clientscopes: ['profile', 'roles'] + delegate_to: localhost + + +- name: Set default and optional client scopes on client level with token auth + community.general.keycloak_clientsecret_info: + auth_client_id: admin-cli + auth_keycloak_url: https://auth.example.com/auth + token: TOKEN + realm: "MyCustomRealm" + client_id: "MyCustomClient" + default_clientscopes: ['profile', 'roles'] + optional_clientscopes: ['phone'] + delegate_to: localhost +''' + +RETURN = ''' +msg: + description: Message as to what action was taken. + returned: always + type: str + sample: "" +proposed: + description: Representation of proposed client-scope types mapping. + returned: always + type: dict + sample: { + default_clientscopes: ["profile", "role"], + optional_clientscopes: [] + } +existing: + description: + - Representation of client scopes before module execution. + returned: always + type: dict + sample: { + default_clientscopes: ["profile", "role"], + optional_clientscopes: ["phone"] + } +end_state: + description: + - Representation of client scopes after module execution. + - The sample is truncated. + returned: on success + type: dict + sample: { + default_clientscopes: ["profile", "role"], + optional_clientscopes: [] + } +''' + +from ansible.module_utils.basic import AnsibleModule + +from ansible_collections.community.general.plugins.module_utils.identity.keycloak.keycloak import ( + KeycloakAPI, KeycloakError, get_token) + +from ansible_collections.community.general.plugins.module_utils.identity.keycloak.keycloak import \ + keycloak_argument_spec + + +def keycloak_clientscope_type_module(): + """ + Returns an AnsibleModule definition. + + :return: argument_spec dict + """ + argument_spec = keycloak_argument_spec() + + meta_args = dict( + realm=dict(default='master'), + client_id=dict(type='str', aliases=['clientId']), + default_clientscopes=dict(type='list', elements='str'), + optional_clientscopes=dict(type='list', elements='str'), + ) + + 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'], + ['default_clientscopes', 'optional_clientscopes'] + ]), + required_together=([['auth_realm', 'auth_username', 'auth_password']]), + mutually_exclusive=[ + ['token', 'auth_realm'], + ['token', 'auth_username'], + ['token', 'auth_password'] + ]) + + return module + + +def clientscopes_to_add(existing, proposed): + to_add = [] + existing_clientscope_ids = extract_field(existing, 'id') + for clientscope in proposed: + if not clientscope['id'] in existing_clientscope_ids: + to_add.append(clientscope) + return to_add + + +def clientscopes_to_delete(existing, proposed): + to_delete = [] + proposed_clientscope_ids = extract_field(proposed, 'id') + for clientscope in existing: + if not clientscope['id'] in proposed_clientscope_ids: + to_delete.append(clientscope) + return to_delete + + +def extract_field(dictionary, field='name'): + return [cs[field] for cs in dictionary] + + +def main(): + """ + Module keycloak_clientscope_type + + :return: + """ + + module = keycloak_clientscope_type_module() + + # 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') + client_id = module.params.get('client_id') + default_clientscopes = module.params.get('default_clientscopes') + optional_clientscopes = module.params.get('optional_clientscopes') + + result = dict(changed=False, msg='', proposed={}, existing={}, end_state={}) + + all_clientscopes = kc.get_clientscopes(realm) + default_clientscopes_real = [] + optional_clientscopes_real = [] + + for client_scope in all_clientscopes: + if default_clientscopes is not None and client_scope["name"] in default_clientscopes: + default_clientscopes_real.append(client_scope) + if optional_clientscopes is not None and client_scope["name"] in optional_clientscopes: + optional_clientscopes_real.append(client_scope) + + if default_clientscopes is not None and len(default_clientscopes_real) != len(default_clientscopes): + module.fail_json(msg='At least one of the default_clientscopes does not exist!') + + if optional_clientscopes is not None and len(optional_clientscopes_real) != len(optional_clientscopes): + module.fail_json(msg='At least one of the optional_clientscopes does not exist!') + + result['proposed'].update({ + 'default_clientscopes': 'no-change' if default_clientscopes is None else default_clientscopes, + 'optional_clientscopes': 'no-change' if optional_clientscopes is None else optional_clientscopes + }) + + default_clientscopes_existing = kc.get_default_clientscopes(realm, client_id) + optional_clientscopes_existing = kc.get_optional_clientscopes(realm, client_id) + + result['existing'].update({ + 'default_clientscopes': extract_field(default_clientscopes_existing), + 'optional_clientscopes': extract_field(optional_clientscopes_existing) + }) + + if module._diff: + result['diff'] = dict(before=result['existing'], after=result['proposed']) + + if module.check_mode: + module.exit_json(**result) + + default_clientscopes_add = clientscopes_to_add(default_clientscopes_existing, default_clientscopes_real) + optional_clientscopes_add = clientscopes_to_add(optional_clientscopes_existing, optional_clientscopes_real) + + default_clientscopes_delete = clientscopes_to_delete(default_clientscopes_existing, default_clientscopes_real) + optional_clientscopes_delete = clientscopes_to_delete(optional_clientscopes_existing, optional_clientscopes_real) + + # first delete so clientscopes can change type + for clientscope in default_clientscopes_delete: + kc.delete_default_clientscope(clientscope['id'], realm, client_id) + for clientscope in optional_clientscopes_delete: + kc.delete_optional_clientscope(clientscope['id'], realm, client_id) + + for clientscope in default_clientscopes_add: + kc.add_default_clientscope(clientscope['id'], realm, client_id) + for clientscope in optional_clientscopes_add: + kc.add_optional_clientscope(clientscope['id'], realm, client_id) + + result["changed"] = ( + len(default_clientscopes_add) > 0 + or len(optional_clientscopes_add) > 0 + or len(default_clientscopes_delete) > 0 + or len(optional_clientscopes_delete) > 0 + ) + + result['end_state'].update({ + 'default_clientscopes': extract_field(kc.get_default_clientscopes(realm, client_id)), + 'optional_clientscopes': extract_field(kc.get_optional_clientscopes(realm, client_id)) + }) + + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/tests/integration/targets/keycloak_clientscope_type/README.md b/tests/integration/targets/keycloak_clientscope_type/README.md new file mode 100644 index 0000000000..3f3685f9be --- /dev/null +++ b/tests/integration/targets/keycloak_clientscope_type/README.md @@ -0,0 +1,16 @@ + + +The integration test can be performed as follows: + +``` +# 1. Start docker-compose: +docker-compose -f tests/integration/targets/keycloak_clientscope_type/docker-compose.yml down +docker-compose -f tests/integration/targets/keycloak_clientscope_type/docker-compose.yml up -d + +# 2. Run the integration tests: +ansible-test integration keycloak_clientscope_type --allow-unsupported -v +``` diff --git a/tests/integration/targets/keycloak_clientscope_type/docker-compose.yml b/tests/integration/targets/keycloak_clientscope_type/docker-compose.yml new file mode 100644 index 0000000000..b73ddff168 --- /dev/null +++ b/tests/integration/targets/keycloak_clientscope_type/docker-compose.yml @@ -0,0 +1,16 @@ +--- +# 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 + +version: '3.4' + +services: + keycloak: + image: quay.io/keycloak/keycloak:21.0.2 + ports: + - 8080:8080 + environment: + KEYCLOAK_ADMIN: admin + KEYCLOAK_ADMIN_PASSWORD: password + command: start-dev diff --git a/tests/integration/targets/keycloak_clientscope_type/tasks/main.yml b/tests/integration/targets/keycloak_clientscope_type/tasks/main.yml new file mode 100644 index 0000000000..76daace734 --- /dev/null +++ b/tests/integration/targets/keycloak_clientscope_type/tasks/main.yml @@ -0,0 +1,164 @@ +--- +# 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 + + +# Fixtures +- name: Create keycloak realm + community.general.keycloak_realm: + auth_keycloak_url: "{{ url }}" + auth_realm: "{{ admin_realm }}" + auth_username: "{{ admin_user }}" + auth_password: "{{ admin_password }}" + realm: "{{ realm }}" + id: "" + state: present + enabled: true + +- name: Create keycloak 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 }}" + state: present + enabled: true + +- name: Create a scope1 client scope + community.general.keycloak_clientscope: + auth_keycloak_url: "{{ url }}" + auth_realm: "{{ admin_realm }}" + auth_username: "{{ admin_user }}" + auth_password: "{{ admin_password }}" + realm: "{{ realm }}" + name: scope1 + description: "test 1" + protocol: openid-connect + +- name: Create a scope2 client scope + community.general.keycloak_clientscope: + auth_keycloak_url: "{{ url }}" + auth_realm: "{{ admin_realm }}" + auth_username: "{{ admin_user }}" + auth_password: "{{ admin_password }}" + realm: "{{ realm }}" + name: scope2 + description: "test 2" + protocol: openid-connect + +### Tests +### Realm +- name: adjust client-scope types in realm + community.general.keycloak_clientscope_type: + auth_keycloak_url: "{{ url }}" + auth_realm: "{{ admin_realm }}" + auth_username: "{{ admin_user }}" + auth_password: "{{ admin_password }}" + realm: "{{ realm }}" + default_clientscopes: ['scope1', 'scope2'] + optional_clientscopes: [] + register: result + +- name: Assert that client scope types are set + assert: + that: + - result is changed + - result.end_state != {} + - '"scope1" in result.end_state.default_clientscopes' + - '"scope2" in result.end_state.default_clientscopes' + - result.end_state.default_clientscopes|length == 2 + - result.end_state.optional_clientscopes|length == 0 + +- name: adjust client-scope types in realm again + community.general.keycloak_clientscope_type: + auth_keycloak_url: "{{ url }}" + auth_realm: "{{ admin_realm }}" + auth_username: "{{ admin_user }}" + auth_password: "{{ admin_password }}" + realm: "{{ realm }}" + default_clientscopes: ['scope1', 'scope2'] + optional_clientscopes: [] + register: result + failed_when: result is changed + +- name: adjust client-scope types in realm move scope 2 to optional + community.general.keycloak_clientscope_type: + auth_keycloak_url: "{{ url }}" + auth_realm: "{{ admin_realm }}" + auth_username: "{{ admin_user }}" + auth_password: "{{ admin_password }}" + realm: "{{ realm }}" + default_clientscopes: ['scope1'] + optional_clientscopes: ['scope2'] + register: result + +- name: Assert that client scope types are set + assert: + that: + - result is changed + - result.end_state != {} + - '"scope1" in result.end_state.default_clientscopes' + - '"scope2" in result.end_state.optional_clientscopes' + - result.end_state.default_clientscopes|length == 1 + - result.end_state.optional_clientscopes|length == 1 + +### Client +- name: adjust client-scope types in client + community.general.keycloak_clientscope_type: + auth_keycloak_url: "{{ url }}" + auth_realm: "{{ admin_realm }}" + auth_username: "{{ admin_user }}" + auth_password: "{{ admin_password }}" + realm: "{{ realm }}" + client_id: "{{ client_id }}" + default_clientscopes: ['scope1', 'scope2'] + optional_clientscopes: [] + register: result + +- name: Assert that client scope types are set + assert: + that: + - result is changed + - result.end_state != {} + - '"scope1" in result.end_state.default_clientscopes' + - '"scope2" in result.end_state.default_clientscopes' + - result.end_state.default_clientscopes|length == 2 + - result.end_state.optional_clientscopes|length == 0 + +- name: adjust client-scope types in client again + community.general.keycloak_clientscope_type: + auth_keycloak_url: "{{ url }}" + auth_realm: "{{ admin_realm }}" + auth_username: "{{ admin_user }}" + auth_password: "{{ admin_password }}" + realm: "{{ realm }}" + client_id: "{{ client_id }}" + default_clientscopes: ['scope1', 'scope2'] + optional_clientscopes: [] + register: result + failed_when: result is changed + +- name: adjust client-scope types in client move scope 2 to optional + community.general.keycloak_clientscope_type: + auth_keycloak_url: "{{ url }}" + auth_realm: "{{ admin_realm }}" + auth_username: "{{ admin_user }}" + auth_password: "{{ admin_password }}" + realm: "{{ realm }}" + client_id: "{{ client_id }}" + default_clientscopes: ['scope1'] + optional_clientscopes: ['scope2'] + register: result + +- name: Assert that client scope types are set + assert: + that: + - result is changed + - result.end_state != {} + - '"scope1" in result.end_state.default_clientscopes' + - '"scope2" in result.end_state.optional_clientscopes' + - result.end_state.default_clientscopes|length == 1 + - result.end_state.optional_clientscopes|length == 1 diff --git a/tests/integration/targets/keycloak_clientscope_type/vars/main.yml b/tests/integration/targets/keycloak_clientscope_type/vars/main.yml new file mode 100644 index 0000000000..7efd2b04ef --- /dev/null +++ b/tests/integration/targets/keycloak_clientscope_type/vars/main.yml @@ -0,0 +1,11 @@ +--- +# 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 +admin_realm: master +admin_user: admin +admin_password: password +realm: clientscope-type-realm +client_id: clientscope-type-client