|
- #!/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()
|