From e3e6c6167e0de123b44cbe6833423333571b80e6 Mon Sep 17 00:00:00 2001 From: eryx12o45 Date: Fri, 17 Apr 2020 10:53:37 +0200 Subject: [PATCH] Added ldap_search module for searching in LDAP servers (#126) * fix CI * Added ldap_search module for searching in LDAP servers * Fixes from pipeline * Fixed second script as well * fix DOCUMENTATION block * fix DOCUMENTATION block * fix DOCUMENTATION block * fix examples and remove changelog fragment * Added integration tests for ldap_search * fixes Co-authored-by: Sebastian Pfahl --- plugins/modules/ldap_search.py | 1 + plugins/modules/net_tools/ldap/ldap_search.py | 189 ++++++++++++++++++ tests/integration/targets/ldap_search/aliases | 6 + .../targets/ldap_search/meta/main.yml | 3 + .../targets/ldap_search/tasks/main.yml | 6 + .../targets/ldap_search/tasks/run-test.yml | 0 .../targets/ldap_search/tasks/tests/basic.yml | 20 ++ .../setup_openldap/files/initial_config.ldif | 22 ++ .../setup_openldap/files/rootpw_cnconfig.ldif | 4 + .../targets/setup_openldap/meta/main.yml | 1 + .../targets/setup_openldap/tasks/main.yml | 63 ++++++ .../targets/setup_openldap/vars/Debian.yml | 55 +++++ .../targets/setup_openldap/vars/Ubuntu.yml | 55 +++++ 13 files changed, 425 insertions(+) create mode 120000 plugins/modules/ldap_search.py create mode 100644 plugins/modules/net_tools/ldap/ldap_search.py create mode 100644 tests/integration/targets/ldap_search/aliases create mode 100644 tests/integration/targets/ldap_search/meta/main.yml create mode 100644 tests/integration/targets/ldap_search/tasks/main.yml create mode 100644 tests/integration/targets/ldap_search/tasks/run-test.yml create mode 100644 tests/integration/targets/ldap_search/tasks/tests/basic.yml create mode 100644 tests/integration/targets/setup_openldap/files/initial_config.ldif create mode 100644 tests/integration/targets/setup_openldap/files/rootpw_cnconfig.ldif create mode 100644 tests/integration/targets/setup_openldap/meta/main.yml create mode 100644 tests/integration/targets/setup_openldap/tasks/main.yml create mode 100644 tests/integration/targets/setup_openldap/vars/Debian.yml create mode 100644 tests/integration/targets/setup_openldap/vars/Ubuntu.yml diff --git a/plugins/modules/ldap_search.py b/plugins/modules/ldap_search.py new file mode 120000 index 0000000000..a21c103342 --- /dev/null +++ b/plugins/modules/ldap_search.py @@ -0,0 +1 @@ +./net_tools/ldap/ldap_search.py \ No newline at end of file diff --git a/plugins/modules/net_tools/ldap/ldap_search.py b/plugins/modules/net_tools/ldap/ldap_search.py new file mode 100644 index 0000000000..0332c9f5a9 --- /dev/null +++ b/plugins/modules/net_tools/ldap/ldap_search.py @@ -0,0 +1,189 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2016, Peter Sagerson +# Copyright: (c) 2020, Sebastian Pfahl +# 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 + +ANSIBLE_METADATA = { + 'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community' +} + +DOCUMENTATION = r""" +--- +module: ldap_search +short_description: Search for entries in a LDAP server +description: + - Return the results of an LDAP search. +notes: + - The default authentication settings will attempt to use a SASL EXTERNAL + bind over a UNIX domain socket. This works well with the default Ubuntu + install for example, which includes a C(cn=peercred,cn=external,cn=auth) ACL + rule allowing root to modify the server configuration. If you need to use + a simple bind to access your server, pass the credentials in I(bind_dn) + and I(bind_pw). +author: + - Sebastian Pfahl (@eryx12o45) +requirements: + - python-ldap +options: + dn: + required: true + type: str + description: + - The LDAP DN to search in. + scope: + choices: [base, onelevel, subordinate, children] + default: base + type: str + description: + - The LDAP scope to use. + filter: + default: '(objectClass=*)' + type: str + description: + - Used for filtering the LDAP search result. + attrs: + type: list + elements: str + description: + - A list of attributes for limiting the result. Use an + actual list or a comma-separated string. + schema: + default: false + type: bool + description: + - Set to C(true) to return the full attribute schema of entries, not + their attribute values. Overrides I(attrs) when provided. +extends_documentation_fragment: + - community.general.ldap.documentation +""" + +EXAMPLES = r""" +- name: Return all entries within the 'groups' organizational unit. + community.general.ldap_search: + dn: "ou=groups,dc=example,dc=com" + register: ldap_groups + +- name: Return GIDs for all groups + community.general.ldap_search: + dn: "ou=groups,dc=example,dc=com" + scope: "onelevel" + attrs: + - "gidNumber" + register: ldap_group_gids +""" + +import traceback + +from ansible.module_utils.basic import AnsibleModule, missing_required_lib +from ansible.module_utils._text import to_native +from ansible_collections.community.general.plugins.module_utils.ldap import LdapGeneric, gen_specs + +LDAP_IMP_ERR = None +try: + import ldap + + HAS_LDAP = True +except ImportError: + LDAP_IMP_ERR = traceback.format_exc() + HAS_LDAP = False + + +def main(): + module = AnsibleModule( + argument_spec=gen_specs( + dn=dict(type='str', required=True), + scope=dict(type='str', default='base', choices=['base', 'onelevel', 'subordinate', 'children']), + filter=dict(type='str', default='(objectClass=*)'), + attrs=dict(type='list', elements='str'), + schema=dict(type='bool', default=False), + ), + supports_check_mode=True, + ) + + if not HAS_LDAP: + module.fail_json(msg=missing_required_lib('python-ldap'), + exception=LDAP_IMP_ERR) + + if not module.check_mode: + try: + LdapSearch(module).main() + except Exception as exception: + module.fail_json(msg="Attribute action failed.", details=to_native(exception)) + + module.exit_json(changed=True) + + +def _extract_entry(dn, attrs): + extracted = {'dn': dn} + for attr, val in list(attrs.items()): + if len(val) == 1: + extracted[attr] = val[0] + else: + extracted[attr] = val + return extracted + + +class LdapSearch(LdapGeneric): + def __init__(self, module): + LdapGeneric.__init__(self, module) + + self.dn = self.module.params['dn'] + self.filterstr = self.module.params['filter'] + self.attrlist = [] + self._load_scope() + self._load_attrs() + self._load_schema() + + def _load_schema(self): + self.schema = self.module.boolean(self.module.params['schema']) + if self.schema: + self.attrsonly = 1 + else: + self.attrsonly = 0 + + def _load_scope(self): + scope = self.module.params['scope'] + if scope == 'base': + self.scope = ldap.SCOPE_BASE + elif scope == 'onelevel': + self.scope = ldap.SCOPE_ONELEVEL + elif scope == 'subordinate': + self.scope = ldap.SCOPE_SUBORDINATE + elif scope == 'children': + self.scope = ldap.SCOPE_SUBTREE + else: + raise AssertionError('Implementation error') + + def _load_attrs(self): + self.attrlist = self.module.params['attrs'] or None + + def main(self): + results = self.perform_search() + self.module.exit_json(changed=True, results=results) + + def perform_search(self): + try: + results = self.connection.search_s( + self.dn, + self.scope, + filterstr=self.filterstr, + attrlist=self.attrlist, + attrsonly=self.attrsonly + ) + if self.schema: + return [dict(dn=result[0], attrs=list(result[1].keys())) for result in results] + else: + return [_extract_entry(result[0], result[1]) for result in results] + except ldap.NO_SUCH_OBJECT: + self.module.fail_json(msg="Base not found: {0}".format(self.dn)) + + +if __name__ == '__main__': + main() diff --git a/tests/integration/targets/ldap_search/aliases b/tests/integration/targets/ldap_search/aliases new file mode 100644 index 0000000000..e75f63b977 --- /dev/null +++ b/tests/integration/targets/ldap_search/aliases @@ -0,0 +1,6 @@ +shippable/posix/group1 +skip/aix +skip/freebsd +skip/osx +skip/rhel +needs/root diff --git a/tests/integration/targets/ldap_search/meta/main.yml b/tests/integration/targets/ldap_search/meta/main.yml new file mode 100644 index 0000000000..093fafe4a2 --- /dev/null +++ b/tests/integration/targets/ldap_search/meta/main.yml @@ -0,0 +1,3 @@ +--- +dependencies: + - setup_openldap diff --git a/tests/integration/targets/ldap_search/tasks/main.yml b/tests/integration/targets/ldap_search/tasks/main.yml new file mode 100644 index 0000000000..3b55cd0d13 --- /dev/null +++ b/tests/integration/targets/ldap_search/tasks/main.yml @@ -0,0 +1,6 @@ +- name: Run LDAP search module tests + block: + - include_tasks: "{{ item }}" + with_fileglob: + - 'tests/*.yml' + when: ansible_os_family in ['Ubuntu', 'Debian'] \ No newline at end of file diff --git a/tests/integration/targets/ldap_search/tasks/run-test.yml b/tests/integration/targets/ldap_search/tasks/run-test.yml new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/integration/targets/ldap_search/tasks/tests/basic.yml b/tests/integration/targets/ldap_search/tasks/tests/basic.yml new file mode 100644 index 0000000000..824be4aa78 --- /dev/null +++ b/tests/integration/targets/ldap_search/tasks/tests/basic.yml @@ -0,0 +1,20 @@ +- debug: + msg: Running tests/basic.yml + +#################################################################### +## Search ########################################################## +#################################################################### +- name: Test simple search for a user + ldap_search: + dn: "ou=users,dc=example,dc=com" + scope: "onelevel" + filter: "(uid=ldaptest)" + ignore_errors: yes + register: output + +- name: assert that test LDAP user can be found + assert: + that: + - output is not failed + - output.results | length == 1 + - output.results.0.displayName == "LDAP Test" diff --git a/tests/integration/targets/setup_openldap/files/initial_config.ldif b/tests/integration/targets/setup_openldap/files/initial_config.ldif new file mode 100644 index 0000000000..13397758f3 --- /dev/null +++ b/tests/integration/targets/setup_openldap/files/initial_config.ldif @@ -0,0 +1,22 @@ +dn: ou=users,dc=example,dc=com +objectClass: organizationalUnit +objectClass: top +ou: users + +dn: uid=ldaptest,ou=users,dc=example,dc=com +uid: ldaptest +uidNumber: 1111 +gidNUmber: 100 +objectClass: top +objectClass: posixAccount +objectClass: shadowAccount +objectClass: person +objectClass: organizationalPerson +objectClass: inetOrgPerson +loginShell: /bin/sh +homeDirectory: /home/ldaptest +cn: LDAP Test +gecos: LDAP Test +displayName: LDAP Test +mail: ldap.test@example.com +sn: Test \ No newline at end of file diff --git a/tests/integration/targets/setup_openldap/files/rootpw_cnconfig.ldif b/tests/integration/targets/setup_openldap/files/rootpw_cnconfig.ldif new file mode 100644 index 0000000000..1fc061dde8 --- /dev/null +++ b/tests/integration/targets/setup_openldap/files/rootpw_cnconfig.ldif @@ -0,0 +1,4 @@ +dn: olcDatabase={0}config,cn=config +changetype: modify +replace: olcRootPW +olcRootPW: "Test1234!" \ No newline at end of file diff --git a/tests/integration/targets/setup_openldap/meta/main.yml b/tests/integration/targets/setup_openldap/meta/main.yml new file mode 100644 index 0000000000..ed97d539c0 --- /dev/null +++ b/tests/integration/targets/setup_openldap/meta/main.yml @@ -0,0 +1 @@ +--- diff --git a/tests/integration/targets/setup_openldap/tasks/main.yml b/tests/integration/targets/setup_openldap/tasks/main.yml new file mode 100644 index 0000000000..58cc5769b1 --- /dev/null +++ b/tests/integration/targets/setup_openldap/tasks/main.yml @@ -0,0 +1,63 @@ +--- +- name: Setup OpenLDAP on Debian or Ubuntu + block: + - name: Include OS-specific variables + include_vars: '{{ ansible_os_family }}.yml' + + - name: Install OpenLDAP server and tools + become: True + package: + name: '{{ item }}' + loop: '{{ openldap_packages_name }}' + + - name: Install python-ldap (Python 3) + become: True + package: + name: '{{ python_ldap_package_name_python3 }}' + when: ansible_python_version is version('3.0', '>=') + + - name: Install python-ldap (Python 2) + become: True + package: + name: '{{ python_ldap_package_name }}' + when: ansible_python_version is version('3.0', '<') + + - name: Make sure OpenLDAP service is stopped + become: True + shell: 'cat /var/run/slapd/slapd.pid | xargs kill -9 ' + + - name: Debconf + shell: 'echo "slapd {{ item.question }} {{ item.vtype }} {{ item.value }}" >> /root/debconf-slapd.conf' + loop: "{{ openldap_debconfs }}" + + - name: Dpkg reconfigure + shell: + cmd: "export DEBIAN_FRONTEND=noninteractive; cat /root/debconf-slapd.conf | debconf-set-selections; dpkg-reconfigure -f noninteractive slapd" + creates: "/root/slapd_configured" + + - name: Start OpenLDAP service + become: True + service: + name: '{{ openldap_service_name }}' + enabled: True + state: started + + - name: Copy initial config ldif file + become: True + copy: + src: 'files/{{ item }}' + dest: '/tmp/{{ item }}' + owner: root + group: root + mode: '0644' + loop: + - rootpw_cnconfig.ldif + - initial_config.ldif + + - name: Configure admin password for cn=config + shell: "ldapmodify -Y EXTERNAL -H ldapi:/// -f /tmp/rootpw_cnconfig.ldif" + + - name: Add initial config + become: True + shell: 'ldapadd -H ldapi:/// -x -D "cn=admin,dc=example,dc=com" -w Test1234! -f /tmp/initial_config.ldif' + when: ansible_os_family in ['Ubuntu', 'Debian'] diff --git a/tests/integration/targets/setup_openldap/vars/Debian.yml b/tests/integration/targets/setup_openldap/vars/Debian.yml new file mode 100644 index 0000000000..bcc4feb9ed --- /dev/null +++ b/tests/integration/targets/setup_openldap/vars/Debian.yml @@ -0,0 +1,55 @@ +python_ldap_package_name: python-ldap +python_ldap_package_name_python3: python3-ldap +openldap_packages_name: + - slapd + - ldap-utils +openldap_service_name: slapd +openldap_debconfs: + - question: "shared/organization" + value: "Example Organization" + vtype: "string" + - question: "slapd/allow_ldap_v2" + value: "false" + vtype: "boolean" + - question: "slapd/backend" + value: "MDB" + vtype: "select" + - question: "slapd/domain" + value: "example.com" + vtype: "string" + - question: "slapd/dump_database" + value: "when needed" + vtype: "select" + - question: "slapd/dump_database_destdir" + value: "/var/backups/slapd-VERSION" + vtype: "string" + - question: "slapd/internal/adminpw" + value: "Test1234!" + vtype: "password" + - question: "slapd/internal/generated_adminpw" + value: "Test1234!" + vtype: "password" + - question: "slapd/invalid_config" + value: "true" + vtype: "boolean" + - question: "slapd/move_old_database" + value: "true" + vtype: "boolean" + - question: "slapd/no_configuration" + value: "false" + vtype: "boolean" + - question: "slapd/password1" + value: "Test1234!" + vtype: "password" + - question: "slapd/password2" + value: "Test1234!" + vtype: "password" + - question: "slapd/password_mismatch" + value: "" + vtype: "note" + - question: "slapd/purge_database" + value: "false" + vtype: "boolean" + - question: "slapd/upgrade_slapcat_failure" + value: "" + vtype: "error" diff --git a/tests/integration/targets/setup_openldap/vars/Ubuntu.yml b/tests/integration/targets/setup_openldap/vars/Ubuntu.yml new file mode 100644 index 0000000000..bcc4feb9ed --- /dev/null +++ b/tests/integration/targets/setup_openldap/vars/Ubuntu.yml @@ -0,0 +1,55 @@ +python_ldap_package_name: python-ldap +python_ldap_package_name_python3: python3-ldap +openldap_packages_name: + - slapd + - ldap-utils +openldap_service_name: slapd +openldap_debconfs: + - question: "shared/organization" + value: "Example Organization" + vtype: "string" + - question: "slapd/allow_ldap_v2" + value: "false" + vtype: "boolean" + - question: "slapd/backend" + value: "MDB" + vtype: "select" + - question: "slapd/domain" + value: "example.com" + vtype: "string" + - question: "slapd/dump_database" + value: "when needed" + vtype: "select" + - question: "slapd/dump_database_destdir" + value: "/var/backups/slapd-VERSION" + vtype: "string" + - question: "slapd/internal/adminpw" + value: "Test1234!" + vtype: "password" + - question: "slapd/internal/generated_adminpw" + value: "Test1234!" + vtype: "password" + - question: "slapd/invalid_config" + value: "true" + vtype: "boolean" + - question: "slapd/move_old_database" + value: "true" + vtype: "boolean" + - question: "slapd/no_configuration" + value: "false" + vtype: "boolean" + - question: "slapd/password1" + value: "Test1234!" + vtype: "password" + - question: "slapd/password2" + value: "Test1234!" + vtype: "password" + - question: "slapd/password_mismatch" + value: "" + vtype: "note" + - question: "slapd/purge_database" + value: "false" + vtype: "boolean" + - question: "slapd/upgrade_slapcat_failure" + value: "" + vtype: "error"