LDAP: Refactor and add ldap_passwd module (#33040)

* 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 <KellerFuchs@hashbang.sh>
Signed-off-by: Abhijeet Kasurde <akasurde@redhat.com>
pull/39250/merge
The Fox in the Shell 7 years ago committed by Abhijeet Kasurde
parent 19e1f41837
commit efe7c20100

@ -0,0 +1,78 @@
# -*- coding: utf-8 -*-
# Copyright: (c) 2016, Peter Sagerson <psagers@ignorare.net>
# Copyright: (c) 2016, Jiri Tyr <jiri.tyr@gmail.com>
# Copyright: (c) 2017-2018 Keller Fuchs (@kellerfuchs) <kellerfuchs@hashbang.sh>
#
# 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

@ -1,8 +1,8 @@
#!/usr/bin/python #!/usr/bin/python
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# (c) 2016, Peter Sagerson <psagers@ignorare.net> # Copyright: (c) 2016, Peter Sagerson <psagers@ignorare.net>
# (c) 2016, Jiri Tyr <jiri.tyr@gmail.com> # Copyright: (c) 2016, Jiri Tyr <jiri.tyr@gmail.com>
# #
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) # 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 __metaclass__ = type
ANSIBLE_METADATA = {'metadata_version': '1.1', ANSIBLE_METADATA = {
'status': ['preview'], 'metadata_version': '1.1',
'supported_by': 'community'} 'status': ['preview'],
'supported_by': 'community'
}
DOCUMENTATION = """ DOCUMENTATION = """
@ -42,33 +44,10 @@ author:
requirements: requirements:
- python-ldap - python-ldap
options: 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: name:
description: description:
- The name of the attribute to modify. - The name of the attribute to modify.
required: true 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: state:
description: description:
- The state of the attribute values. If C(present), all given - 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 strings. The complex argument format is required in order to pass
a list of strings (see examples). a list of strings (see examples).
required: true required: true
validate_certs: extends_documentation_fragment: ldap.documentation
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"
""" """
@ -178,30 +151,25 @@ modlist:
import traceback 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: try:
import ldap import ldap
import ldap.sasl
HAS_LDAP = True HAS_LDAP = True
except ImportError: except ImportError:
HAS_LDAP = False 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): def __init__(self, module):
LdapGeneric.__init__(self, module)
# Shortcuts # 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.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.state = self.module.params['state']
self.verify_cert = self.module.params['validate_certs']
# Normalize values # Normalize values
if isinstance(self.module.params['values'], list): if isinstance(self.module.params['values'], list):
@ -209,9 +177,6 @@ class LdapAttr(object):
else: else:
self.values = [str(self.module.params['values'])] self.values = [str(self.module.params['values'])]
# Establish connection
self.connection = self._connect_to_ldap()
def add(self): def add(self):
values_to_add = filter(self._is_value_absent, self.values) values_to_add = filter(self._is_value_absent, self.values)
@ -237,9 +202,7 @@ class LdapAttr(object):
results = self.connection.search_s( results = self.connection.search_s(
self.dn, ldap.SCOPE_BASE, attrlist=[self.name]) self.dn, ldap.SCOPE_BASE, attrlist=[self.name])
except ldap.LDAPError as e: except ldap.LDAPError as e:
self.module.fail_json( self.fail("Cannot search for attribute %s" % self.name, e)
msg="Cannot search for attribute %s" % self.name,
details=to_native(e))
current = results[0][1].get(self.name, []) current = results[0][1].get(self.name, [])
modlist = [] modlist = []
@ -268,46 +231,17 @@ class LdapAttr(object):
""" True if the target attribute doesn't have the given value. """ """ True if the target attribute doesn't have the given value. """
return not self._is_value_present(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(): def main():
module = AnsibleModule( module = AnsibleModule(
argument_spec={ argument_spec=gen_specs(
'bind_dn': dict(default=None), name=dict(required=True),
'bind_pw': dict(default='', no_log=True), params=dict(type='dict'),
'dn': dict(required=True), state=dict(
'name': dict(required=True),
'params': dict(type='dict'),
'server_uri': dict(default='ldapi:///'),
'start_tls': dict(default=False, type='bool'),
'state': dict(
default='present', default='present',
choices=['present', 'absent', 'exact']), choices=['present', 'absent', 'exact']),
'values': dict(required=True, type='raw'), values=dict(required=True, type='raw'),
'validate_certs': dict(default=True, type='bool'), ),
},
supports_check_mode=True, supports_check_mode=True,
) )

@ -1,8 +1,8 @@
#!/usr/bin/python #!/usr/bin/python
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# (c) 2016, Peter Sagerson <psagers@ignorare.net> # Copyright: (c) 2016, Peter Sagerson <psagers@ignorare.net>
# (c) 2016, Jiri Tyr <jiri.tyr@gmail.com> # Copyright: (c) 2016, Jiri Tyr <jiri.tyr@gmail.com>
# #
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) # 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 __metaclass__ = type
ANSIBLE_METADATA = {'metadata_version': '1.1', ANSIBLE_METADATA = {
'status': ['preview'], 'metadata_version': '1.1',
'supported_by': 'community'} 'status': ['preview'],
'supported_by': 'community'
}
DOCUMENTATION = """ DOCUMENTATION = """
@ -36,18 +38,6 @@ author:
requirements: requirements:
- python-ldap - python-ldap
options: 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: attributes:
description: description:
- If I(state=present), attributes necessary to create an entry. Existing - 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 - 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 I(attributes) options. To remove an option, set the value of the option
to C(null). 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: state:
description: description:
- The target state of the entry. - The target state of the entry.
choices: [present, absent] choices: [present, absent]
default: present default: present
validate_certs: extends_documentation_fragment: ldap.documentation
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"
""" """
@ -135,31 +108,25 @@ RETURN = """
import traceback 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: try:
import ldap
import ldap.modlist import ldap.modlist
import ldap.sasl
HAS_LDAP = True HAS_LDAP = True
except ImportError: except ImportError:
HAS_LDAP = False 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): def __init__(self, module):
LdapGeneric.__init__(self, module)
# Shortcuts # 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.state = self.module.params['state']
self.verify_cert = self.module.params['validate_certs']
# Add the objectClass into the list of attributes # Add the objectClass into the list of attributes
self.module.params['attributes']['objectClass'] = ( self.module.params['attributes']['objectClass'] = (
@ -169,9 +136,6 @@ class LdapEntry(object):
if self.state == 'present': if self.state == 'present':
self.attrs = self._load_attrs() self.attrs = self._load_attrs()
# Establish connection
self.connection = self._connect_to_ldap()
def _load_attrs(self): def _load_attrs(self):
""" Turn attribute's value to array. """ """ Turn attribute's value to array. """
attrs = {} attrs = {}
@ -222,46 +186,15 @@ class LdapEntry(object):
return is_present 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(): def main():
module = AnsibleModule( module = AnsibleModule(
argument_spec={ argument_spec=gen_specs(
'attributes': dict(default={}, type='dict'), attributes=dict(default={}, type='dict'),
'bind_dn': dict(), objectClass=dict(type='raw'),
'bind_pw': dict(default='', no_log=True), params=dict(type='dict'),
'dn': dict(required=True), state=dict(default='present', choices=['present', 'absent']),
'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'),
},
supports_check_mode=True, supports_check_mode=True,
) )

@ -0,0 +1,147 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright: (c) 2017-2018, Keller Fuchs <kellerfuchs@hashbang.sh>
#
# 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()

@ -0,0 +1,42 @@
# -*- coding: utf-8 -*-
# Copyright: (c) 2016, Peter Sagerson <psagers@ignorare.net>
# Copyright: (c) 2016, Jiri Tyr <jiri.tyr@gmail.com>
# Copyright: (c) 2017-2018 Keller Fuchs (@kellerfuchs) <kellerfuchs@hashbang.sh>
#
# 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"
'''
Loading…
Cancel
Save