add plugin for generic keycloak component (#8826)
* add plugin for generic keycloak component * add changelog fragment * fix import in test * Update plugins/modules/keycloak_component.py Co-authored-by: Felix Fontein <felix@fontein.de> * Update plugins/modules/keycloak_component.py Co-authored-by: Felix Fontein <felix@fontein.de> * Update plugins/modules/keycloak_component.py Co-authored-by: Felix Fontein <felix@fontein.de> * Update plugins/modules/keycloak_component.py Co-authored-by: Felix Fontein <felix@fontein.de> * set correct supported diff_mode * fix line lenght * Update docblock Co-authored-by: Felix Fontein <felix@fontein.de> * Update changelogs/fragments/8826-keycloak-component.yml Co-authored-by: Alexei Znamensky <103110+russoz@users.noreply.github.com> * Update plugins/modules/keycloak_component.py Co-authored-by: Alexei Znamensky <103110+russoz@users.noreply.github.com> * Update plugins/modules/keycloak_component.py Co-authored-by: Alexei Znamensky <103110+russoz@users.noreply.github.com> * update docblocks * add entry to BOTMETA.yml * update copyright * Set Version number Co-authored-by: Alexei Znamensky <103110+russoz@users.noreply.github.com> * remove changelog fragment --------- Co-authored-by: Felix Fontein <felix@fontein.de> Co-authored-by: Alexei Znamensky <103110+russoz@users.noreply.github.com>pull/9060/head
parent
93be499f26
commit
9fb686fe35
|
@ -800,6 +800,8 @@ files:
|
||||||
maintainers: fynncfchen johncant
|
maintainers: fynncfchen johncant
|
||||||
$modules/keycloak_clientsecret_regenerate.py:
|
$modules/keycloak_clientsecret_regenerate.py:
|
||||||
maintainers: fynncfchen johncant
|
maintainers: fynncfchen johncant
|
||||||
|
$modules/keycloak_component.py:
|
||||||
|
maintainers: fivetide
|
||||||
$modules/keycloak_group.py:
|
$modules/keycloak_group.py:
|
||||||
maintainers: adamgoossens
|
maintainers: adamgoossens
|
||||||
$modules/keycloak_identity_provider.py:
|
$modules/keycloak_identity_provider.py:
|
||||||
|
|
|
@ -0,0 +1,323 @@
|
||||||
|
#!/usr/bin/python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# Copyright (c) 2024, Björn Bösel <bjoernboesel@gmail.com>
|
||||||
|
# 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_component
|
||||||
|
|
||||||
|
short_description: Allows administration of Keycloak components via Keycloak API
|
||||||
|
|
||||||
|
version_added: 10.0.0
|
||||||
|
|
||||||
|
description:
|
||||||
|
- This module allows the administration of Keycloak components 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, C(admin-cli)
|
||||||
|
and an C(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/latest/rest-api/index.html).
|
||||||
|
Aliases are provided so camelCased versions can be used as well.
|
||||||
|
|
||||||
|
attributes:
|
||||||
|
check_mode:
|
||||||
|
support: full
|
||||||
|
diff_mode:
|
||||||
|
support: full
|
||||||
|
|
||||||
|
options:
|
||||||
|
state:
|
||||||
|
description:
|
||||||
|
- State of the Keycloak component.
|
||||||
|
- On V(present), the component will be created (or updated if it exists already).
|
||||||
|
- On V(absent), the component will be removed if it exists.
|
||||||
|
choices: ['present', 'absent']
|
||||||
|
default: 'present'
|
||||||
|
type: str
|
||||||
|
name:
|
||||||
|
description:
|
||||||
|
- Name of the component to create.
|
||||||
|
type: str
|
||||||
|
required: true
|
||||||
|
parent_id:
|
||||||
|
description:
|
||||||
|
- The parent_id of the component. In practice the ID (name) of the realm.
|
||||||
|
type: str
|
||||||
|
required: true
|
||||||
|
provider_id:
|
||||||
|
description:
|
||||||
|
- The name of the "provider ID" for the key.
|
||||||
|
type: str
|
||||||
|
required: true
|
||||||
|
provider_type:
|
||||||
|
description:
|
||||||
|
- The name of the "provider type" for the key. That is, V(org.keycloak.storage.UserStorageProvider),
|
||||||
|
V(org.keycloak.userprofile.UserProfileProvider), ...
|
||||||
|
- See U(https://www.keycloak.org/docs/latest/server_development/index.html#_providers).
|
||||||
|
type: str
|
||||||
|
required: true
|
||||||
|
config:
|
||||||
|
description:
|
||||||
|
- Configuration properties for the provider.
|
||||||
|
- Contents vary depending on the provider type.
|
||||||
|
type: dict
|
||||||
|
|
||||||
|
extends_documentation_fragment:
|
||||||
|
- community.general.keycloak
|
||||||
|
- community.general.attributes
|
||||||
|
|
||||||
|
author:
|
||||||
|
- Björn Bösel (@fivetide)
|
||||||
|
'''
|
||||||
|
|
||||||
|
EXAMPLES = '''
|
||||||
|
- name: Manage Keycloak User Storage Provider
|
||||||
|
community.general.keycloak_component:
|
||||||
|
auth_keycloak_url: http://localhost:8080/auth
|
||||||
|
auth_username: keycloak
|
||||||
|
auth_password: keycloak
|
||||||
|
auth_realm: master
|
||||||
|
name: my storage provider
|
||||||
|
state: present
|
||||||
|
parent_id: some_realm
|
||||||
|
provider_id: my storage
|
||||||
|
provider_type: "org.keycloak.storage.UserStorageProvider"
|
||||||
|
config:
|
||||||
|
myCustomKey: "my_custom_key"
|
||||||
|
cachePolicy: "NO_CACHE"
|
||||||
|
enabled: true
|
||||||
|
'''
|
||||||
|
|
||||||
|
RETURN = '''
|
||||||
|
end_state:
|
||||||
|
description: Representation of the keycloak_component after module execution.
|
||||||
|
returned: on success
|
||||||
|
type: dict
|
||||||
|
contains:
|
||||||
|
id:
|
||||||
|
description: ID of the component.
|
||||||
|
type: str
|
||||||
|
returned: when O(state=present)
|
||||||
|
sample: 5b7ec13f-99da-46ad-8326-ab4c73cf4ce4
|
||||||
|
name:
|
||||||
|
description: Name of the component.
|
||||||
|
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: component configuration.
|
||||||
|
type: dict
|
||||||
|
'''
|
||||||
|
|
||||||
|
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():
|
||||||
|
argument_spec = keycloak_argument_spec()
|
||||||
|
|
||||||
|
meta_args = dict(
|
||||||
|
state=dict(type='str', default='present', choices=['present', 'absent']),
|
||||||
|
name=dict(type='str', required=True),
|
||||||
|
parent_id=dict(type='str', required=True),
|
||||||
|
provider_id=dict(type='str', required=True),
|
||||||
|
provider_type=dict(type='str', required=True),
|
||||||
|
config=dict(
|
||||||
|
type='dict',
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
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']]))
|
||||||
|
|
||||||
|
result = dict(changed=False, msg='', end_state={}, diff=dict(before={}, after={}))
|
||||||
|
|
||||||
|
# This will include the current state of the component if it is already
|
||||||
|
# present. This is only used for diff-mode.
|
||||||
|
before_component = {}
|
||||||
|
before_component['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", "parent_id"]
|
||||||
|
|
||||||
|
# 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]
|
||||||
|
|
||||||
|
provider_type = module.params.get("provider_type")
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
||||||
|
# Make a deep copy of the changeset. This is use when determining
|
||||||
|
# changes to the current state.
|
||||||
|
changeset_copy = deepcopy(changeset)
|
||||||
|
|
||||||
|
# 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')
|
||||||
|
provider_type = module.params.get('provider_type')
|
||||||
|
parent_id = module.params.get('parent_id')
|
||||||
|
|
||||||
|
# Get a list of all Keycloak components that are of keyprovider type.
|
||||||
|
current_components = kc.get_components(urlencode(dict(type=provider_type)), parent_id)
|
||||||
|
|
||||||
|
# If this component is present get its key ID. Confusingly the key ID is
|
||||||
|
# also known as the Provider ID.
|
||||||
|
component_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 component in current_components:
|
||||||
|
if component['name'] == name:
|
||||||
|
component_id = component['id']
|
||||||
|
changeset['id'] = component_id
|
||||||
|
changeset_copy['id'] = component_id
|
||||||
|
|
||||||
|
# Compare top-level parameters
|
||||||
|
for param, value in changeset.items():
|
||||||
|
before_component[param] = component[param]
|
||||||
|
|
||||||
|
if changeset_copy[param] != component[param] and param != 'config':
|
||||||
|
changes += "%s: %s -> %s, " % (param, component[param], changeset_copy[param])
|
||||||
|
result['changed'] = True
|
||||||
|
# Compare parameters under the "config" key
|
||||||
|
for p, v in changeset_copy['config'].items():
|
||||||
|
try:
|
||||||
|
before_component['config'][p] = component['config'][p] or []
|
||||||
|
except KeyError:
|
||||||
|
before_component['config'][p] = []
|
||||||
|
if changeset_copy['config'][p] != component['config'][p]:
|
||||||
|
changes += "config.%s: %s -> %s, " % (p, component['config'][p], changeset_copy['config'][p])
|
||||||
|
result['changed'] = True
|
||||||
|
|
||||||
|
# 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 component_id and state == 'present':
|
||||||
|
if result['changed']:
|
||||||
|
if module._diff:
|
||||||
|
result['diff'] = dict(before=before_component, after=changeset_copy)
|
||||||
|
|
||||||
|
if module.check_mode:
|
||||||
|
result['msg'] = "Component %s would be changed: %s" % (name, changes.strip(", "))
|
||||||
|
else:
|
||||||
|
kc.update_component(changeset, parent_id)
|
||||||
|
result['msg'] = "Component %s changed: %s" % (name, changes.strip(", "))
|
||||||
|
else:
|
||||||
|
result['msg'] = "Component %s was in sync" % (name)
|
||||||
|
|
||||||
|
result['end_state'] = changeset_copy
|
||||||
|
elif component_id and state == 'absent':
|
||||||
|
if module._diff:
|
||||||
|
result['diff'] = dict(before=before_component, after={})
|
||||||
|
|
||||||
|
if module.check_mode:
|
||||||
|
result['changed'] = True
|
||||||
|
result['msg'] = "Component %s would be deleted" % (name)
|
||||||
|
else:
|
||||||
|
kc.delete_component(component_id, parent_id)
|
||||||
|
result['changed'] = True
|
||||||
|
result['msg'] = "Component %s deleted" % (name)
|
||||||
|
|
||||||
|
result['end_state'] = {}
|
||||||
|
elif not component_id and state == 'present':
|
||||||
|
if module._diff:
|
||||||
|
result['diff'] = dict(before={}, after=changeset_copy)
|
||||||
|
|
||||||
|
if module.check_mode:
|
||||||
|
result['changed'] = True
|
||||||
|
result['msg'] = "Component %s would be created" % (name)
|
||||||
|
else:
|
||||||
|
kc.create_component(changeset, parent_id)
|
||||||
|
result['changed'] = True
|
||||||
|
result['msg'] = "Component %s created" % (name)
|
||||||
|
|
||||||
|
result['end_state'] = changeset_copy
|
||||||
|
elif not component_id and state == 'absent':
|
||||||
|
result['changed'] = False
|
||||||
|
result['msg'] = "Component %s not present" % (name)
|
||||||
|
result['end_state'] = {}
|
||||||
|
|
||||||
|
module.exit_json(**result)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
|
@ -0,0 +1,327 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# Copyright (c) 2021, 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
|
||||||
|
|
||||||
|
from contextlib import contextmanager
|
||||||
|
from itertools import count
|
||||||
|
|
||||||
|
from ansible.module_utils.six import StringIO
|
||||||
|
from ansible_collections.community.general.plugins.modules import keycloak_realm_key
|
||||||
|
from ansible_collections.community.general.tests.unit.compat import unittest
|
||||||
|
from ansible_collections.community.general.tests.unit.compat.mock import patch
|
||||||
|
from ansible_collections.community.general.tests.unit.plugins.modules.utils import AnsibleExitJson, ModuleTestCase, set_module_args
|
||||||
|
|
||||||
|
from ansible_collections.community.general.plugins.modules import keycloak_component
|
||||||
|
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def patch_keycloak_api(get_components=None, create_component=None, update_component=None, delete_component=None):
|
||||||
|
"""Mock context manager for patching the methods in KeycloakAPI
|
||||||
|
"""
|
||||||
|
|
||||||
|
obj = keycloak_realm_key.KeycloakAPI
|
||||||
|
with patch.object(obj, 'get_components', side_effect=get_components) \
|
||||||
|
as mock_get_components:
|
||||||
|
with patch.object(obj, 'create_component', side_effect=create_component) \
|
||||||
|
as mock_create_component:
|
||||||
|
with patch.object(obj, 'update_component', side_effect=update_component) \
|
||||||
|
as mock_update_component:
|
||||||
|
with patch.object(obj, 'delete_component', side_effect=delete_component) \
|
||||||
|
as mock_delete_component:
|
||||||
|
yield mock_get_components, mock_create_component, mock_update_component, mock_delete_component
|
||||||
|
|
||||||
|
|
||||||
|
def get_response(object_with_future_response, method, get_id_call_count):
|
||||||
|
if callable(object_with_future_response):
|
||||||
|
return object_with_future_response()
|
||||||
|
if isinstance(object_with_future_response, dict):
|
||||||
|
return get_response(
|
||||||
|
object_with_future_response[method], method, get_id_call_count)
|
||||||
|
if isinstance(object_with_future_response, list):
|
||||||
|
call_number = next(get_id_call_count)
|
||||||
|
return get_response(
|
||||||
|
object_with_future_response[call_number], method, get_id_call_count)
|
||||||
|
return object_with_future_response
|
||||||
|
|
||||||
|
|
||||||
|
def build_mocked_request(get_id_user_count, response_dict):
|
||||||
|
def _mocked_requests(*args, **kwargs):
|
||||||
|
url = args[0]
|
||||||
|
method = kwargs['method']
|
||||||
|
future_response = response_dict.get(url, None)
|
||||||
|
return get_response(future_response, method, get_id_user_count)
|
||||||
|
|
||||||
|
return _mocked_requests
|
||||||
|
|
||||||
|
|
||||||
|
def create_wrapper(text_as_string):
|
||||||
|
"""Allow to mock many times a call to one address.
|
||||||
|
Without this function, the StringIO is empty for the second call.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def _create_wrapper():
|
||||||
|
return StringIO(text_as_string)
|
||||||
|
|
||||||
|
return _create_wrapper
|
||||||
|
|
||||||
|
|
||||||
|
def mock_good_connection():
|
||||||
|
token_response = {
|
||||||
|
'http://keycloak.url/auth/realms/master/protocol/openid-connect/token': create_wrapper('{"access_token": "alongtoken"}'), }
|
||||||
|
return patch(
|
||||||
|
'ansible_collections.community.general.plugins.module_utils.identity.keycloak.keycloak.open_url',
|
||||||
|
side_effect=build_mocked_request(count(), token_response),
|
||||||
|
autospec=True
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestKeycloakComponent(ModuleTestCase):
|
||||||
|
def setUp(self):
|
||||||
|
super(TestKeycloakComponent, self).setUp()
|
||||||
|
self.module = keycloak_component
|
||||||
|
|
||||||
|
def test_create_when_absent(self):
|
||||||
|
"""Add a new realm key"""
|
||||||
|
|
||||||
|
module_args = {
|
||||||
|
'auth_keycloak_url': 'http://keycloak.url/auth',
|
||||||
|
'auth_realm': 'master',
|
||||||
|
'auth_username': 'admin',
|
||||||
|
'auth_password': 'admin',
|
||||||
|
'parent_id': 'realm-name',
|
||||||
|
'name': 'test-user-provider',
|
||||||
|
'state': 'present',
|
||||||
|
'provider_id': 'my-provider',
|
||||||
|
'provider_type': 'org.keycloak.storage.UserStorageProvider',
|
||||||
|
'config': {
|
||||||
|
'enabled': True,
|
||||||
|
'my_custom_config': 'foo',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return_value_component_create = [
|
||||||
|
{
|
||||||
|
"id": "ebb7d999-60cc-4dfe-ab79-48f7bbd9d4d9",
|
||||||
|
"name": "test-user-provider",
|
||||||
|
"providerId": "my-provider",
|
||||||
|
"parentId": "90c8fef9-15f8-4d5b-8b22-44e2e1cdcd09",
|
||||||
|
"config": {
|
||||||
|
"myCustomConfig": [
|
||||||
|
"foo",
|
||||||
|
],
|
||||||
|
"enabled": [
|
||||||
|
"true"
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
# get before_comp, get default_mapper, get after_mapper
|
||||||
|
return_value_components_get = [
|
||||||
|
[], [], []
|
||||||
|
]
|
||||||
|
changed = True
|
||||||
|
|
||||||
|
set_module_args(module_args)
|
||||||
|
|
||||||
|
# Run the module
|
||||||
|
|
||||||
|
with mock_good_connection():
|
||||||
|
with patch_keycloak_api(get_components=return_value_components_get, create_component=return_value_component_create) \
|
||||||
|
as (mock_get_components, mock_create_component, mock_update_component, mock_delete_component):
|
||||||
|
with self.assertRaises(AnsibleExitJson) as exec_info:
|
||||||
|
self.module.main()
|
||||||
|
|
||||||
|
self.assertEqual(len(mock_get_components.mock_calls), 1)
|
||||||
|
self.assertEqual(len(mock_create_component.mock_calls), 1)
|
||||||
|
self.assertEqual(len(mock_update_component.mock_calls), 0)
|
||||||
|
self.assertEqual(len(mock_delete_component.mock_calls), 0)
|
||||||
|
|
||||||
|
# must not contain parent_id
|
||||||
|
mock_create_component.assert_called_once_with({
|
||||||
|
'name': 'test-user-provider',
|
||||||
|
'providerId': 'my-provider',
|
||||||
|
'providerType': 'org.keycloak.storage.UserStorageProvider',
|
||||||
|
'config': {
|
||||||
|
'enabled': ['true'],
|
||||||
|
'myCustomConfig': ['foo'],
|
||||||
|
},
|
||||||
|
}, 'realm-name')
|
||||||
|
|
||||||
|
# Verify that the module's changed status matches what is expected
|
||||||
|
self.assertIs(exec_info.exception.args[0]['changed'], changed)
|
||||||
|
|
||||||
|
def test_create_when_present(self):
|
||||||
|
"""Update existing realm key"""
|
||||||
|
|
||||||
|
module_args = {
|
||||||
|
'auth_keycloak_url': 'http://keycloak.url/auth',
|
||||||
|
'auth_realm': 'master',
|
||||||
|
'auth_username': 'admin',
|
||||||
|
'auth_password': 'admin',
|
||||||
|
'parent_id': 'realm-name',
|
||||||
|
'name': 'test-user-provider',
|
||||||
|
'state': 'present',
|
||||||
|
'provider_id': 'my-provider',
|
||||||
|
'provider_type': 'org.keycloak.storage.UserStorageProvider',
|
||||||
|
'config': {
|
||||||
|
'enabled': True,
|
||||||
|
'my_custom_config': 'foo',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return_value_components_get = [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "c1a957aa-3df0-4f70-9418-44202bf4ae1f",
|
||||||
|
"name": "test-user-provider",
|
||||||
|
"providerId": "rsa",
|
||||||
|
"providerType": "org.keycloak.storage.UserStorageProvider",
|
||||||
|
"parentId": "90c8fef9-15f8-4d5b-8b22-44e2e1cdcd09",
|
||||||
|
"config": {
|
||||||
|
"myCustomConfig": [
|
||||||
|
"foo",
|
||||||
|
],
|
||||||
|
"enabled": [
|
||||||
|
"true"
|
||||||
|
],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[],
|
||||||
|
[]
|
||||||
|
]
|
||||||
|
return_value_component_update = [
|
||||||
|
None
|
||||||
|
]
|
||||||
|
changed = True
|
||||||
|
|
||||||
|
set_module_args(module_args)
|
||||||
|
|
||||||
|
# Run the module
|
||||||
|
|
||||||
|
with mock_good_connection():
|
||||||
|
with patch_keycloak_api(get_components=return_value_components_get,
|
||||||
|
update_component=return_value_component_update) \
|
||||||
|
as (mock_get_components, mock_create_component, mock_update_component, mock_delete_component):
|
||||||
|
with self.assertRaises(AnsibleExitJson) as exec_info:
|
||||||
|
self.module.main()
|
||||||
|
|
||||||
|
self.assertEqual(len(mock_get_components.mock_calls), 1)
|
||||||
|
self.assertEqual(len(mock_create_component.mock_calls), 0)
|
||||||
|
self.assertEqual(len(mock_update_component.mock_calls), 1)
|
||||||
|
self.assertEqual(len(mock_delete_component.mock_calls), 0)
|
||||||
|
|
||||||
|
# Verify that the module's changed status matches what is expected
|
||||||
|
self.assertIs(exec_info.exception.args[0]['changed'], changed)
|
||||||
|
|
||||||
|
def test_delete_when_absent(self):
|
||||||
|
"""Remove an absent realm key"""
|
||||||
|
|
||||||
|
module_args = {
|
||||||
|
'auth_keycloak_url': 'http://keycloak.url/auth',
|
||||||
|
'auth_realm': 'master',
|
||||||
|
'auth_username': 'admin',
|
||||||
|
'auth_password': 'admin',
|
||||||
|
'parent_id': 'realm-name',
|
||||||
|
'name': 'test-user-provider',
|
||||||
|
'state': 'absent',
|
||||||
|
'provider_id': 'my-provider',
|
||||||
|
'provider_type': 'org.keycloak.storage.UserStorageProvider',
|
||||||
|
'config': {
|
||||||
|
'enabled': True,
|
||||||
|
'my_custom_config': 'foo',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return_value_components_get = [
|
||||||
|
[]
|
||||||
|
]
|
||||||
|
changed = False
|
||||||
|
|
||||||
|
set_module_args(module_args)
|
||||||
|
|
||||||
|
# Run the module
|
||||||
|
|
||||||
|
with mock_good_connection():
|
||||||
|
with patch_keycloak_api(get_components=return_value_components_get) \
|
||||||
|
as (mock_get_components, mock_create_component, mock_update_component, mock_delete_component):
|
||||||
|
with self.assertRaises(AnsibleExitJson) as exec_info:
|
||||||
|
self.module.main()
|
||||||
|
|
||||||
|
self.assertEqual(len(mock_get_components.mock_calls), 1)
|
||||||
|
self.assertEqual(len(mock_create_component.mock_calls), 0)
|
||||||
|
self.assertEqual(len(mock_update_component.mock_calls), 0)
|
||||||
|
self.assertEqual(len(mock_delete_component.mock_calls), 0)
|
||||||
|
|
||||||
|
# Verify that the module's changed status matches what is expected
|
||||||
|
self.assertIs(exec_info.exception.args[0]['changed'], changed)
|
||||||
|
|
||||||
|
def test_delete_when_present(self):
|
||||||
|
"""Remove an existing realm key"""
|
||||||
|
|
||||||
|
module_args = {
|
||||||
|
'auth_keycloak_url': 'http://keycloak.url/auth',
|
||||||
|
'auth_realm': 'master',
|
||||||
|
'auth_username': 'admin',
|
||||||
|
'auth_password': 'admin',
|
||||||
|
'parent_id': 'realm-name',
|
||||||
|
'name': 'test-user-provider',
|
||||||
|
'state': 'absent',
|
||||||
|
'provider_id': 'my-provider',
|
||||||
|
'provider_type': 'org.keycloak.storage.UserStorageProvider',
|
||||||
|
'config': {
|
||||||
|
'enabled': True,
|
||||||
|
'my_custom_config': 'foo',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return_value_components_get = [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "c1a957aa-3df0-4f70-9418-44202bf4ae1f",
|
||||||
|
"name": "test-user-provider",
|
||||||
|
"providerId": "my-provider",
|
||||||
|
"providerType": "org.keycloak.storage.UserStorageProvider",
|
||||||
|
"parentId": "90c8fef9-15f8-4d5b-8b22-44e2e1cdcd09",
|
||||||
|
"config": {
|
||||||
|
"myCustomConfig": [
|
||||||
|
"foo",
|
||||||
|
],
|
||||||
|
"enabled": [
|
||||||
|
"true"
|
||||||
|
],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[],
|
||||||
|
[]
|
||||||
|
]
|
||||||
|
return_value_component_delete = [
|
||||||
|
None
|
||||||
|
]
|
||||||
|
changed = True
|
||||||
|
|
||||||
|
set_module_args(module_args)
|
||||||
|
|
||||||
|
# Run the module
|
||||||
|
|
||||||
|
with mock_good_connection():
|
||||||
|
with patch_keycloak_api(get_components=return_value_components_get, delete_component=return_value_component_delete) \
|
||||||
|
as (mock_get_components, mock_create_component, mock_update_component, mock_delete_component):
|
||||||
|
with self.assertRaises(AnsibleExitJson) as exec_info:
|
||||||
|
self.module.main()
|
||||||
|
|
||||||
|
self.assertEqual(len(mock_get_components.mock_calls), 1)
|
||||||
|
self.assertEqual(len(mock_create_component.mock_calls), 0)
|
||||||
|
self.assertEqual(len(mock_update_component.mock_calls), 0)
|
||||||
|
self.assertEqual(len(mock_delete_component.mock_calls), 1)
|
||||||
|
|
||||||
|
# Verify that the module's changed status matches what is expected
|
||||||
|
self.assertIs(exec_info.exception.args[0]['changed'], changed)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
Loading…
Reference in New Issue