From f3082d504edf31cf8c311dae87d08c50378a4089 Mon Sep 17 00:00:00 2001 From: rsuplina Date: Wed, 21 Aug 2024 12:01:13 +0100 Subject: [PATCH] Add External_user_mappings module Signed-off-by: rsuplina --- plugins/modules/external_user_mappings.py | 337 ++++++++++++++++++ .../modules/external_user_mappings_info.py | 178 +++++++++ .../test_external_user_mappings.py | 128 +++++++ .../test_external_user_mappings_info.py | 88 +++++ 4 files changed, 731 insertions(+) create mode 100644 plugins/modules/external_user_mappings.py create mode 100644 plugins/modules/external_user_mappings_info.py create mode 100644 tests/unit/plugins/modules/external_user_mappings/test_external_user_mappings.py create mode 100644 tests/unit/plugins/modules/external_user_mappings_info/test_external_user_mappings_info.py diff --git a/plugins/modules/external_user_mappings.py b/plugins/modules/external_user_mappings.py new file mode 100644 index 00000000..67036a9c --- /dev/null +++ b/plugins/modules/external_user_mappings.py @@ -0,0 +1,337 @@ +# Copyright 2024 Cloudera, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from ansible_collections.cloudera.cluster.plugins.module_utils.cm_utils import ( + ClouderaManagerModule, +) + +from cm_client import ( + ExternalUserMappingsResourceApi, + ApiExternalUserMapping, + ApiAuthRoleRef, +) + +ANSIBLE_METADATA = { + "metadata_version": "1.1", + "status": ["preview"], + "supported_by": "community", +} + +DOCUMENTATION = r""" +--- +module: external_user_mappings +short_description: Create, update, or delete external user mappings +description: + - Configure details of a specific external user mapping. + - Create a new external user mapping. + - Update an existing external user mapping. + - Delete a external user mapping. + - The module supports C(check_mode). +author: + - "Ronald Suplina (@rsuplina)" +requirements: + - cm_client +options: + name: + description: + - The name of the external mapping. + type: str + required: no + uuid: + description: + - The uuid of the external mapping. + type: str + required: no + type: + description: + - The type of the external mapping. + type: str + required: no + auth_roles: + description: + - A list of auth roles that the external user mapping will include. + type: list + required: no + state: + description: + - If I(state=present), the external user mapping will be created or updated. + - If I(state=absent), the external user mapping will be updated or deleted. + type: str + required: no + default: present + choices: + - present + - absent + purge: + description: + - Flag for whether the declared auth roles should append or overwrite any existing auth roles. + type: bool + default: False +attributes: + check_mode: + support: full + diff_mode: + support: full +""" + +EXAMPLES = r""" +--- +- name: Create external user mapping with admin permissions + cloudera.cluster.external_user_mappings: + host: example.cloudera.com + username: "jane_smith" + password: "S&peR4Ec*re" + name: "admin_user" + state: "present" + type: "LDAP" + auth_roles: ["ROLE_CLUSTER_ADMIN"] + +- name: Add additional permissions to external user mapping + cloudera.cluster.external_user_mappings: + host: example.cloudera.com + username: "jane_smith" + password: "S&peR4Ec*re" + name: "basic_user" + state: "present" + type: "LDAP" + auth_roles: ["ROLE_DASHBOARD_USER","ROLE_USER","ROLE_CLUSTER_CREATOR"] + +- name: Replace current permissions in external user mapping + cloudera.cluster.external_user_mappings: + host: example.cloudera.com + username: "jane_smith" + password: "S&peR4Ec*re" + name: "basic_user" + state: "present" + purge: "True" + type: "LDAP" + auth_roles: ["ROLE_DASHBOARD_USER","ROLE_USER"] + +- name: Remove external user mapping + cloudera.cluster.external_user_mappings: + host: example.cloudera.com + username: "jane_smith" + password: "S&peR4Ec*re" + name: "default_user" + state: "absent" + type: "LDAP" + +- name: Remove permissions from external user mapping + cloudera.cluster.external_user_mappings: + host: example.cloudera.com + username: "jane_smith" + password: "S&peR4Ec*re" + name: "default_user" + state: "absent" + type: "LDAP" + auth_roles: ["ROLE_DASHBOARD_USER","ROLE_USER"] +""" + +RETURN = r""" +--- +external_user_mappings: + description: + - A dictionary containing details of external user mapping. + type: dict + elements: complex + returned: always + contains: + name: + description: + - The name of the external mapping. + type: str + returned: always + type: + description: + - The type of the external mapping. + type: str + returned: always + uuid: + description: + - The UUID of the external mapping. + type: str + returned: always + auth_roles: + description: + - The list of auth roles associated with external user mapping. + type: list + returned: always +""" + + +class ClouderaExternalUserMappingsInfo(ClouderaManagerModule): + def __init__(self, module): + super(ClouderaExternalUserMappingsInfo, self).__init__(module) + + # Set the parameters + self.name = self.get_param("name") + self.uuid = self.get_param("uuid") + self.state = self.get_param("state") + self.type = self.get_param("type") + self.purge = self.get_param("purge") + self.auth_roles = self.get_param("auth_roles") + + # Initialize the return value + self.host_template = [] + self.external_user_mappings_output = [] + self.changed = False + self.diff = {} + + # Execute the logic + self.process() + + @ClouderaManagerModule.handle_process + def process(self): + api_instance = ExternalUserMappingsResourceApi(self.api_client) + existing = [] + + if self.name: + all_external_user_mappings = api_instance.read_external_user_mappings() + for mapping in all_external_user_mappings.items: + if self.name == mapping.name: + existing = api_instance.read_external_user_mapping( + uuid=mapping.uuid + ).to_dict() + break + if self.uuid: + existing = api_instance.read_external_user_mapping(uuid=self.uuid).to_dict() + if self.state == "present": + if existing: + existing_auth_roles = [ + auth_role["name"] for auth_role in existing["auth_roles"] + ] + incoming_auth_roles = set(self.auth_roles) + if existing_auth_roles != incoming_auth_roles: + if self.module._diff: + self.diff.update( + before=list(existing_auth_roles - incoming_auth_roles), + after=list(incoming_auth_roles - existing_auth_roles), + ) + if self.purge: + target_auth_roles = incoming_auth_roles + else: + existing_auth_roles = set(existing_auth_roles) + new_auth_roles = incoming_auth_roles - existing_auth_roles + target_auth_roles = existing_auth_roles.union(new_auth_roles) + + auth_roles = [ + ApiAuthRoleRef(name=role) for role in target_auth_roles + ] + update_existing_auth_roles = ApiExternalUserMapping( + name=self.name, + uuid=mapping.uuid, + type=self.type, + auth_roles=auth_roles, + ) + if not self.module.check_mode: + self.external_user_mappings_output = ( + api_instance.update_external_user_mapping( + uuid=mapping.uuid, body=update_existing_auth_roles + ) + ).to_dict() + self.changed = True + else: + auth_roles = [ApiAuthRoleRef(name=role) for role in self.auth_roles] + external_user_mappings_body = ApiExternalUserMapping( + name=self.name, + uuid=mapping.uuid, + type=self.type, + auth_roles=auth_roles, + ) + + if not self.module.check_mode: + self.external_user_mappings_output = ( + api_instance.create_external_user_mappings( + body={"items": [external_user_mappings_body]} + ) + ).to_dict()["items"] + self.changed = True + + if self.state == "absent": + if existing: + if self.auth_roles: + existing_auth_roles = set( + auth_role["name"] for auth_role in existing["auth_roles"] + ) + incoming_auth_roles = set(self.auth_roles) + + roles_to_delete = existing_auth_roles.intersection( + incoming_auth_roles + ) + if self.module._diff: + self.diff.update( + before=list(roles_to_delete), + after=[], + ) + if roles_to_delete: + remaining_roles = existing_auth_roles - roles_to_delete + auth_roles = [ + ApiAuthRoleRef(name=role) for role in remaining_roles + ] + update_existing_auth_roles = ApiExternalUserMapping( + name=self.name, + uuid=mapping.uuid, + type=self.type, + auth_roles=auth_roles, + ) + if not self.module.check_mode: + self.external_user_mappings_output = ( + api_instance.update_external_user_mapping( + uuid=mapping.uuid, body=update_existing_auth_roles + ) + ).to_dict() + self.changed = True + else: + if not self.module.check_mode: + self.external_user_mappings_output = ( + api_instance.delete_external_user_mapping(uuid=mapping.uuid) + ).to_dict() + self.changed = True + + +def main(): + module = ClouderaManagerModule.ansible_module( + argument_spec=dict( + name=dict(required=False, type="str"), + uuid=dict(required=False, type="str"), + type=dict(required=False, type="str"), + purge=dict(required=False, type="bool", default=False), + auth_roles=dict(required=False, type="list"), + state=dict( + type="str", + default="present", + choices=["present", "absent"], + ), + ), + supports_check_mode=True, + ) + + result = ClouderaExternalUserMappingsInfo(module) + + output = dict( + changed=result.changed, + external_user_mappings_output=result.external_user_mappings_output, + ) + if module._diff: + output.update(diff=result.diff) + + if result.debug: + log = result.log_capture.getvalue() + output.update(debug=log, debug_lines=log.split("\n")) + + module.exit_json(**output) + + +if __name__ == "__main__": + main() diff --git a/plugins/modules/external_user_mappings_info.py b/plugins/modules/external_user_mappings_info.py new file mode 100644 index 00000000..81dcccd6 --- /dev/null +++ b/plugins/modules/external_user_mappings_info.py @@ -0,0 +1,178 @@ +# Copyright 2024 Cloudera, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from ansible_collections.cloudera.cluster.plugins.module_utils.cm_utils import ( + ClouderaManagerModule, +) + +from cm_client import ( + ExternalUserMappingsResourceApi, +) +from cm_client.rest import ApiException + +ANSIBLE_METADATA = { + "metadata_version": "1.1", + "status": ["preview"], + "supported_by": "community", +} + +DOCUMENTATION = r""" +--- +module: external_user_mappings_info +short_description: Retrieve details of external user mappings +description: + - Retrieve details of a specific or all external user mappings within the Cloudera Manager. +author: + - "Ronald Suplina (@rsuplina)" +requirements: + - cm_client +options: + name: + description: + - The name of the external mapping. + type: str + required: no + uuid: + description: + - The uuid of the external mapping. + type: str + required: no +""" + +EXAMPLES = r""" +--- +- name: Retrieve the defailts about a specific user mapping + cloudera.cluster.external_user_mappings_info: + host: example.cloudera.com + username: "jane_smith" + password: "S&peR4Ec*re" + name: "test_mapping" + +- name: Retrieve the defailts about a specific user mapping with uuid parameter + cloudera.cluster.external_user_mappings_info: + host: example.cloudera.com + username: "jane_smith" + password: "S&peR4Ec*re" + uuid: "1111aa-1111-111a-111a-11111111" + +- name: Retrieve the details about all user mappings + cloudera.cluster.external_user_mappings_info: + host: example.cloudera.com + username: "jane_smith" + password: "S&peR4Ec*re" +""" + +RETURN = r""" +--- +external_user_mappings_info: + description: + - List of external user mappings within the cloudera manager. + type: list + elements: dict + returned: always + contains: + name: + description: + - The name of the external mapping. + type: str + returned: always + type: + description: + - The type of the external mapping. + type: str + returned: always + uuid: + description: + - The UUID of the external mapping. + type: str + returned: always + auth_roles: + description: + - The list of auth roles associated with external user mapping. + type: list + returned: always +""" + + +class ClouderaExternalUserMappingsInfo(ClouderaManagerModule): + def __init__(self, module): + super(ClouderaExternalUserMappingsInfo, self).__init__(module) + + # Set the parameters + self.name = self.get_param("name") + self.uuid = self.get_param("uuid") + + # Initialize the return value + self.external_user_mappings_info_output = [] + self.changed = False + + # Execute the logic + self.process() + + @ClouderaManagerModule.handle_process + def process(self): + api_instance = ExternalUserMappingsResourceApi(self.api_client) + try: + if self.name: + external_user_mappings = api_instance.read_external_user_mappings() + for mapping in external_user_mappings.items: + if self.name == mapping.name: + self.external_user_mappings_info_output = [ + api_instance.read_external_user_mapping( + uuid=mapping.uuid + ).to_dict() + ] + elif self.uuid: + self.external_user_mappings_info_output = [ + api_instance.read_external_user_mapping(uuid=self.uuid).to_dict() + ] + else: + self.external_user_mappings_info_output = ( + api_instance.read_external_user_mappings().to_dict()["items"] + ) + except ApiException as ex: + if ex.status != 400: + raise ex + + +def main(): + module = ClouderaManagerModule.ansible_module( + argument_spec=dict( + name=dict(required=False, type="str"), + uuid=dict(required=False, type="str"), + ), + supports_check_mode=True, + mutually_exclusive=[ + ["name", "uuid"], + ], + ) + + result = ClouderaExternalUserMappingsInfo(module) + + output = dict( + changed=result.changed, + external_user_mappings_info_output=result.external_user_mappings_info_output, + ) + if module._diff: + output.update(diff=result.diff) + + if result.debug: + log = result.log_capture.getvalue() + output.update(debug=log, debug_lines=log.split("\n")) + + module.exit_json(**output) + + +if __name__ == "__main__": + main() diff --git a/tests/unit/plugins/modules/external_user_mappings/test_external_user_mappings.py b/tests/unit/plugins/modules/external_user_mappings/test_external_user_mappings.py new file mode 100644 index 00000000..c033fe17 --- /dev/null +++ b/tests/unit/plugins/modules/external_user_mappings/test_external_user_mappings.py @@ -0,0 +1,128 @@ +# Copyright 2024 Cloudera, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +import logging +import os +import pytest + +from ansible_collections.cloudera.cluster.plugins.modules import external_user_mappings +from ansible_collections.cloudera.cluster.tests.unit import ( + AnsibleExitJson, +) + +LOG = logging.getLogger(__name__) + + +@pytest.fixture +def conn(): + conn = dict(username=os.getenv("CM_USERNAME"), password=os.getenv("CM_PASSWORD")) + + if os.getenv("CM_HOST", None): + conn.update(host=os.getenv("CM_HOST")) + + if os.getenv("CM_PORT", None): + conn.update(port=os.getenv("CM_PORT")) + + if os.getenv("CM_ENDPOINT", None): + conn.update(url=os.getenv("CM_ENDPOINT")) + + if os.getenv("CM_PROXY", None): + conn.update(proxy=os.getenv("CM_PROXY")) + + return { + **conn, + "verify_tls": "no", + "debug": "no", + } + + +def test_create_admin_external_mapping(module_args, conn): + conn.update( + name="admin_mapping", + type="LDAP", + auth_roles=["ROLE_CLUSTER_ADMIN"], + ) + + module_args(conn) + + with pytest.raises(AnsibleExitJson) as e: + external_user_mappings.main() + + LOG.info(str(e.value.external_user_mappings_output)) + +def test_delete_admin_external_mapping(module_args, conn): + conn.update( + name="admin_mapping", + type="LDAP", + state="absent", + ) + + module_args(conn) + + with pytest.raises(AnsibleExitJson) as e: + external_user_mappings.main() + + LOG.info(str(e.value.external_user_mappings_output)) + +def test_create_default_user_external_mapping(module_args, conn): + conn.update( + name="user_mapping", + type="LDAP", + auth_roles=[ "ROLE_DASHBOARD_USER","ROLE_USER"], + + ) + + module_args(conn) + + with pytest.raises(AnsibleExitJson) as e: + external_user_mappings.main() + + LOG.info(str(e.value.external_user_mappings_output)) + + +def test_upgrade_default_user_external_mapping(module_args, conn): + conn.update( + name="user_mapping", + type="LDAP", + auth_roles=[ "ROLE_CLUSTER_ADMIN"], + + ) + + module_args(conn) + + with pytest.raises(AnsibleExitJson) as e: + external_user_mappings.main() + + LOG.info(str(e.value.external_user_mappings_output)) + + +def test_configure_new_auth_roles_on_existing_user(module_args, conn): + conn.update( + name="user_mapping", + type="LDAP", + auth_roles=[ "ROLE_CLUSTER_ADMIN"], + purge=True, + + ) + + module_args(conn) + + with pytest.raises(AnsibleExitJson) as e: + external_user_mappings.main() + + LOG.info(str(e.value.external_user_mappings_output)) diff --git a/tests/unit/plugins/modules/external_user_mappings_info/test_external_user_mappings_info.py b/tests/unit/plugins/modules/external_user_mappings_info/test_external_user_mappings_info.py new file mode 100644 index 00000000..536e70d2 --- /dev/null +++ b/tests/unit/plugins/modules/external_user_mappings_info/test_external_user_mappings_info.py @@ -0,0 +1,88 @@ +# Copyright 2024 Cloudera, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +import logging +import os +import pytest + +from ansible_collections.cloudera.cluster.plugins.modules import external_user_mappings_info +from ansible_collections.cloudera.cluster.tests.unit import ( + AnsibleExitJson, +) + +LOG = logging.getLogger(__name__) + + +@pytest.fixture +def conn(): + conn = dict(username=os.getenv("CM_USERNAME"), password=os.getenv("CM_PASSWORD")) + + if os.getenv("CM_HOST", None): + conn.update(host=os.getenv("CM_HOST")) + + if os.getenv("CM_PORT", None): + conn.update(port=os.getenv("CM_PORT")) + + if os.getenv("CM_ENDPOINT", None): + conn.update(url=os.getenv("CM_ENDPOINT")) + + if os.getenv("CM_PROXY", None): + conn.update(proxy=os.getenv("CM_PROXY")) + + return { + **conn, + "verify_tls": "no", + "debug": "no", + } + + +def test_get_external_user_mappings_with_name(module_args, conn): + conn.update( + name="my_mapping", + ) + + module_args(conn) + + with pytest.raises(AnsibleExitJson) as e: + external_user_mappings_info.main() + + LOG.info(str(e.value.external_user_mappings_info_output)) + + +def test_get_external_user_mappings_with_uuid(module_args, conn): + conn.update( + uuid="11111-11a1-111a-111a-111111aaa", + ) + + module_args(conn) + + with pytest.raises(AnsibleExitJson) as e: + external_user_mappings_info.main() + + LOG.info(str(e.value.external_user_mappings_info_output)) + + +def test_get_all_external_user_mappings(module_args, conn): + conn.update() + + module_args(conn) + + with pytest.raises(AnsibleExitJson) as e: + external_user_mappings_info.main() + + LOG.info(str(e.value.external_user_mappings_info_output))