288 lines
		
	
	
	
		
			9.3 KiB
		
	
	
	
		
			Python
		
	
	
		
			Executable file
		
	
	
	
	
			
		
		
	
	
			288 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=(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()
 |