From 1b80a9c5879f343a915b281da0cffaff79c2ca22 Mon Sep 17 00:00:00 2001 From: Gaetan2907 <48204380+Gaetan2907@users.noreply.github.com> Date: Fri, 9 Jul 2021 07:33:35 +0100 Subject: [PATCH] Add option to the keycloak_client module (#2949) * Add authentication_flow_binding_overrides option to the keycloak_client module * Add changelog fragment * Update changelogs/fragments/2949-add_authentication-flow-binding_keycloak-client.yml Co-authored-by: Amin Vakil * Update plugins/modules/identity/keycloak/keycloak_client.py Co-authored-by: Amin Vakil * Update plugins/modules/identity/keycloak/keycloak_client.py Co-authored-by: Amin Vakil * Add unit test authentication_flow_binding_overrides feature on keycloak_client module Co-authored-by: Amin Vakil --- ...ntication-flow-binding_keycloak-client.yml | 3 + .../identity/keycloak/keycloak_client.py | 11 ++ .../identity/keycloak/test_keycloak_client.py | 150 ++++++++++++++++++ 3 files changed, 164 insertions(+) create mode 100644 changelogs/fragments/2949-add_authentication-flow-binding_keycloak-client.yml create mode 100644 tests/unit/plugins/modules/identity/keycloak/test_keycloak_client.py diff --git a/changelogs/fragments/2949-add_authentication-flow-binding_keycloak-client.yml b/changelogs/fragments/2949-add_authentication-flow-binding_keycloak-client.yml new file mode 100644 index 0000000000..cdc0d4ae69 --- /dev/null +++ b/changelogs/fragments/2949-add_authentication-flow-binding_keycloak-client.yml @@ -0,0 +1,3 @@ +--- +minor_changes: + - keycloak_client - add ``authentication_flow_binding_overrides`` option (https://github.com/ansible-collections/community.general/pull/2949). diff --git a/plugins/modules/identity/keycloak/keycloak_client.py b/plugins/modules/identity/keycloak/keycloak_client.py index e3e39fc173..e37997e752 100644 --- a/plugins/modules/identity/keycloak/keycloak_client.py +++ b/plugins/modules/identity/keycloak/keycloak_client.py @@ -318,6 +318,14 @@ options: aliases: - authorizationSettings + authentication_flow_binding_overrides: + description: + - Override realm authentication flow bindings. + type: dict + aliases: + - authenticationFlowBindingOverrides + version_added: 3.4.0 + protocol_mappers: description: - a list of dicts defining protocol mappers for this client. @@ -593,6 +601,8 @@ EXAMPLES = ''' default_roles: - test01 - test02 + authentication_flow_binding_overrides: + browser: 4c90336b-bf1d-4b87-916d-3677ba4e5fbb protocol_mappers: - config: access.token.claim: True @@ -745,6 +755,7 @@ def main(): use_template_config=dict(type='bool', aliases=['useTemplateConfig']), use_template_scope=dict(type='bool', aliases=['useTemplateScope']), use_template_mappers=dict(type='bool', aliases=['useTemplateMappers']), + authentication_flow_binding_overrides=dict(type='dict', aliases=['authenticationFlowBindingOverrides']), protocol_mappers=dict(type='list', elements='dict', options=protmapper_spec, aliases=['protocolMappers']), authorization_settings=dict(type='dict', aliases=['authorizationSettings']), ) diff --git a/tests/unit/plugins/modules/identity/keycloak/test_keycloak_client.py b/tests/unit/plugins/modules/identity/keycloak/test_keycloak_client.py new file mode 100644 index 0000000000..e017a5985c --- /dev/null +++ b/tests/unit/plugins/modules/identity/keycloak/test_keycloak_client.py @@ -0,0 +1,150 @@ +# -*- coding: utf-8 -*- + +# Copyright: (c) 2021, Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +from contextlib import contextmanager + +from ansible_collections.community.general.tests.unit.compat import unittest +from ansible_collections.community.general.tests.unit.compat.mock import call, patch +from ansible_collections.community.general.tests.unit.plugins.modules.utils import AnsibleExitJson, AnsibleFailJson, \ + ModuleTestCase, set_module_args + +from ansible_collections.community.general.plugins.modules.identity.keycloak import keycloak_client + +from itertools import count + +from ansible.module_utils.six import StringIO + + +@contextmanager +def patch_keycloak_api(get_client_by_clientid=None, get_client_by_id=None, update_client=None, create_client=None, + delete_client=None): + """Mock context manager for patching the methods in PwPolicyIPAClient that contact the IPA server + + Patches the `login` and `_post_json` methods + + Keyword arguments are passed to the mock object that patches `_post_json` + + No arguments are passed to the mock object that patches `login` because no tests require it + + Example:: + + with patch_ipa(return_value={}) as (mock_login, mock_post): + ... + """ + + obj = keycloak_client.KeycloakAPI + with patch.object(obj, 'get_client_by_clientid', side_effect=get_client_by_clientid) as mock_get_client_by_clientid: + with patch.object(obj, 'get_client_by_id', side_effect=get_client_by_id) as mock_get_client_by_id: + with patch.object(obj, 'create_client', side_effect=create_client) as mock_create_client: + with patch.object(obj, 'update_client', side_effect=update_client) as mock_update_client: + with patch.object(obj, 'delete_client', side_effect=delete_client) as mock_delete_client: + yield mock_get_client_by_clientid, mock_get_client_by_id, mock_create_client, mock_update_client, mock_delete_client + + +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 TestKeycloakRealm(ModuleTestCase): + def setUp(self): + super(TestKeycloakRealm, self).setUp() + self.module = keycloak_client + + def test_authentication_flow_binding_overrides_feature(self): + """Add a new realm""" + + module_args = { + 'auth_keycloak_url': 'https: // auth.example.com / auth', + 'token': '{{ access_token }}', + 'state': 'present', + 'realm': 'master', + 'client_id': 'test', + 'authentication_flow_binding_overrides': { + 'browser': '4c90336b-bf1d-4b87-916d-3677ba4e5fbb' + } + } + return_value_get_client_by_clientid = [ + None, + { + "authenticationFlowBindingOverrides": { + "browser": "f9502b6d-d76a-4efe-8331-2ddd853c9f9c" + }, + "clientId": "onboardingid", + "enabled": "true", + "protocol": "openid-connect", + "redirectUris": [ + "*" + ] + } + ] + changed = True + + set_module_args(module_args) + + # Run the module + + with mock_good_connection(): + with patch_keycloak_api(get_client_by_clientid=return_value_get_client_by_clientid) \ + as (mock_get_client_by_clientid, mock_get_client_by_id, mock_create_client, mock_update_client, mock_delete_client): + with self.assertRaises(AnsibleExitJson) as exec_info: + self.module.main() + + self.assertEqual(mock_get_client_by_clientid.call_count, 2) + self.assertEqual(mock_get_client_by_id.call_count, 0) + self.assertEqual(mock_create_client.call_count, 1) + self.assertEqual(mock_update_client.call_count, 0) + self.assertEqual(mock_delete_client.call_count, 0) + + # 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()