From efe7c2010087b6bbc5e14598e272f8839ad4ba64 Mon Sep 17 00:00:00 2001 From: The Fox in the Shell Date: Fri, 27 Apr 2018 10:24:05 +0000 Subject: [PATCH] LDAP: Refactor and add ldap_passwd module (#33040) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * modules/net_tools/ldap: Refactor shared options * modules/net_tools/ldap: Refactor shared code * modules/net_tools/ldap: Add ldap_passwd module * modules/net_tools/ldap/ldap_passwd: More robust change check * In some deployments, using compare_s results in spurious “changed” results, while bind is more reliable. The downside is that it results in an extra connection, and the code it more involved. * ldap_passwd: Rename methods passwd_[cs] * ldap_passwd: Remove unecessary type=str * ldap: Factor-out failure cases * ldap_passwd: Provide more precise error messages * ldap_passwd: Irrelevant syntax changes * ldap_passwd: Rename u_con to tmp_con * ldap_passwd: Keep HAS_LDAP local * LDAP doc update * Resolved all copyright related issues * Resolved self.fail calls * Update documentation Signed-off-by: The Fox in the Shell Signed-off-by: Abhijeet Kasurde --- lib/ansible/module_utils/ldap.py | 78 ++++++++++ .../modules/net_tools/ldap/ldap_attr.py | 110 +++---------- .../modules/net_tools/ldap/ldap_entry.py | 111 +++---------- .../modules/net_tools/ldap/ldap_passwd.py | 147 ++++++++++++++++++ .../utils/module_docs_fragments/ldap.py | 42 +++++ 5 files changed, 311 insertions(+), 177 deletions(-) create mode 100644 lib/ansible/module_utils/ldap.py create mode 100644 lib/ansible/modules/net_tools/ldap/ldap_passwd.py create mode 100644 lib/ansible/utils/module_docs_fragments/ldap.py diff --git a/lib/ansible/module_utils/ldap.py b/lib/ansible/module_utils/ldap.py new file mode 100644 index 00000000000..96119b0b09f --- /dev/null +++ b/lib/ansible/module_utils/ldap.py @@ -0,0 +1,78 @@ +# -*- coding: utf-8 -*- + +# Copyright: (c) 2016, Peter Sagerson +# Copyright: (c) 2016, Jiri Tyr +# Copyright: (c) 2017-2018 Keller Fuchs (@kellerfuchs) +# +# 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 + +import traceback +from ansible.module_utils._text import to_native + +try: + import ldap + import ldap.sasl + + HAS_LDAP = True +except ImportError: + HAS_LDAP = False + + +def gen_specs(**specs): + specs.update({ + 'bind_dn': dict(), + 'bind_pw': dict(default='', no_log=True), + 'dn': dict(required=True), + 'server_uri': dict(default='ldapi:///'), + 'start_tls': dict(default=False, type='bool'), + 'validate_certs': dict(default=True, type='bool'), + }) + + return specs + + +class LdapGeneric(object): + def __init__(self, module): + # Shortcuts + self.module = module + self.bind_dn = self.module.params['bind_dn'] + self.bind_pw = self.module.params['bind_pw'] + self.dn = self.module.params['dn'] + self.server_uri = self.module.params['server_uri'] + self.start_tls = self.module.params['start_tls'] + self.verify_cert = self.module.params['validate_certs'] + + # Establish connection + self.connection = self._connect_to_ldap() + + def fail(self, msg, exn): + self.module.fail_json( + msg=msg, + details=to_native(exn), + exception=traceback.format_exc() + ) + + def _connect_to_ldap(self): + if not self.verify_cert: + ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_NEVER) + + connection = ldap.initialize(self.server_uri) + + if self.start_tls: + try: + connection.start_tls_s() + except ldap.LDAPError as e: + self.fail("Cannot start TLS.", e) + + try: + if self.bind_dn is not None: + connection.simple_bind_s(self.bind_dn, self.bind_pw) + else: + connection.sasl_interactive_bind_s('', ldap.sasl.external()) + except ldap.LDAPError as e: + self.fail("Cannot bind to the server.", e) + + return connection diff --git a/lib/ansible/modules/net_tools/ldap/ldap_attr.py b/lib/ansible/modules/net_tools/ldap/ldap_attr.py index a1784768da1..9212a314227 100644 --- a/lib/ansible/modules/net_tools/ldap/ldap_attr.py +++ b/lib/ansible/modules/net_tools/ldap/ldap_attr.py @@ -1,8 +1,8 @@ #!/usr/bin/python # -*- coding: utf-8 -*- -# (c) 2016, Peter Sagerson -# (c) 2016, Jiri Tyr +# Copyright: (c) 2016, Peter Sagerson +# Copyright: (c) 2016, Jiri Tyr # # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) @@ -10,9 +10,11 @@ from __future__ import absolute_import, division, print_function __metaclass__ = type -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} +ANSIBLE_METADATA = { + 'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community' +} DOCUMENTATION = """ @@ -42,33 +44,10 @@ author: requirements: - python-ldap options: - bind_dn: - description: - - A DN to bind with. If this is omitted, we'll try a SASL bind with - the EXTERNAL mechanism. If this is blank, we'll use an anonymous - bind. - bind_pw: - description: - - The password to use with I(bind_dn). - dn: - description: - - The DN of the entry to modify. - required: true name: description: - The name of the attribute to modify. required: true - server_uri: - description: - - A URI to the LDAP server. The default value lets the underlying - LDAP client library look for a UNIX domain socket in its default - location. - default: ldapi:/// - start_tls: - description: - - If true, we'll use the START_TLS LDAP extension. - type: bool - default: 'no' state: description: - The state of the attribute values. If C(present), all given @@ -85,13 +64,7 @@ options: strings. The complex argument format is required in order to pass a list of strings (see examples). required: true - validate_certs: - description: - - If C(no), SSL certificates will not be validated. This should only be - used on sites using self-signed certificates. - type: bool - default: 'yes' - version_added: "2.4" +extends_documentation_fragment: ldap.documentation """ @@ -178,30 +151,25 @@ modlist: import traceback +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils._text import to_native +from ansible.module_utils.ldap import LdapGeneric, gen_specs + try: import ldap - import ldap.sasl HAS_LDAP = True except ImportError: HAS_LDAP = False -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils._text import to_native - -class LdapAttr(object): +class LdapAttr(LdapGeneric): def __init__(self, module): + LdapGeneric.__init__(self, module) + # Shortcuts - self.module = module - self.bind_dn = self.module.params['bind_dn'] - self.bind_pw = self.module.params['bind_pw'] - self.dn = self.module.params['dn'] self.name = self.module.params['name'] - self.server_uri = self.module.params['server_uri'] - self.start_tls = self.module.params['start_tls'] self.state = self.module.params['state'] - self.verify_cert = self.module.params['validate_certs'] # Normalize values if isinstance(self.module.params['values'], list): @@ -209,9 +177,6 @@ class LdapAttr(object): else: self.values = [str(self.module.params['values'])] - # Establish connection - self.connection = self._connect_to_ldap() - def add(self): values_to_add = filter(self._is_value_absent, self.values) @@ -237,9 +202,7 @@ class LdapAttr(object): results = self.connection.search_s( self.dn, ldap.SCOPE_BASE, attrlist=[self.name]) except ldap.LDAPError as e: - self.module.fail_json( - msg="Cannot search for attribute %s" % self.name, - details=to_native(e)) + self.fail("Cannot search for attribute %s" % self.name, e) current = results[0][1].get(self.name, []) modlist = [] @@ -268,46 +231,17 @@ class LdapAttr(object): """ True if the target attribute doesn't have the given value. """ return not self._is_value_present(value) - def _connect_to_ldap(self): - if not self.verify_cert: - ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_NEVER) - - connection = ldap.initialize(self.server_uri) - - if self.start_tls: - try: - connection.start_tls_s() - except ldap.LDAPError as e: - self.module.fail_json(msg="Cannot start TLS.", details=to_native(e)) - - try: - if self.bind_dn is not None: - connection.simple_bind_s(self.bind_dn, self.bind_pw) - else: - connection.sasl_interactive_bind_s('', ldap.sasl.external()) - except ldap.LDAPError as e: - self.module.fail_json( - msg="Cannot bind to the server.", details=to_native(e)) - - return connection - def main(): module = AnsibleModule( - argument_spec={ - 'bind_dn': dict(default=None), - 'bind_pw': dict(default='', no_log=True), - 'dn': dict(required=True), - 'name': dict(required=True), - 'params': dict(type='dict'), - 'server_uri': dict(default='ldapi:///'), - 'start_tls': dict(default=False, type='bool'), - 'state': dict( + argument_spec=gen_specs( + name=dict(required=True), + params=dict(type='dict'), + state=dict( default='present', choices=['present', 'absent', 'exact']), - 'values': dict(required=True, type='raw'), - 'validate_certs': dict(default=True, type='bool'), - }, + values=dict(required=True, type='raw'), + ), supports_check_mode=True, ) diff --git a/lib/ansible/modules/net_tools/ldap/ldap_entry.py b/lib/ansible/modules/net_tools/ldap/ldap_entry.py index 73fb10bd6ca..ebc4a969e77 100644 --- a/lib/ansible/modules/net_tools/ldap/ldap_entry.py +++ b/lib/ansible/modules/net_tools/ldap/ldap_entry.py @@ -1,8 +1,8 @@ #!/usr/bin/python # -*- coding: utf-8 -*- -# (c) 2016, Peter Sagerson -# (c) 2016, Jiri Tyr +# Copyright: (c) 2016, Peter Sagerson +# Copyright: (c) 2016, Jiri Tyr # # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) @@ -10,9 +10,11 @@ from __future__ import absolute_import, division, print_function __metaclass__ = type -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} +ANSIBLE_METADATA = { + 'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community' +} DOCUMENTATION = """ @@ -36,18 +38,6 @@ author: requirements: - python-ldap options: - bind_dn: - description: - - A DN to bind with. If this is omitted, we'll try a SASL bind with - the EXTERNAL mechanism. If this is blank, we'll use an anonymous - bind. - bind_pw: - description: - - The password to use with I(bind_dn). - dn: - description: - - The DN of the entry to add or remove. - required: true attributes: description: - If I(state=present), attributes necessary to create an entry. Existing @@ -63,29 +53,12 @@ options: - List of options which allows to overwrite any of the task or the I(attributes) options. To remove an option, set the value of the option to C(null). - server_uri: - description: - - A URI to the LDAP server. The default value lets the underlying - LDAP client library look for a UNIX domain socket in its default - location. - default: ldapi:/// - start_tls: - description: - - If true, we'll use the START_TLS LDAP extension. - type: bool - default: 'no' state: description: - The target state of the entry. choices: [present, absent] default: present - validate_certs: - description: - - If C(no), SSL certificates will not be validated. This should only be - used on sites using self-signed certificates. - type: bool - default: 'yes' - version_added: "2.4" +extends_documentation_fragment: ldap.documentation """ @@ -135,31 +108,25 @@ RETURN = """ import traceback +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.six import string_types +from ansible.module_utils._text import to_native +from ansible.module_utils.ldap import LdapGeneric, gen_specs + try: - import ldap import ldap.modlist - import ldap.sasl HAS_LDAP = True except ImportError: HAS_LDAP = False -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.six import string_types -from ansible.module_utils._text import to_native - -class LdapEntry(object): +class LdapEntry(LdapGeneric): def __init__(self, module): + LdapGeneric.__init__(self, module) + # Shortcuts - self.module = module - self.bind_dn = self.module.params['bind_dn'] - self.bind_pw = self.module.params['bind_pw'] - self.dn = self.module.params['dn'] - self.server_uri = self.module.params['server_uri'] - self.start_tls = self.module.params['start_tls'] self.state = self.module.params['state'] - self.verify_cert = self.module.params['validate_certs'] # Add the objectClass into the list of attributes self.module.params['attributes']['objectClass'] = ( @@ -169,9 +136,6 @@ class LdapEntry(object): if self.state == 'present': self.attrs = self._load_attrs() - # Establish connection - self.connection = self._connect_to_ldap() - def _load_attrs(self): """ Turn attribute's value to array. """ attrs = {} @@ -222,46 +186,15 @@ class LdapEntry(object): return is_present - def _connect_to_ldap(self): - if not self.verify_cert: - ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_NEVER) - - connection = ldap.initialize(self.server_uri) - - if self.start_tls: - try: - connection.start_tls_s() - except ldap.LDAPError as e: - self.module.fail_json(msg="Cannot start TLS.", details=to_native(e), - exception=traceback.format_exc()) - - try: - if self.bind_dn is not None: - connection.simple_bind_s(self.bind_dn, self.bind_pw) - else: - connection.sasl_interactive_bind_s('', ldap.sasl.external()) - except ldap.LDAPError as e: - self.module.fail_json( - msg="Cannot bind to the server.", details=to_native(e), - exception=traceback.format_exc()) - - return connection - def main(): module = AnsibleModule( - argument_spec={ - 'attributes': dict(default={}, type='dict'), - 'bind_dn': dict(), - 'bind_pw': dict(default='', no_log=True), - 'dn': dict(required=True), - 'objectClass': dict(type='raw'), - 'params': dict(type='dict'), - 'server_uri': dict(default='ldapi:///'), - 'start_tls': dict(default=False, type='bool'), - 'state': dict(default='present', choices=['present', 'absent']), - 'validate_certs': dict(default=True, type='bool'), - }, + argument_spec=gen_specs( + attributes=dict(default={}, type='dict'), + objectClass=dict(type='raw'), + params=dict(type='dict'), + state=dict(default='present', choices=['present', 'absent']), + ), supports_check_mode=True, ) diff --git a/lib/ansible/modules/net_tools/ldap/ldap_passwd.py b/lib/ansible/modules/net_tools/ldap/ldap_passwd.py new file mode 100644 index 00000000000..4827cd0cdae --- /dev/null +++ b/lib/ansible/modules/net_tools/ldap/ldap_passwd.py @@ -0,0 +1,147 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2017-2018, Keller Fuchs +# +# 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 = """ +--- +module: ldap_passwd +short_description: Set passwords in LDAP. +description: + - Set a password for an LDAP entry. This module only asserts that + a given password is valid for a given entry. To assert the + existence of an entry, see M(ldap_entry). +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 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). +version_added: '2.6' +author: + - Keller Fuchs (@kellerfuchs) +requirements: + - python-ldap +options: + passwd: + required: true + default: null + description: + - The (plaintext) password to be set for I(dn). +extends_documentation_fragment: ldap.documentation +""" + +EXAMPLES = """ +- name: Set a password for the admin user + ldap_passwd: + dn: cn=admin,dc=example,dc=com + passwd: "{{ vault_secret }}" + +- name: Setting passwords in bulk + ldap_passwd: + dn: "{{ item.key }}" + passwd: "{{ item.value }}" + with_dict: + alice: alice123123 + bob: "|30b!" + admin: "{{ vault_secret }}" +""" + +RETURN = """ +modlist: + description: list of modified parameters + returned: success + type: list + sample: '[[2, "olcRootDN", ["cn=root,dc=example,dc=com"]]]' +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.ldap import LdapGeneric, gen_specs + +try: + import ldap + + HAS_LDAP = True +except ImportError: + HAS_LDAP = False + + +class LdapPasswd(LdapGeneric): + def __init__(self, module): + LdapGeneric.__init__(self, module) + + # Shortcuts + self.passwd = self.module.params['passwd'] + + def passwd_check(self): + try: + tmp_con = ldap.initialize(self.server_uri) + except ldap.LDAPError as e: + self.fail("Cannot initialize LDAP connection", e) + + if self.start_tls: + try: + tmp_con.start_tls_s() + except ldap.LDAPError as e: + self.fail("Cannot start TLS.", e) + + try: + tmp_con.simple_bind_s(self.dn, self.passwd) + except ldap.INVALID_CREDENTIALS: + return True + except ldap.LDAPError as e: + self.fail("Cannot bind to the server.", e) + else: + return False + finally: + tmp_con.unbind() + + def passwd_set(self): + # Exit early if the password is already valid + if not self.passwd_check(): + return False + + # Change the password (or throw an exception) + try: + self.connection.passwd_set(self.dn, None, self.passwd) + except ldap.LDAPError as e: + self.fail("Unable to set password", e) + + # Password successfully changed + return True + + +def main(): + module = AnsibleModule( + argument_spec=gen_specs(passwd=dict(no_log=True)), + supports_check_mode=True, + ) + + if not HAS_LDAP: + module.fail_json( + msg="Missing required 'ldap' module (pip install python-ldap).") + + ldap = LdapPasswd(module) + + if module.check_mode: + module.exit_json(changed=ldap.passwd_check()) + + module.exit_json(changed=ldap.passwd_set()) + + +if __name__ == '__main__': + main() diff --git a/lib/ansible/utils/module_docs_fragments/ldap.py b/lib/ansible/utils/module_docs_fragments/ldap.py new file mode 100644 index 00000000000..6fcf3090bb9 --- /dev/null +++ b/lib/ansible/utils/module_docs_fragments/ldap.py @@ -0,0 +1,42 @@ +# -*- coding: utf-8 -*- + +# Copyright: (c) 2016, Peter Sagerson +# Copyright: (c) 2016, Jiri Tyr +# Copyright: (c) 2017-2018 Keller Fuchs (@kellerfuchs) +# +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + + +class ModuleDocFragment(object): + # Standard LDAP documentation fragment + DOCUMENTATION = ''' +options: + bind_dn: + description: + - A DN to bind with. If this is omitted, we'll try a SASL bind with the EXTERNAL mechanism. + - If this is blank, we'll use an anonymous bind. + bind_pw: + description: + - The password to use with I(bind_dn). + dn: + required: true + description: + - The DN of the entry to add or remove. + server_uri: + default: ldapi:/// + description: + - A URI to the LDAP server. + - The default value lets the underlying LDAP client library look for a UNIX domain socket in its default location. + start_tls: + default: 'no' + type: bool + description: + - If true, we'll use the START_TLS LDAP extension. + validate_certs: + default: 'yes' + type: bool + description: + - If set to C(no), SSL certificates will not be validated. + - This should only be used on sites using self-signed certificates. + version_added: "2.4" +'''