ansible-ldap-modules/ldap_attr
2018-05-10 01:24:22 +03:00

289 lines
9.3 KiB
Python
Executable file

#!/usr/bin/env python
from traceback import format_exc
import ldap
import ldap.sasl
DOCUMENTATION = """
---
module: ldap_attr
short_description: Add or remove LDAP attribute values.
description:
- Add or remove LDAP attribute values.
notes:
- This only deals with attributes on existing entries. To add or remove
whole entries, see M(ldap_entry).
- 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 C(bind_dn)
and C(bind_pw).
- For C(state=present) and C(state=absent), all value comparisons are
performed on the server for maximum accuracy. For C(state=exact), values
have to be compared in Python, which obviously ignores LDAP matching
rules. This should work out in most cases, but it is theoretically
possible to see spurious changes when target and actual values are
semantically identical but lexically distinct.
version_added: null
author: Peter Sagerson
requirements:
- python-ldap
options:
dn:
required: true
description:
- The DN of the entry to modify.
name:
required: true
description:
- The name of the attribute to modify.
values:
required: true
description:
- The value(s) to add or remove. This can be a string or a list of
strings. The complex argument format is required in order to pass
a list of strings (see examples).
state:
required: false
choices: [present, absent, exact]
default: present
description:
- The state of the attribute values. If C(present), all given
values will be added if they're missing. If C(absent), all given
values will be removed if present. If C(exact), the set of values
will be forced to exactly those provided and no others. If
C(state=exact) and C(values) is empty, all values for this
attribute will be removed.
server_uri:
required: false
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:
required: false
default: false
description:
- If true, we'll use the START_TLS LDAP extension.
bind_dn:
required: false
description:
- A DN to bind with. If this is omitted, we'll try a SASL bind with
the EXTERNAL mechanism (see note). If this is blank, we'll use an
anonymous bind.
bind_pw:
required: false
description:
- The password to use with C(bind_dn).
"""
EXAMPLES = """
# Configure directory number 1 for example.com.
- ldap_attr: dn='olcDatabase={1}hdb,cn=config' name=olcSuffix values='dc=example,dc=com' state=exact
sudo: true
# Set up the ACL. The complex argument format is required here to pass a list
# of ACL strings.
- ldap_attr:
sudo: true
args:
dn: olcDatabase={1}hdb,cn=config
name: olcAccess
values:
- '{0}to attrs=userPassword,shadowLastChange
by self write
by anonymous auth
by dn="cn=admin,dc=example,dc=com" write
by * none'
- '{1}to dn.base="dc=example,dc=com"
by dn="cn=admin,dc=example,dc=com" write
by * read'
state: exact
# Declare some indexes.
- ldap_attr: dn='olcDatabase={1}hdb,cn=config' name=olcDbIndex values={{ item }}
sudo: true
with_items:
- objectClass eq
- uid eq
# Set up a root user, which we can use later to bootstrap the directory.
- ldap_attr: dn='olcDatabase={1}hdb,cn=config' name={{ item.key }} values={{ item.value }} state=exact
sudo: true
with_dict:
olcRootDN: 'cn=root,dc=example,dc=com'
olcRootPW: '{SSHA}mRskON0Stk+5wO5K+MMk2xmakKt8h7eJ'
"""
def main():
module = AnsibleModule(
argument_spec={
'dn': dict(required=True),
'name': dict(required=True),
'values': dict(required=True),
'state': dict(default='present', choices=['present', 'absent', 'exact']),
'server_uri': dict(default='ldapi:///'),
'start_tls': dict(default='false', choices=(list(BOOLEANS)+['True', True, 'False', False])),
'bind_dn': dict(default=None),
'bind_pw': dict(default='', no_log=True),
},
supports_check_mode=True,
)
try:
LdapAttr(module).main()
except ldap.LDAPError, e:
module.fail_json(msg=str(e), exc=format_exc())
class LdapAttr(object):
def __init__(self, module):
self.module = module
# Parameters that we have to directly pass to python-ldap need
# to converted to UTF-8 first, as python-ldap doesn't
# understand unicode strings.
# Server parameters
self.server_uri = self.module.params['server_uri']
self.start_tls = self.module.boolean(self.module.params['start_tls'])
self.bind_dn = self._utf8_param('bind_dn')
self.bind_pw = self._utf8_param('bind_pw')
# Attribute parameters
self.dn = self._utf8_param('dn')
self.name = self._utf8_param('name')
self.values = self._normalized_values()
self.state = self.module.params['state']
self._connection = None
def _force_utf8(self, value):
"""If value is Unicode, encode to UTF-8."""
if isinstance(value, unicode):
return value.encode('utf-8')
return value
def _utf8_param(self, name):
"""Extract a parameter as UTF-8."""
return self._force_utf8(self.module.params[name])
def _normalized_values(self):
"""Parses the 'values' parameter into a list of UTF-8 strings."""
values = self.module.params['values']
if isinstance(values, basestring):
if values == '':
values = []
else:
values = [values]
if not (isinstance(values, list) and all(isinstance(value, basestring) for value in values)):
self.module.fail_json(msg="values must be a string or list of strings.")
return list(map(self._force_utf8, values))
def main(self):
if self.state == 'present':
modlist = self.handle_present()
elif self.state == 'absent':
modlist = self.handle_absent()
elif self.state == 'exact':
modlist = self.handle_exact()
else:
modlist = []
if len(modlist) > 0:
changed = True
if not self.module.check_mode:
self.connection.modify_s(self.dn, modlist)
else:
changed = False
self.module.exit_json(changed=changed, modlist=modlist)
#
# State Implementations
#
def handle_present(self):
values_to_add = filter(self.is_value_absent, self.values)
if len(values_to_add) > 0:
return [(ldap.MOD_ADD, self.name, values_to_add)]
else:
return []
def handle_absent(self):
values_to_delete = filter(self.is_value_present, self.values)
if len(values_to_delete) > 0:
return [(ldap.MOD_DELETE, self.name, values_to_delete)]
else:
return []
def handle_exact(self):
current = self.current_values()
if frozenset(self.values) != frozenset(current):
if len(current) == 0:
return [(ldap.MOD_ADD, self.name, self.values)]
elif len(self.values) == 0:
return [(ldap.MOD_DELETE, self.name, None)]
else:
return [(ldap.MOD_REPLACE, self.name, self.values)]
return []
#
# Util
#
def is_value_present(self, value):
"""True if the target attribute has the given value."""
try:
return bool(self.connection.compare_s(self.dn, self.name, value))
except ldap.NO_SUCH_ATTRIBUTE:
return False
def is_value_absent(self, value):
"""True if the target attribute does not have the given value."""
return (not self.is_value_present(value))
def current_values(self):
"""Returns the full list of values on the target attribute."""
results = self.connection.search_s(self.dn, ldap.SCOPE_BASE, attrlist=[self.name])
values = results[0][1].get(self.name, [])
return values
#
# LDAP Connection
#
@property
def connection(self):
""" An authenticated connection to the LDAP server (cached). """
if self._connection is None:
self._connection = self._connect_to_ldap()
return self._connection
def _connect_to_ldap(self):
connection = ldap.initialize(self.server_uri)
if self.start_tls:
connection.start_tls_s()
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())
return connection
from ansible.module_utils.basic import * # noqa
main()