Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added ldap_search module for searching in LDAP servers #126

Merged
merged 10 commits into from
Apr 17, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions plugins/modules/ldap_search.py
189 changes: 189 additions & 0 deletions plugins/modules/net_tools/ldap/ldap_search.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-

# Copyright: (c) 2016, Peter Sagerson <psagers@ignorare.net>
# Copyright: (c) 2020, Sebastian Pfahl <eryx@gmx.net>
# 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()
6 changes: 6 additions & 0 deletions tests/integration/targets/ldap_search/aliases
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
shippable/posix/group1
skip/aix
felixfontein marked this conversation as resolved.
Show resolved Hide resolved
skip/freebsd
skip/osx
skip/rhel
needs/root
3 changes: 3 additions & 0 deletions tests/integration/targets/ldap_search/meta/main.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
---
dependencies:
- setup_openldap
6 changes: 6 additions & 0 deletions tests/integration/targets/ldap_search/tasks/main.yml
Original file line number Diff line number Diff line change
@@ -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']
Empty file.
20 changes: 20 additions & 0 deletions tests/integration/targets/ldap_search/tasks/tests/basic.yml
Original file line number Diff line number Diff line change
@@ -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"
22 changes: 22 additions & 0 deletions tests/integration/targets/setup_openldap/files/initial_config.ldif
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
dn: olcDatabase={0}config,cn=config
changetype: modify
replace: olcRootPW
olcRootPW: "Test1234!"
1 change: 1 addition & 0 deletions tests/integration/targets/setup_openldap/meta/main.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
---
63 changes: 63 additions & 0 deletions tests/integration/targets/setup_openldap/tasks/main.yml
Original file line number Diff line number Diff line change
@@ -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']
55 changes: 55 additions & 0 deletions tests/integration/targets/setup_openldap/vars/Debian.yml
Original file line number Diff line number Diff line change
@@ -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"
Loading