1#!/usr/local/bin/python3.8
2# -*- coding: utf-8 -*-
3
4# Copyright: (c) 2016, Peter Sagerson <psagers@ignorare.net>
5# Copyright: (c) 2016, Jiri Tyr <jiri.tyr@gmail.com>
6#
7# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
8
9from __future__ import absolute_import, division, print_function
10__metaclass__ = type
11
12
13DOCUMENTATION = '''
14---
15module: ldap_entry
16short_description: Add or remove LDAP entries.
17description:
18  - Add or remove LDAP entries. This module only asserts the existence or
19    non-existence of an LDAP entry, not its attributes. To assert the
20    attribute values of an entry, see M(community.general.ldap_attrs).
21notes:
22  - The default authentication settings will attempt to use a SASL EXTERNAL
23    bind over a UNIX domain socket. This works well with the default Ubuntu
24    install for example, which includes a cn=peercred,cn=external,cn=auth ACL
25    rule allowing root to modify the server configuration. If you need to use
26    a simple bind to access your server, pass the credentials in I(bind_dn)
27    and I(bind_pw).
28author:
29  - Jiri Tyr (@jtyr)
30requirements:
31  - python-ldap
32options:
33  attributes:
34    description:
35      - If I(state=present), attributes necessary to create an entry. Existing
36        entries are never modified. To assert specific attribute values on an
37        existing entry, use M(community.general.ldap_attrs) module instead.
38    type: dict
39  objectClass:
40    description:
41      - If I(state=present), value or list of values to use when creating
42        the entry. It can either be a string or an actual list of
43        strings.
44    type: list
45    elements: str
46  state:
47    description:
48      - The target state of the entry.
49    choices: [present, absent]
50    default: present
51    type: str
52extends_documentation_fragment:
53- community.general.ldap.documentation
54
55'''
56
57
58EXAMPLES = """
59- name: Make sure we have a parent entry for users
60  community.general.ldap_entry:
61    dn: ou=users,dc=example,dc=com
62    objectClass: organizationalUnit
63
64- name: Make sure we have an admin user
65  community.general.ldap_entry:
66    dn: cn=admin,dc=example,dc=com
67    objectClass:
68      - simpleSecurityObject
69      - organizationalRole
70    attributes:
71      description: An LDAP administrator
72      userPassword: "{SSHA}tabyipcHzhwESzRaGA7oQ/SDoBZQOGND"
73
74- name: Get rid of an old entry
75  community.general.ldap_entry:
76    dn: ou=stuff,dc=example,dc=com
77    state: absent
78    server_uri: ldap://localhost/
79    bind_dn: cn=admin,dc=example,dc=com
80    bind_pw: password
81
82#
83# The same as in the previous example but with the authentication details
84# stored in the ldap_auth variable:
85#
86# ldap_auth:
87#   server_uri: ldap://localhost/
88#   bind_dn: cn=admin,dc=example,dc=com
89#   bind_pw: password
90#
91# In the example below, 'args' is a task keyword, passed at the same level as the module
92- name: Get rid of an old entry
93  community.general.ldap_entry:
94    dn: ou=stuff,dc=example,dc=com
95    state: absent
96  args: "{{ ldap_auth }}"
97"""
98
99
100RETURN = """
101# Default return values
102"""
103
104import traceback
105
106from ansible.module_utils.basic import AnsibleModule, missing_required_lib
107from ansible.module_utils.common.text.converters import to_native, to_bytes
108from ansible_collections.community.general.plugins.module_utils.ldap import LdapGeneric, gen_specs
109
110LDAP_IMP_ERR = None
111try:
112    import ldap.modlist
113
114    HAS_LDAP = True
115except ImportError:
116    LDAP_IMP_ERR = traceback.format_exc()
117    HAS_LDAP = False
118
119
120class LdapEntry(LdapGeneric):
121    def __init__(self, module):
122        LdapGeneric.__init__(self, module)
123
124        # Shortcuts
125        self.state = self.module.params['state']
126
127        # Add the objectClass into the list of attributes
128        self.module.params['attributes']['objectClass'] = (
129            self.module.params['objectClass'])
130
131        # Load attributes
132        if self.state == 'present':
133            self.attrs = self._load_attrs()
134
135    def _load_attrs(self):
136        """ Turn attribute's value to array. """
137        attrs = {}
138
139        for name, value in self.module.params['attributes'].items():
140            if isinstance(value, list):
141                attrs[name] = list(map(to_bytes, value))
142            else:
143                attrs[name] = [to_bytes(value)]
144
145        return attrs
146
147    def add(self):
148        """ If self.dn does not exist, returns a callable that will add it. """
149        def _add():
150            self.connection.add_s(self.dn, modlist)
151
152        if not self._is_entry_present():
153            modlist = ldap.modlist.addModlist(self.attrs)
154            action = _add
155        else:
156            action = None
157
158        return action
159
160    def delete(self):
161        """ If self.dn exists, returns a callable that will delete it. """
162        def _delete():
163            self.connection.delete_s(self.dn)
164
165        if self._is_entry_present():
166            action = _delete
167        else:
168            action = None
169
170        return action
171
172    def _is_entry_present(self):
173        try:
174            self.connection.search_s(self.dn, ldap.SCOPE_BASE)
175        except ldap.NO_SUCH_OBJECT:
176            is_present = False
177        else:
178            is_present = True
179
180        return is_present
181
182
183def main():
184    module = AnsibleModule(
185        argument_spec=gen_specs(
186            attributes=dict(default={}, type='dict'),
187            objectClass=dict(type='list', elements='str'),
188            state=dict(default='present', choices=['present', 'absent']),
189        ),
190        required_if=[('state', 'present', ['objectClass'])],
191        supports_check_mode=True,
192    )
193
194    if not HAS_LDAP:
195        module.fail_json(msg=missing_required_lib('python-ldap'),
196                         exception=LDAP_IMP_ERR)
197
198    state = module.params['state']
199
200    # Instantiate the LdapEntry object
201    ldap = LdapEntry(module)
202
203    # Get the action function
204    if state == 'present':
205        action = ldap.add()
206    elif state == 'absent':
207        action = ldap.delete()
208
209    # Perform the action
210    if action is not None and not module.check_mode:
211        try:
212            action()
213        except Exception as e:
214            module.fail_json(msg="Entry action failed.", details=to_native(e), exception=traceback.format_exc())
215
216    module.exit_json(changed=(action is not None))
217
218
219if __name__ == '__main__':
220    main()
221