diff --git a/.github/BOTMETA.yml b/.github/BOTMETA.yml index 23b0bf1cc9..4088a7cc3b 100644 --- a/.github/BOTMETA.yml +++ b/.github/BOTMETA.yml @@ -725,6 +725,8 @@ files: maintainers: kris2kris $modules/keycloak_realm_info.py: maintainers: fynncfchen + $modules/keycloak_realm_key.py: + maintainers: mattock $modules/keycloak_role.py: maintainers: laurpaum $modules/keycloak_user.py: diff --git a/plugins/modules/keycloak_realm_key.py b/plugins/modules/keycloak_realm_key.py new file mode 100644 index 0000000000..d681cb2371 --- /dev/null +++ b/plugins/modules/keycloak_realm_key.py @@ -0,0 +1,457 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright (c) 2017, Eike Frost +# Copyright (c) 2021, Christophe Gilles +# 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_realm_key + +short_description: Allows administration of Keycloak realm keys via Keycloak API + +version_added: 7.5.0 + +description: + - This module allows the administration of Keycloak realm keys via the Keycloak REST API. It + requires access to the REST API via OpenID Connect; the user connecting and the realm 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 realm 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). + Aliases are provided so camelCased versions can be used as well. + + - This module is unable to detect changes to the actual cryptographic key after importing it. + However, if some other property is changed alongside the cryptographic key, then the key + will also get changed as a side-effect, as the JSON payload needs to include the private key. + This can be considered either a bug or a feature, as the alternative would be to always + update the realm key whether it has changed or not. + + - If certificate is not explicitly provided it will be dynamically created by Keycloak. + Therefore comparing the current state of the certificate to the desired state (which may be + empty) is not possible. + +attributes: + check_mode: + support: full + diff_mode: + support: partial + +options: + state: + description: + - State of the keycloak realm key. + - On V(present), the realm key will be created (or updated if it exists already). + - On V(absent), the realm key will be removed if it exists. + choices: ['present', 'absent'] + default: 'present' + type: str + name: + description: + - Name of the realm key to create. + type: str + required: true + force: + description: + - Enforce the state of the private key and certificate. This is not automatically the + case as this module is unable to determine the current state of the private key and + thus cannot trigger an update based on an actual divergence. That said, a private key + update may happen even if force is false as a side-effect of other changes. + default: false + type: bool + parent_id: + description: + - The parent_id of the realm key. In practice the ID (name) of the realm. + type: str + required: true + provider_id: + description: + - The name of the "provider ID" for the key. + choices: ['rsa'] + default: 'rsa' + type: str + config: + description: + - Dict specifying the key and its properties. + type: dict + suboptions: + active: + description: + - Whether they key is active or inactive. Not to be confused with the state + of the Ansible resource managed by the O(state) parameter. + default: true + type: bool + enabled: + description: + - Whether the key is enabled or disabled. Not to be confused with the state + of the Ansible resource managed by the O(state) parameter. + default: true + type: bool + priority: + description: + - The priority of the key. + type: int + required: true + algorithm: + description: + - Key algorithm. + default: RS256 + choices: ['RS256'] + type: str + private_key: + description: + - The private key as an ASCII string. Contents of the key must match O(config.algorithm) + and O(provider_id). + - Please note that the module cannot detect whether the private key specified differs from the + current state's private key. Use O(force=true) to force the module to update the private key + if you expect it to be updated. + required: true + type: str + certificate: + description: + - A certificate signed with the private key as an ASCII string. Contents of the + key must match O(config.algorithm) and O(provider_id). + - If you want Keycloak to automatically generate a certificate using your private key + then set this to an empty string. + required: true + type: str +notes: + - Current value of the private key cannot be fetched from Keycloak. + Therefore comparing its desired state to the current state is not + possible. + - If certificate is not explicitly provided it will be dynamically created + by Keycloak. Therefore comparing the current state of the certificate to + the desired state (which may be empty) is not possible. + - Due to the private key and certificate options the module is + B(not fully idempotent). You can use O(force=true) to force the module + to always update if you know that the private key might have changed. + +extends_documentation_fragment: + - community.general.keycloak + - community.general.attributes + +author: + - Samuli Seppänen (@mattock) +''' + +EXAMPLES = ''' +- name: Manage Keycloak realm key (certificate autogenerated by Keycloak) + community.general.keycloak_realm_key: + name: custom + state: present + parent_id: master + provider_id: rsa + auth_keycloak_url: http://localhost:8080/auth + auth_username: keycloak + auth_password: keycloak + auth_realm: master + config: + private_key: "{{ private_key }}" + enabled: true + active: true + priority: 120 + algorithm: RS256 +- name: Manage Keycloak realm key and certificate + community.general.keycloak_realm_key: + name: custom + state: present + parent_id: master + provider_id: rsa + auth_keycloak_url: http://localhost:8080/auth + auth_username: keycloak + auth_password: keycloak + auth_realm: master + config: + private_key: "{{ private_key }}" + certificate: "{{ certificate }}" + enabled: true + active: true + priority: 120 + algorithm: RS256 +''' + +RETURN = ''' +msg: + description: Message as to what action was taken. + returned: always + type: str + +end_state: + description: Representation of the keycloak_realm_key after module execution. + returned: on success + type: dict + contains: + id: + description: ID of the realm key. + type: str + returned: when O(state=present) + sample: 5b7ec13f-99da-46ad-8326-ab4c73cf4ce4 + name: + description: Name of the realm key. + type: str + returned: when O(state=present) + sample: mykey + parentId: + description: ID of the realm this key belongs to. + type: str + returned: when O(state=present) + sample: myrealm + providerId: + description: The ID of the key provider. + type: str + returned: when O(state=present) + sample: rsa + providerType: + description: The type of provider. + type: str + returned: when O(state=present) + config: + description: Realm key configuration. + type: dict + returned: when O(state=present) + sample: { + "active": ["true"], + "algorithm": ["RS256"], + "enabled": ["true"], + "priority": ["140"] + } +''' + +from ansible_collections.community.general.plugins.module_utils.identity.keycloak.keycloak import KeycloakAPI, camel, \ + keycloak_argument_spec, get_token, KeycloakError +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.six.moves.urllib.parse import urlencode +from copy import deepcopy + + +def main(): + """ + Module execution + + :return: + """ + argument_spec = keycloak_argument_spec() + + meta_args = dict( + state=dict(type='str', default='present', choices=['present', 'absent']), + name=dict(type='str', required=True), + force=dict(type='bool', default=False), + parent_id=dict(type='str', required=True), + provider_id=dict(type='str', default='rsa', choices=['rsa']), + config=dict( + type='dict', + options=dict( + active=dict(type='bool', default=True), + enabled=dict(type='bool', default=True), + priority=dict(type='int', required=True), + algorithm=dict(type='str', default='RS256', choices=['RS256']), + private_key=dict(type='str', required=True, no_log=True), + certificate=dict(type='str', required=True, no_log=True) + ) + ) + ) + + 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']]), + required_together=([['auth_realm', 'auth_username', 'auth_password']])) + + # Initialize the result object. Only "changed" seems to have special + # meaning for Ansible. + result = dict(changed=False, msg='', end_state={}, diff=dict(before={}, after={})) + + # This will include the current state of the realm key if it is already + # present. This is only used for diff-mode. + before_realm_key = {} + before_realm_key['config'] = {} + + # 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) + + params_to_ignore = list(keycloak_argument_spec().keys()) + ["state", "force"] + + # Filter and map the parameters names that apply to the role + component_params = [x for x in module.params + if x not in params_to_ignore and + module.params.get(x) is not None] + + # We only support one component provider type in this module + provider_type = 'org.keycloak.keys.KeyProvider' + + # Build a proposed changeset from parameters given to this module + changeset = {} + changeset['config'] = {} + + # Generate a JSON payload for Keycloak Admin API from the module + # parameters. Parameters that do not belong to the JSON payload (e.g. + # "state" or "auth_keycloal_url") have been filtered away earlier (see + # above). + # + # This loop converts Ansible module parameters (snake-case) into + # Keycloak-compatible format (camel-case). For example private_key + # becomes privateKey. + # + # It also converts bool, str and int parameters into lists with a single + # entry of 'str' type. Bool values are also lowercased. This is required + # by Keycloak. + # + for component_param in component_params: + if component_param == 'config': + for config_param in module.params.get('config'): + changeset['config'][camel(config_param)] = [] + raw_value = module.params.get('config')[config_param] + if isinstance(raw_value, bool): + value = str(raw_value).lower() + else: + value = str(raw_value) + + changeset['config'][camel(config_param)].append(value) + else: + # No need for camelcase in here as these are one word parameters + new_param_value = module.params.get(component_param) + changeset[camel(component_param)] = new_param_value + + # As provider_type is not a module parameter we have to add it to the + # changeset explicitly. + changeset['providerType'] = provider_type + + # Make a deep copy of the changeset. This is use when determining + # changes to the current state. + changeset_copy = deepcopy(changeset) + + # It is not possible to compare current keys to desired keys, because the + # certificate parameter is a base64-encoded binary blob created on the fly + # when a key is added. Moreover, the Keycloak Admin API does not seem to + # return the value of the private key for comparison. So, in effect, it we + # just have to ignore changes to the keys. However, as the privateKey + # parameter needs be present in the JSON payload, any changes done to any + # other parameters (e.g. config.priority) will trigger update of the keys + # as a side-effect. + del changeset_copy['config']['privateKey'] + del changeset_copy['config']['certificate'] + + # Make it easier to refer to current module parameters + name = module.params.get('name') + force = module.params.get('force') + state = module.params.get('state') + enabled = module.params.get('enabled') + provider_id = module.params.get('provider_id') + parent_id = module.params.get('parent_id') + + # Get a list of all Keycloak components that are of keyprovider type. + realm_keys = kc.get_components(urlencode(dict(type=provider_type, parent=parent_id)), parent_id) + + # If this component is present get its key ID. Confusingly the key ID is + # also known as the Provider ID. + key_id = None + + # Track individual parameter changes + changes = "" + + # This tells Ansible whether the key was changed (added, removed, modified) + result['changed'] = False + + # Loop through the list of components. If we encounter a component whose + # name matches the value of the name parameter then assume the key is + # already present. + for key in realm_keys: + if key['name'] == name: + key_id = key['id'] + changeset['id'] = key_id + changeset_copy['id'] = key_id + + # Compare top-level parameters + for param, value in changeset.items(): + before_realm_key[param] = key[param] + + if changeset_copy[param] != key[param] and param != 'config': + changes += "%s: %s -> %s, " % (param, key[param], changeset_copy[param]) + result['changed'] = True + + # Compare parameters under the "config" key + for p, v in changeset_copy['config'].items(): + before_realm_key['config'][p] = key['config'][p] + if changeset_copy['config'][p] != key['config'][p]: + changes += "config.%s: %s -> %s, " % (p, key['config'][p], changeset_copy['config'][p]) + result['changed'] = True + + # Sanitize linefeeds for the privateKey. Without this the JSON payload + # will be invalid. + changeset['config']['privateKey'][0] = changeset['config']['privateKey'][0].replace('\\n', '\n') + changeset['config']['certificate'][0] = changeset['config']['certificate'][0].replace('\\n', '\n') + + # Check all the possible states of the resource and do what is needed to + # converge current state with desired state (create, update or delete + # the key). + if key_id and state == 'present': + if result['changed']: + if module._diff: + del before_realm_key['config']['privateKey'] + del before_realm_key['config']['certificate'] + result['diff'] = dict(before=before_realm_key, after=changeset_copy) + + if module.check_mode: + result['msg'] = "Realm key %s would be changed: %s" % (name, changes.strip(", ")) + else: + kc.update_component(changeset, parent_id) + result['msg'] = "Realm key %s changed: %s" % (name, changes.strip(", ")) + elif not result['changed'] and force: + kc.update_component(changeset, parent_id) + result['changed'] = True + result['msg'] = "Realm key %s was forcibly updated" % (name) + else: + result['msg'] = "Realm key %s was in sync" % (name) + + result['end_state'] = changeset_copy + elif key_id and state == 'absent': + if module._diff: + del before_realm_key['config']['privateKey'] + del before_realm_key['config']['certificate'] + result['diff'] = dict(before=before_realm_key, after={}) + + if module.check_mode: + result['changed'] = True + result['msg'] = "Realm key %s would be deleted" % (name) + else: + kc.delete_component(key_id, parent_id) + result['changed'] = True + result['msg'] = "Realm key %s deleted" % (name) + + result['end_state'] = {} + elif not key_id and state == 'present': + if module._diff: + result['diff'] = dict(before={}, after=changeset_copy) + + if module.check_mode: + result['changed'] = True + result['msg'] = "Realm key %s would be created" % (name) + else: + kc.create_component(changeset, parent_id) + result['changed'] = True + result['msg'] = "Realm key %s created" % (name) + + result['end_state'] = changeset_copy + elif not key_id and state == 'absent': + result['changed'] = False + result['msg'] = "Realm key %s not present" % (name) + result['end_state'] = {} + + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/tests/integration/targets/keycloak_realm_key/aliases b/tests/integration/targets/keycloak_realm_key/aliases new file mode 100644 index 0000000000..bd1f024441 --- /dev/null +++ b/tests/integration/targets/keycloak_realm_key/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_realm_key/readme.adoc b/tests/integration/targets/keycloak_realm_key/readme.adoc new file mode 100644 index 0000000000..1941e54efd --- /dev/null +++ b/tests/integration/targets/keycloak_realm_key/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_realm_key/tasks/main.yml b/tests/integration/targets/keycloak_realm_key/tasks/main.yml new file mode 100644 index 0000000000..c02950600f --- /dev/null +++ b/tests/integration/targets/keycloak_realm_key/tasks/main.yml @@ -0,0 +1,373 @@ +--- +# 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: Remove Keycloak test realm to avoid failures from previous failed runs + community.general.keycloak_realm: + auth_keycloak_url: "{{ url }}" + auth_realm: "{{ admin_realm }}" + auth_username: "{{ admin_user }}" + auth_password: "{{ admin_password }}" + realm: "{{ realm }}" + id: "{{ realm }}" + state: absent + +- name: Create Keycloak test realm + community.general.keycloak_realm: + auth_keycloak_url: "{{ url }}" + auth_realm: "{{ admin_realm }}" + auth_username: "{{ admin_user }}" + auth_password: "{{ admin_password }}" + realm: "{{ realm }}" + id: "{{ realm }}" + state: present + +- name: Create custom realm key (check mode) + community.general.keycloak_realm_key: + auth_keycloak_url: "{{ url }}" + auth_realm: "{{ admin_realm }}" + auth_username: "{{ admin_user }}" + auth_password: "{{ admin_password }}" + name: testkey + state: present + parent_id: "{{ realm }}" + config: + private_key: "{{ realm_private_key }}" + certificate: "" + enabled: true + active: true + priority: 150 + check_mode: true + register: result + +- name: Assert that nothing has changed + assert: + that: + - result is changed + - result.end_state != {} + - result.end_state.name == "testkey" + - result.end_state.parentId == "realm_key_test" + - result.end_state.providerId == "rsa" + - result.end_state.providerType == "org.keycloak.keys.KeyProvider" + - result.end_state.config.active == ["true"] + - result.end_state.config.enabled == ["true"] + - result.end_state.config.algorithm == ["RS256"] + - result.end_state.config.priority == ["150"] + - result.msg == "Realm key testkey would be created" + +- name: Create custom realm key + community.general.keycloak_realm_key: + auth_keycloak_url: "{{ url }}" + auth_realm: "{{ admin_realm }}" + auth_username: "{{ admin_user }}" + auth_password: "{{ admin_password }}" + name: testkey + state: present + parent_id: "{{ realm }}" + config: + private_key: "{{ realm_private_key }}" + certificate: "" + enabled: true + active: true + priority: 150 + diff: true + register: result + +- name: Assert that realm key was created + assert: + that: + - result is changed + - result.end_state != {} + - result.end_state.name == "testkey" + - result.end_state.parentId == "realm_key_test" + - result.end_state.providerId == "rsa" + - result.end_state.providerType == "org.keycloak.keys.KeyProvider" + - result.end_state.config.active == ["true"] + - result.end_state.config.enabled == ["true"] + - result.end_state.config.algorithm == ["RS256"] + - result.end_state.config.priority == ["150"] + - result.msg == "Realm key testkey created" + +- name: Create custom realm key (test for idempotency) + community.general.keycloak_realm_key: + auth_keycloak_url: "{{ url }}" + auth_realm: "{{ admin_realm }}" + auth_username: "{{ admin_user }}" + auth_password: "{{ admin_password }}" + name: testkey + state: present + parent_id: "{{ realm }}" + config: + private_key: "{{ realm_private_key }}" + certificate: "" + enabled: true + active: true + priority: 150 + register: result + +- name: Assert that nothing has changed + assert: + that: + - result is not changed + - result.end_state != {} + - result.end_state.name == "testkey" + - result.end_state.parentId == "realm_key_test" + - result.end_state.providerId == "rsa" + - result.end_state.providerType == "org.keycloak.keys.KeyProvider" + - result.end_state.config.active == ["true"] + - result.end_state.config.enabled == ["true"] + - result.end_state.config.algorithm == ["RS256"] + - result.end_state.config.priority == ["150"] + - result.msg == "Realm key testkey was in sync" + +- name: Update custom realm key (check mode) + community.general.keycloak_realm_key: + auth_keycloak_url: "{{ url }}" + auth_realm: "{{ admin_realm }}" + auth_username: "{{ admin_user }}" + auth_password: "{{ admin_password }}" + name: testkey + state: present + parent_id: "{{ realm }}" + config: + private_key: "{{ realm_private_key }}" + certificate: "" + enabled: true + active: true + priority: 140 + check_mode: true + register: result + +- name: Assert that nothing has changed + assert: + that: + - result is changed + - result.end_state != {} + - result.end_state.name == "testkey" + - result.end_state.parentId == "realm_key_test" + - result.end_state.providerId == "rsa" + - result.end_state.providerType == "org.keycloak.keys.KeyProvider" + - result.end_state.config.active == ["true"] + - result.end_state.config.enabled == ["true"] + - result.end_state.config.algorithm == ["RS256"] + - result.end_state.config.priority == ["140"] + - result.msg == "Realm key testkey would be changed: config.priority ['150'] -> ['140']" + +- name: Update custom realm key + community.general.keycloak_realm_key: + auth_keycloak_url: "{{ url }}" + auth_realm: "{{ admin_realm }}" + auth_username: "{{ admin_user }}" + auth_password: "{{ admin_password }}" + name: testkey + state: present + parent_id: "{{ realm }}" + config: + private_key: "{{ realm_private_key }}" + certificate: "" + enabled: true + active: true + priority: 140 + diff: true + register: result + +- name: Assert that realm key was updated + assert: + that: + - result is changed + - result.end_state != {} + - result.end_state.name == "testkey" + - result.end_state.parentId == "realm_key_test" + - result.end_state.providerId == "rsa" + - result.end_state.providerType == "org.keycloak.keys.KeyProvider" + - result.end_state.config.active == ["true"] + - result.end_state.config.enabled == ["true"] + - result.end_state.config.algorithm == ["RS256"] + - result.end_state.config.priority == ["140"] + - result.msg == "Realm key testkey changed: config.priority ['150'] -> ['140']" + +- name: Update custom realm key (test for idempotency) + community.general.keycloak_realm_key: + auth_keycloak_url: "{{ url }}" + auth_realm: "{{ admin_realm }}" + auth_username: "{{ admin_user }}" + auth_password: "{{ admin_password }}" + name: testkey + state: present + parent_id: "{{ realm }}" + config: + private_key: "{{ realm_private_key }}" + certificate: "" + enabled: true + active: true + priority: 140 + register: result + +- name: Assert that nothing has changed + assert: + that: + - result is not changed + - result.end_state != {} + - result.end_state.name == "testkey" + - result.end_state.parentId == "realm_key_test" + - result.end_state.providerId == "rsa" + - result.end_state.providerType == "org.keycloak.keys.KeyProvider" + - result.end_state.config.active == ["true"] + - result.end_state.config.enabled == ["true"] + - result.end_state.config.algorithm == ["RS256"] + - result.end_state.config.priority == ["140"] + - result.msg == "Realm key testkey was in sync" + +- name: Force update custom realm key + community.general.keycloak_realm_key: + auth_keycloak_url: "{{ url }}" + auth_realm: "{{ admin_realm }}" + auth_username: "{{ admin_user }}" + auth_password: "{{ admin_password }}" + name: testkey + force: true + state: present + parent_id: "{{ realm }}" + config: + private_key: "{{ realm_private_key_2 }}" + certificate: "" + enabled: true + active: true + priority: 140 + register: result + +- name: Assert that forced update ran correctly + assert: + that: + - result is changed + - result.end_state != {} + - result.end_state.name == "testkey" + - result.end_state.parentId == "realm_key_test" + - result.end_state.providerId == "rsa" + - result.end_state.providerType == "org.keycloak.keys.KeyProvider" + - result.end_state.config.active == ["true"] + - result.end_state.config.enabled == ["true"] + - result.end_state.config.algorithm == ["RS256"] + - result.end_state.config.priority == ["140"] + - result.msg == "Realm key testkey was forcibly updated" + +- name: Remove custom realm key + community.general.keycloak_realm_key: + auth_keycloak_url: "{{ url }}" + auth_realm: "{{ admin_realm }}" + auth_username: "{{ admin_user }}" + auth_password: "{{ admin_password }}" + name: testkey + state: absent + parent_id: "{{ realm }}" + config: + private_key: "{{ realm_private_key }}" + certificate: "" + priority: 140 + diff: true + register: result + +- name: Assert that realm key was deleted + assert: + that: + - result is changed + - result.end_state == {} + - result.msg == "Realm key testkey deleted" + +- name: Remove custom realm key (test for idempotency) + community.general.keycloak_realm_key: + auth_keycloak_url: "{{ url }}" + auth_realm: "{{ admin_realm }}" + auth_username: "{{ admin_user }}" + auth_password: "{{ admin_password }}" + name: testkey + state: absent + parent_id: "{{ realm }}" + config: + private_key: "{{ realm_private_key }}" + certificate: "" + priority: 140 + register: result + +- name: Assert that nothing has changed + assert: + that: + - result is not changed + - result.end_state == {} + - result.msg == "Realm key testkey not present" + +- name: Create custom realm key with a custom certificate + community.general.keycloak_realm_key: + auth_keycloak_url: "{{ url }}" + auth_realm: "{{ admin_realm }}" + auth_username: "{{ admin_user }}" + auth_password: "{{ admin_password }}" + name: testkey_with_certificate + state: present + parent_id: "{{ realm }}" + config: + private_key: "{{ realm_private_key }}" + certificate: "{{ realm_certificate }}" + enabled: true + active: true + priority: 150 + diff: true + register: result + +- name: Assert that realm key with custom certificate was created + assert: + that: + - result is changed + - result.end_state != {} + - result.end_state.name == "testkey_with_certificate" + - result.end_state.parentId == "realm_key_test" + - result.end_state.providerId == "rsa" + - result.end_state.providerType == "org.keycloak.keys.KeyProvider" + - result.end_state.config.active == ["true"] + - result.end_state.config.enabled == ["true"] + - result.end_state.config.algorithm == ["RS256"] + - result.end_state.config.priority == ["150"] + - result.msg == "Realm key testkey_with_certificate created" + +- name: Attempt to change the private key and the certificate + community.general.keycloak_realm_key: + auth_keycloak_url: "{{ url }}" + auth_realm: "{{ admin_realm }}" + auth_username: "{{ admin_user }}" + auth_password: "{{ admin_password }}" + name: testkey_with_certificate + state: present + parent_id: "{{ realm }}" + config: + private_key: "a different private key string" + certificate: "a different certificate string" + enabled: true + active: true + priority: 150 + diff: true + register: result + +- name: Assert that nothing has changed + assert: + that: + - result is not changed + - result.end_state != {} + - result.end_state.name == "testkey_with_certificate" + - result.end_state.parentId == "realm_key_test" + - result.end_state.providerId == "rsa" + - result.end_state.providerType == "org.keycloak.keys.KeyProvider" + - result.end_state.config.active == ["true"] + - result.end_state.config.enabled == ["true"] + - result.end_state.config.algorithm == ["RS256"] + - result.end_state.config.priority == ["150"] + - result.msg == "Realm key testkey_with_certificate was in sync" + +- name: Remove Keycloak test realm + community.general.keycloak_realm: + auth_keycloak_url: "{{ url }}" + auth_realm: "{{ admin_realm }}" + auth_username: "{{ admin_user }}" + auth_password: "{{ admin_password }}" + realm: "{{ realm }}" + id: "{{ realm }}" + state: absent diff --git a/tests/integration/targets/keycloak_realm_key/vars/main.yml b/tests/integration/targets/keycloak_realm_key/vars/main.yml new file mode 100644 index 0000000000..d39cf8f737 --- /dev/null +++ b/tests/integration/targets/keycloak_realm_key/vars/main.yml @@ -0,0 +1,48 @@ +--- +# 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: realm_key_test +realm_private_key_name: testkey +realm_private_key: | + -----BEGIN PRIVATE KEY----- + MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQC9Mi7IKXPhqGiWGwgEYEXnqc8nytG1pHbC6QYZe1gUa43jDtGYQln36It02BGw4e5XydCUj+M26X2sH+kKaV+KHEnJtcEqdAuVX1QaDVzeiOoo1/B9HC8By6NZBsOSdxpat3EvilQ+R7NP9yi53J08+vfeZSEGyPmKV1me7nJnRh3/zcRsOi92GTsBd7gApKfP8sorDjY8m9NRuPLwleK2nh/aRvj1yK8x3UAqUIbOCVaE39bSN6VUTFK2Q/+MX3vF0Zugsk7PKKmfqcEW6wj7dtSElbX4uhrfTkGMmwIWdIiLDNRA/jVRvGxUB1SyMy6kmMC8jC2QGWpZgfkSKtHlAgMBAAECggEACWkSVh7ntmjtwM+Z47vVJkt2NBS8vxPt206DYOeXbzaVUV6mkrP0LSZKL3bi1GE8fW3am9UXWF8fQt04dm3c1G4JRojtkXrBq72Y3Y3eGWyGdx8chWCOPwDdwFsbhbC6ZRo8PUDcZVekJd1Vj38XbBXQl+WAUcnTzauAF+1kz9mhJq1gpglIbB+8l7VjMXwXeaGWJQ5OL/MSsq7r3P1elVjHwprFBM7HHA5+RTu/KY/GcEutgm5uwTRqRZNC1IBXAQtBO7HQJbuLqDPTQ3RRCPEur8R+0dk5bF+8IyzQ8Bh+Dhuou9xzfS/A7lV6L/CZSpv4Bvq1H3Uxk+orXf2Q2QKBgQDBOf1nSJB0VgQdIcdtgVpVgQ2SyWAd+N8Qk7QsyVQf9f7ZqiFLejWJbaaeY9WtfZ01D8tgHJfPqsO1/Jux255mtkyk2K2c6dav1Lsd4l+iPfidsDJNWkcd59nQqwC9BLjzWK/J4rO20apm34abLaZ9oVk8Mgz8VWJWOxTgCr+COQKBgQD6qP1lm6rzlCSIEz9eCuGPkQkVo+NIP437e3i+sxtkLlMgnmfzSwSJdVF8AKH3gXi3NyWjfBVYeAZEkm1kHF8IWOiK4U1y95Vx3uud3NX4SC+cjePc+pDPQJiz9L+zq9I6WFZWmm7n/9heTxu/l0vxI4FHaBmt95BMwLJNkzbdDQKBgCHGwUUMqjOr1YxCG1pJAkFwDa9bBDI5DsUXDKfHia0Mkz/5PVi0RCeBw15slS1+h7x+xk5GsULb1to5Df5JJadOtpcaST7koWKbDRpsN8tkidEGu8RJw6S2opyXR8nCyZHALvpbZo7Ol7rj1+PIVxIe4jpjhWGWi1oHed6wAkoBAoGAJx2F5XxEUhx1EvMF+XPzPQciBsl7Z0PbsTnUXtXuWVTNThLKH/I99AFlxNcIb2o530VwzzFG13Zra/n5rhyrS88sArgj8OPn40wpMopKraL+Iw0VWN+VB3KKIdL4s14FwWsVlhAlbHjFV/o6V0yR4kBrJSx+jWJLl16etHJbpmUCgYBUWCQwcT1aw9XHWJXiNYTnQSYg88hgGYhts1qSzhfu+n1t2BlAlxM0gu2+gez21mM8uiYsqbU2OZeG2U4as6kdai8Q4tzNQt2f1r3ZewJN/QHrkx6FT94PNa0w4ILiQ9Eu7xssaHcYjHyrI1NlbMKypVy6waDG2ajLOFAVeHGpOg== + -----END PRIVATE KEY----- +realm_certificate: | + -----BEGIN CERTIFICATE----- + MIIDQDCCAiigAwIBAgIUMfPlHWcZn6xfeSjfbhgmt4yy6mMwDQYJKoZIhvcNAQELBQAwQjELMAkGA1UEBhMCWFgxFTATBgNVBAcMDERlZmF1bHQgQ2l0eTEcMBoGA1UECgwTRGVmYXVsdCBDb21wYW55IEx0ZDAeFw0yMzA4MTgxMTU5MDFaFw0zMzA4MTUxMTU5MDFaMEIxCzAJBgNVBAYTAlhYMRUwEwYDVQQHDAxEZWZhdWx0IENpdHkxHDAaBgNVBAoME0RlZmF1bHQgQ29tcGFueSBMdGQwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC9Mi7IKXPhqGiWGwgEYEXnqc8nytG1pHbC6QYZe1gUa43jDtGYQln36It02BGw4e5XydCUj+M26X2sH+kKaV+KHEnJtcEqdAuVX1QaDVzeiOoo1/B9HC8By6NZBsOSdxpat3EvilQ+R7NP9yi53J08+vfeZSEGyPmKV1me7nJnRh3/zcRsOi92GTsBd7gApKfP8sorDjY8m9NRuPLwleK2nh/aRvj1yK8x3UAqUIbOCVaE39bSN6VUTFK2Q/+MX3vF0Zugsk7PKKmfqcEW6wj7dtSElbX4uhrfTkGMmwIWdIiLDNRA/jVRvGxUB1SyMy6kmMC8jC2QGWpZgfkSKtHlAgMBAAGjLjAsMAsGA1UdDwQEAwIEkDAdBgNVHQ4EFgQUcZirWRV5EzRhanUVSQ9rmAavVbEwDQYJKoZIhvcNAQELBQADggEBAIt2aFr/sxvtZfDc+Nb9tgspBuoX8f9Gf9mrS6dTdvdqSMHQrcoejSEEAZNljdSpKAhnhyR3+uCIev++WS4tixZoooQ8aYxDGNIwyry51GNEK7LKXVRmkbZFODidRuYZ1XWQORaJoaXWplaPaNtLvUr1swachz36K4n8/UIi109w/addajOHFbFGAzUmGRR4saMZPGrQCaNFje7G1o5wb/mQD1L+Jfk81Id5/F6NFBsSEIi+/O7Xs7fOWuab6cdfwI7zQQclEo55WQkLXefFLn+Ju0Ftgl023awpNEE4pjl6jD5VSEOkQ+I2sxGvymgjz7Av4zPOD/Lr05lRnMxf8dA= + -----END CERTIFICATE----- +realm_private_key_2: | + -----BEGIN PRIVATE KEY----- + MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCyQ5FKuqbnWEtt + KI0FHKFvd+G/RyEI2ow29Ytjs3fZ7/gMYfXozHLLJl3jgCOvSf9Ta55arL0XnCCf + RKQb0vpgMmOTQw++A1UmNXe8atTczZMRiHMHFdLhXUvUKthcMGOrTH8xegCnm0bG + rZwimjQDog/kROMAN78Uv8SD1lMpGBxPr2DWXNl4kRF670m/jC0cM7SeDGCCKVF5 + SEh6rMDgI62AxKnbtxuAbF9SOO/6kTsYv5+dc8wxDEb0aaT1jC1CLhjAVmjc6vO7 + WHE0LLas+ARs4ghMONLN6VdOkJxBuEtlLqM3M+/viD1TRftZCeLarYLWdEsg8Yz9 + Ufb0oawzAgMBAAECggEARqPDxWsljHNOoFj7WNU5m6RTzqpvCsUf3v96Vu3dRn1z + O+Ttv2yU6K+xcN9sRJ/8D6CLxb7Bx8NUoghfR69ZDBmrn8VpTZCgg12Yrw9efojw + CHibrGkXgbqou9CmoBGEzXKozIBeFgzQBRby0jts9SuZRImPspxkmeJMCzo5BgUg + ksNibaWikvUJYMgFc7PdXEvxhCKcWTTGC3fxJwpRxXkqKsYDa3JhdhloH8hHqynm + o7WEXeGAn4UV7C1tg3OdTciHn/ONMRItPcyonwk19meZTvsEub6ZsNjVg/5oJVBr + WG8vPZBi1VzAMayDXxDOnEAKW5eJXRSNX1vZ7EQTEQKBgQDXg5pSp9hVdVZc+eN/ + Ab/1NMMdgrQdbyTeB9esjLiwNuXysQm/KaG8gTkLpiKVvJ8R7SOcxb9Y5Gt9Y5Ej + eu943V4zLDIzNt/ST4bXGW/gQ84zkMBdhKz9hKA5tartVjI1ycznjpDbgn/jAYPI + 8VXGmjID2oDIJ7P+dLD8lMBDvQKBgQDTwIyimy+4EwFUuuppfWArXRsqsWUScGWD + +06xbc+Ld92LJBvakvSTdDNnS/PlYGl/fJjqQ4wq5UPREJYCi3UW9I5jtfsIg8Pl + oCnIhEYkn8xPZ7X8grU4emkM6QAPhstCDlXE6t0T202TpYVYjtEEDRQu4rKAbJ0h + gqSh5Ge2rwKBgEjrx6jWEBYCaOF20ComTmxKmQaANi+Lbt8NqkVBLDC7spymmJSt + IoOk+cdeRG+D7hLjuVwPcQpD57b6nJ5zt1mfFYOdHbNEiwEfVZGskrVAXCIIhX5f + KSVy3cAJHzfFJaIbkRB8pbkQc/M8jPnN5ucXP3scUNzoyjd8BnLAZjnFAoGAWwwY + rDYTz48EbH0uG4uYFS0kaDf8YHBJhfVBgdLYgXxZmuE8xL+ZP+mfzJOA3CiXVASr + 71Z551vKzBLYnWF/SA6BRuhRdvjI+2vha2FMk6TOAXpzao59AzrG/pEUwJhRvyZQ + xKnDwyzxb0GlU02dG6PQANTisYuCCI2W4jFGUusCgYB72p5o5uBr7qrFMTdMMxxe + f/9Go/9QBR/uNYk3D/rWj0F/bXGbiYMddNMD4v3XE24NL4ZvBJn0Po64Tuz5+wtu + 5ICKc6ED1l55MPsKdegVMpXGIFRjZt2TtCk4FE68m5QJpT1IIK7I9jv0+FGKjFYa + ukdTEghu13cANd8eKpxBsQ== + -----END PRIVATE KEY-----