diff --git a/docs/rolereference.rst b/docs/rolereference.rst index f4d3912e1371feb0054ad56113422ff47d9d6fda..0c75f68ae2ff51db415f272ac050a80b11cfc9b5 100644 --- a/docs/rolereference.rst +++ b/docs/rolereference.rst @@ -647,8 +647,10 @@ Parameters LDAP DN entry. **state** (string, optional, ``present``) - Whether the entry should be present or not. Value can be anything supported - by the ``ldap_entry`` module. + Whether the entry should be present or not. Value can be anything + supported by the ``ldap_entry`` module. Keep in mind that state + ``present`` will not update the attributes and their values if the + entry is already present. **attributes** (dictionary, mandatory) Dictionary describing remaining attributes (except ``dn``). The keys in this diff --git a/docs/usage.rst b/docs/usage.rst index cb1b89768ea7b5e34d4b05a01121b1f74f713624..f22ea95e051752b4555607a9c2303a9638685f34 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -766,20 +766,6 @@ role. userPassword: janedoe mail: jane.doe@example.com - # Now, let's add the two users to the mail group. Observe that we use - # the "state: append" option. This is a bit of a cheat since the - # ldap_entries option passes the provided entries directly to the - # m_ldap_entry module (this is a custom module, not to be confused with - # the official ldap_entry module). "state: append" will make sure we - # don't overwrite the group, and instead add the attributes to it (in - # this case we add the two users from above). - - dn: cn=mail,ou=groups,dc=example,dc=com - state: append - attributes: - uniqueMember: - - uid=johndoe,ou=people,dc=example,dc=com - - uid=janedoe,ou=people,dc=example,dc=com - # Let's register our domain in LDAP directory. - dn: dc=example.com,ou=domains,ou=mail,ou=services,dc=example,dc=com attributes: @@ -795,7 +781,14 @@ role. cn: postmaster@example.com rfc822MailMember: john.doe@example.com -5. Once again, before we apply the configuration, we must make sure the +5. Let's add the two users to the mail group (otherwise, the mail + server will ignore them). We'll use the ``ldap_attr`` module + directly to make our life a bit easier:: + + workon mysite && ansible --become -m ldap_attr -a "dn=cn=mail,ou=groups,dc=example,dc=com state=present name=uniqueMember value=uid=johndoe,ou=people,dc=example,dc=com" communications + workon mysite && ansible --become -m ldap_attr -a "dn=cn=mail,ou=groups,dc=example,dc=com state=present name=uniqueMember value=uid=janedoe,ou=people,dc=example,dc=com" communications + +6. Once again, before we apply the configuration, we must make sure the necessary TLS private keys and certificates are available. In this particular case, we need to set-up separate key/certificate pair for both the SMTP and IMAP service: @@ -833,11 +826,11 @@ role. certtool --sec-param normal --generate-privkey --outfile ~/mysite/tls/comms.example.com_imap.key certtool --generate-certificate --load-ca-privkey ~/mysite/tls/ca.key --load-ca-certificate ~/mysite/tls/ca.pem --template ~/mysite/tls/comms.example.com_imap.cfg --load-privkey ~/mysite/tls/comms.example.com_imap.key --outfile ~/mysite/tls/comms.example.com_imap.pem -6. Configuration and TLS keys have ben set-up, so it is time to apply the changes:: +7. Configuration and TLS keys have ben set-up, so it is time to apply the changes:: workon mysite && ansible-playbook playbooks/site.yml -7. If no errors have been reported, at this point you should have two mail +8. If no errors have been reported, at this point you should have two mail accounts - ``john.doe@example.com``, with password ``johndoe``, and ``jane.doe@example.com``, with password ``janedoe``. In this particular set-up, the mail addresses are used as usernames. If you want to test it out, @@ -1030,25 +1023,10 @@ role. Same warning applies here as for mail server role for managing the user/group entries! Scroll up and re-read it if you missed it! - :file:`~/mysite/group_vars/communications.yml` :: - # Don't replace the entire ldap_entries, just append the new group - # modification. - ldap_entries: - # Add the two users to the xmpp group. Observe that we use - # the "state: append" option. This is a bit of a cheat since the - # ldap_entries option passes the provided entries directly to the - # m_ldap_entry module (this is a custom module, not to be confused - # with the official ldap_entry module). "state: append" will make sure - # we don't overwrite the group, and instead add the attributes to it - # (in this case we add the two users). - - dn: cn=xmpp,ou=groups,dc=example,dc=com - state: append - attributes: - uniqueMember: - - uid=johndoe,ou=people,dc=example,dc=com - - uid=janedoe,ou=people,dc=example,dc=com + workon mysite && ansible --become -m ldap_attr -a "dn=cn=xmpp,ou=groups,dc=example,dc=com state=present name=uniqueMember value=uid=johndoe,ou=people,dc=example,dc=com" communications + workon mysite && ansible --become -m ldap_attr -a "dn=cn=xmpp,ou=groups,dc=example,dc=com state=present name=uniqueMember value=uid=janedoe,ou=people,dc=example,dc=com" communications 5. Do you know what comes next? Yes! Create some more TLS private keys and certificates, this time for our XMPP server ;) diff --git a/roles/ldap_server/library/m_ldap_entry.py b/roles/ldap_server/library/m_ldap_entry.py deleted file mode 100644 index d1679d037a463a1f88a8f28648098468c86b0fe2..0000000000000000000000000000000000000000 --- a/roles/ldap_server/library/m_ldap_entry.py +++ /dev/null @@ -1,419 +0,0 @@ -#!/usr/bin/env python - -from ansible.module_utils.basic import AnsibleModule - -# Try to load the Python LDAP module. -try: - import ldap - import ldap.sasl - import ldap.modlist -except ImportError: - ldap_found = False -else: - ldap_found = True - - -DOCUMENTATION = """ ---- -module: m_ldap_entry -short_description: Creates, updates, or removes an LDAP entry. -description: - - Creates, updates, or removes an LDAP entry in an LDAP directory. -version_added: 1.8.2 -author: Branko Majic -notes: - - Requires the python-ldap Python package on remote host. For Debian and - derivatives, this is as easy as apt-get install python-ldap. -requirements: - - python-ldap -options: - dn: - description: - - Distinguished name of the entry. - required: true - default: "" - state: - description: - - LDAP entry state. State C(present) requires that all entry attributes - are listed. If you wish to append attributes to existing entry (or - create a new one if it does not exist) use state C(append). If - you wish to replace existing values for an attribute or create a new - entry if it does not exist, use C(replace). - required: true - default: "present" - choices: [ "present", "absent", "append", "replace" ] - server_uri: - description: - - LDAP connection URI specifying what server to connect to. - required: false - default: "ldapi:///" - bind_dn: - description: - - DN for binding to the LDAP server using simple bind. If not set, - EXTERNAL SASL binding method will be used. - required: false - default: "" - bind_password: - description: - - Password for binding to the LDAP server using simple bind. - required: false - default: "" - - attributes: - description: - - Dictionary defining attributes used for the LDAP entry. This is an - alternative way to provide entry attributes, and can be used alone or in - conjunction with method described for "UNLISTED_OPTIONS" (see below). - - OTHER_OPTIONS: - description: - - All remaining options are considered to be attributes for an LDAP - entry. LDAP schema constraints should be kept in mind (i.e. one - structural objectClass etc). Attributes can be passed in as a simple - string (for one value of an attribute), for storing multiple values for - same attribute. If providing a base64-encoded value, prefix it with - C(base64:) (this is useful for I(usercertificate;binary) or - I(displayName) attributes). In order to remove an attribute, set its - value to an empty string (C("")), and set the state to - C(replace). - required: false - default: "" -""" - -EXAMPLES = """ -# Create sub-trees for storing user and group information. -m_ldap_entry: - dn: ou=people,dc=example,dc=com - attributes: - objectClass: organizationalUnit - ou: people - -m_ldap_entry: - dn: ou=groups,dc=example,dc=com - attributes: - objectClass: organizationalUnit - ou: groups - -# Remove old entries, using simple bind authentication. -m_ldap_entry: - dn: ou=accounting,dc=example,dc=com - state: absent - bind_dn: cn=admin,dc=example,dc=com - bind_password: foo123 - -# Create a complex entry that has multiple values for single attribute. -m_ldap_entry: - dn: uid=john,ou=people,dc=example,dc=com - attributes: - objectClass: - - inetOrgPerson - - simpleSecurityObject - uid: john - cn: John Doe - sn: Doe - givenName: John - displayName: base64:Sm9obiBEb2U= - initials: JD - mail: john.doe@example.com - mobile: +1 11 111 111 11 - usercertificate;binary: base64:MIIC...lotsofcharacters...+/A== - -# Add attribute to an entry. -m_ldap_entry: - dn: uid=john,ou=people,dc=example,dc=com - state: append - attributes: - mail: john.doe@example.com - -# Make sure the configuration database has specific logging level enabled. -m_ldap_entry: - dn: cn=config - state: replace - attributes: - olcLogLevel: 256 - -# Remove attribute from an entry. -m_ldap_entry: - dn: uid=john,ou=people,dc=example,dc=com - state: replace - attributes: - uid: "" -""" - - -def get_ldap_connection(uri, bind_dn=None, bind_password=""): - """ - Connects and binds to an LDAP server. - - Arguments: - - uri - LDAP connection URI specifying what server to connect to, including the - protocol. - - bind_dn - Distinguished name to be used for simple bind. If not set, SASL EXTERNAL - mechanism will be used for log-in. Default is None. - - bind_password - Password to be used for simple bind. Needs to be set only if bind_dn is - set as well. Default is "". - - Returns: - - LDAP connection object. - """ - - connection = ldap.initialize(uri) - - if bind_dn: - connection.simple_bind_s(bind_dn, bind_password) - else: - connection.sasl_interactive_bind_s("", ldap.sasl.external()) - - return connection - - -class LDAPEntry(object): - """ - Implements a convenience wrapper for managing an LDAP entry. - """ - - def __init__(self, dn, attributes, connection): - """ - Initialises class instance, setting-up the necessary properties. - - Arguments: - - dn - Distinguished name (DN) of an entry. - - attributes - Attributes that should be set for an entry. - - connection - An instance of LDAPObject class that will be used for running queries - against an LDAP server. - """ - - self.connection = connection - self.dn = dn - self.attributes = attributes - - def add(self): - """ - Adds entry to the LDAP directory. - - Returns: - - True, if entry was added, or had to be updated to match with requested - attributes. False, if no change was necessary. - """ - - # If entry already exists with set attributes, only update it. - if self.exists(): - return self._update() - - # Otherwise we need to add a new entry. - self.connection.add_s(self.dn, ldap.modlist.addModlist(self.attributes)) - - return True - - def remove(self): - """ - Removes entry from an LDAP directory. - - Returns: - - True, if entry was removed. False if no change was necessary (entry is - already not present). - """ - - if self.exists(): - self.connection.delete_s(self.dn) - - return True - - return False - - def append(self): - """ - Append attributes to an existing entry. If the entry does not exist, - create it. - - Returns: - - True, if entry was updated with new attribute values or if a new entry - has been created. False if no change was necessary (values are already - present). - """ - - if not self.exists(): - return self.add() - - attribute_list = self.attributes.keys() - - current_attributes = self.connection.search_s(self.dn, ldap.SCOPE_BASE, attrlist=attribute_list)[0][1] - - # This dictionary will contain all new attributes (or attribute values) - # that should be added to the entry. We can't rely on modifyModlist - # unfortunately, since if the values already exists, it will try to - # remove and re-add them. - new_attributes = {} - - # If attribute is already present, only add the difference between - # requested and current values. - for attribute, values in current_attributes.iteritems(): - if attribute in self.attributes: - new_attributes[attribute] = [item for item in self.attributes[attribute] if item not in values] - else: - new_attributes[attribute] = values - - modification_list = ldap.modlist.modifyModlist({}, new_attributes) - - if not modification_list: - return False - - self.connection.modify_s(self.dn, modification_list) - - return True - - def replace(self): - """ - Replace attributes of an existing entry. If the entry does not exist, - create it. - - Returns: - - True, if entry was updated with new attribute values or if a new entry - has been created. False if no change was necessary (values are already - present). - """ - - if not self.exists(): - return self.add() - - attribute_list = self.attributes.keys() - - current_attributes = self.connection.search_s(self.dn, ldap.SCOPE_BASE, attrlist=attribute_list)[0][1] - - modification_list = ldap.modlist.modifyModlist(current_attributes, - self.attributes, ignore_oldexistent=1) - - if not modification_list: - return False - - self.connection.modify_s(self.dn, modification_list) - - return True - - def _update(self): - """ - Updates an LDAP entry to have the requested attributes. - - Returns: - - True, if LDAP entry was updated. False if no change was necessary (entry - already has the correct attributes). - """ - - self.current_attributes = self.connection.search_s(self.dn, ldap.SCOPE_BASE)[0][1] - - modification_list = ldap.modlist.modifyModlist(self.current_attributes, - self.attributes) - - if not modification_list: - return False - - self.connection.modify_s(self.dn, modification_list) - - return True - - def exists(self): - """ - Checks if the entry already exists in LDAP directory or not. - - Returns: - True, if entry exists. False otherwise. - """ - - try: - self.connection.search_s(self.dn, ldap.SCOPE_BASE, attrlist=["dn"]) - except ldap.NO_SUCH_OBJECT: - return False - - return True - - -def main(): - """ - Runs the module. - """ - - # Construct the module helper for parsing the arguments. - module = AnsibleModule( - argument_spec=dict( - dn=dict(required=True), - state=dict(required=False, choices=["present", "absent", "append", "replace"], default="present"), - server_uri=dict(required=False, default="ldapi:///"), - bind_dn=dict(required=False, default=None), - bind_password=dict(required=False, no_log=True), - attributes=dict(required=False, type='dict', default=None), - ), - ) - - if not ldap_found: - module.fail_json(msg="The Python LDAP module is required") - - # Extract the attributes. If a single value is provided for an attribute, it - # must be convereted into one-element list. All items must be converted into - # UTF-8 strings otherwise. - attributes = {} - - def repack_value(value): - """ - Small helper to repack a single value into list of UTF-8-encoded - strings. - """ - - if isinstance(value, list): - value = [str(i).encode("utf-8") for i in value] - else: - value = [str(value).encode("utf-8")] - - return value - - if module.params["attributes"]: - for name, value in module.params["attributes"].iteritems(): - attributes[name] = repack_value(value) - - try: - connection = get_ldap_connection(module.params["server_uri"], - module.params["bind_dn"], - module.params["bind_password"]) - except ldap.LDAPError as e: - module.fail_json(msg="LDAP error: %s" % str(e)) - state = module.params["state"] - - entry = LDAPEntry(module.params["dn"], - attributes, - connection) - - # Add/remove entry as requested. - try: - if state == "present": - changed = entry.add() - elif state == "append": - changed = entry.append() - elif state == "replace": - changed = entry.replace() - else: - changed = entry.remove() - except ldap.LDAPError as e: - module.fail_json(msg="LDAP error: %s" % str(e)) - - module.exit_json(changed=changed) - - -# Import module snippets. -main() diff --git a/roles/ldap_server/tasks/main.yml b/roles/ldap_server/tasks/main.yml index de142f887d185d907ab2ad70e236b4386d99e46d..d77074d3d144ebca0904671a8ae1c03f056c0abb 100644 --- a/roles/ldap_server/tasks/main.yml +++ b/roles/ldap_server/tasks/main.yml @@ -82,11 +82,11 @@ mode: 0644 - name: Change log level for slapd - m_ldap_entry: + ldap_attr: dn: cn=config - state: replace - attributes: - olcLogLevel: "{{ ldap_server_log_level }}" + state: exact + name: olcLogLevel + values: "{{ ldap_server_log_level }}" - name: Test if LDAP misc schema has been applied command: "ldapsearch -H ldapi:/// -Q -LLL -A -Y EXTERNAL -b cn=schema,cn=config -s one '(cn={*}misc)' cn" @@ -125,40 +125,83 @@ group: root mode: 0644 -- name: Configure TLS for slapd (includes hardening) - m_ldap_entry: +# We need to have this hack around TLS configuration because OpenLDAP +# expects both private key and certificate to be set at the same +# time. +# +# OpenLDAP server behaviour is a bit weird around this thing, so here +# is what happens: +# +# 1. First we set the private key, but ignore all errors. This has not +# yet changed the private key path, though. +# +# 2. Then we set the certificate. This succeeds, but the private key +# path still has the old value. If we haven't done the step (1), +# this task would fail too. +# +# 3. Now we can finally change the private key too, and LDAP server +# will be able to validate it against the corresponding certificate. +# +# See https://github.com/ansible/ansible/issues/25665 for more +# information. +- name: Configure TLS private key (ignore errors) + ldap_attr: dn: cn=config - state: replace - attributes: - olcTLSCertificateFile: "/etc/ssl/certs/{{ ansible_fqdn }}_ldap.pem" - olcTLSCertificateKeyFile: "/etc/ssl/private/{{ ansible_fqdn }}_ldap.key" - olcTLSCipherSuite: "{{ ldap_tls_ciphers }}" - notify: - - Restart slapd + name: olcTLSCertificateKeyFile + values: "/etc/ssl/private/{{ ansible_fqdn }}_ldap.key" + state: exact + failed_when: false -- name: Configure SSF - m_ldap_entry: +- name: Configure TLS certificate + ldap_attr: dn: cn=config - state: replace - attributes: - olcSecurity: "ssf={{ ldap_server_ssf }}" - olcLocalSSF: "{{ ldap_server_ssf }}" + name: olcTLSCertificateFile + values: "/etc/ssl/certs/{{ ansible_fqdn }}_ldap.pem" + state: exact + +- name: Configure TLS private key + ldap_attr: + dn: cn=config + name: olcTLSCertificateKeyFile + values: "/etc/ssl/private/{{ ansible_fqdn }}_ldap.key" + state: exact + +- name: Configure TLS cipher suites + ldap_attr: + dn: cn=config + name: olcTLSCipherSuite + values: "{{ ldap_tls_ciphers }}" + state: exact + +- name: Configure SSF for local unix socket connections + ldap_attr: + dn: cn=config + state: exact + name: olcLocalSSF + values: "{{ ldap_server_ssf }}" + +- name: Configure required SSF + ldap_attr: + dn: cn=config + state: exact + name: olcSecurity + values: "ssf={{ ldap_server_ssf }}" - name: Enable the memberof module - m_ldap_entry: + ldap_attr: dn: "cn=module{0},cn=config" - state: append - attributes: - olcModuleLoad: "{1}memberof" + state: present + name: olcModuleLoad + values: "{1}memberof" - name: Enable the memberof overlay for database - m_ldap_entry: + ldap_entry: dn: "olcOverlay={0}memberof,olcDatabase={1}mdb,cn=config" + objectClass: + - olcConfig + - olcMemberOf + - olcOverlayConfig attributes: - objectClass: - - olcConfig - - olcMemberOf - - olcOverlayConfig olcOverlay: memberof olcMemberOfRefInt: "TRUE" olcMemberOfGroupOC: groupOfUniqueNames @@ -170,12 +213,11 @@ rules: "{{ ldap_permissions }}" - name: Create basic LDAP directory structure - m_ldap_entry: "" - args: + ldap_entry: dn: "ou={{ item }},{{ ldap_server_int_basedn }}" + objectClass: + - organizationalUnit attributes: - objectClass: - - organizationalUnit ou: "{{ item }}" with_items: - people @@ -183,54 +225,62 @@ - services - name: Create the entry that will contain mail service information - m_ldap_entry: "" - args: + ldap_entry: dn: "ou=mail,ou=services,{{ ldap_server_int_basedn }}" + objectClass: + - organizationalUnit attributes: - objectClass: organizationalUnit ou: mail - name: Create LDAP directory structure for mail service - m_ldap_entry: "" - args: + ldap_entry: dn: "ou={{ item }},ou=mail,ou=services,{{ ldap_server_int_basedn }}" + objectClass: + - organizationalUnit attributes: - objectClass: organizationalUnit ou: "{{ item }}" with_items: - domains - aliases - name: Create or remove login entries for services - m_ldap_entry: "" - args: + ldap_entry: dn: "cn={{ item.name }},ou=services,{{ ldap_server_int_basedn }}" + objectClass: + - applicationProcess + - simpleSecurityObject attributes: - objectClass: - - applicationProcess - - simpleSecurityObject cn: "{{ item.name }}" userPassword: "{{ item.password }}" state: "{{ item.state | default('present') }}" with_items: "{{ ldap_server_consumers }}" +- name: Update services login passwords + ldap_attr: + dn: "cn={{ item.name }},ou=services,{{ ldap_server_int_basedn }}" + name: userPassword + values: "{{ item.password }}" + state: exact + with_items: "{{ ldap_server_consumers }}" + when: "item.state | default('present') == 'present'" + - name: Create or remove user-supplied groups - m_ldap_entry: "" - args: + ldap_entry: dn: "cn={{ item.name }},ou=groups,{{ ldap_server_int_basedn }}" + objectClass: + - groupOfUniqueNames attributes: - objectClass: groupOfUniqueNames cn: "{{ item.name }}" uniqueMember: "cn=NONE" - state: "{{ item.state | default('append') }}" + state: "{{ item.state | default('present') }}" with_items: "{{ ldap_server_groups }}" - name: Create user-supplied LDAP entries - m_ldap_entry: "" - args: + ldap_entry: dn: "{{ item.dn }}" - state: "{{ item.state | default('present')}}" + objectClass: "{{ item.attributes.objectClass }}" attributes: "{{ item.attributes }}" + state: "{{ item.state | default('present')}}" with_items: "{{ ldap_entries }}" - name: Deploy firewall configuration for LDAP