Ansible modules for talking to LDAP servers https://github.com/unchained-capital/ansible-ldap-modules
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

230 lines
7.3KB

  1. #!/usr/bin/env python
  2. from traceback import format_exc
  3. import ldap
  4. import ldap.modlist
  5. import ldap.sasl
  6. DOCUMENTATION = """
  7. ---
  8. module: ldap_upsert
  9. short_description: Insert or update an LDAP entry.
  10. description:
  11. - Insert or update an LDAP entry. Existing entries with matching
  12. C(dn) will have their attributes updated. Otherwise a new entry
  13. will be created with the given attributes. This module cannot
  14. delete entries or remove attributes, see M(ldap_entry) and
  15. M(ldap_attr), respectively.
  16. notes: []
  17. version_added: null
  18. author: Peter Sagerson
  19. requirements:
  20. - python-ldap
  21. options:
  22. dn:
  23. required: true
  24. description:
  25. - The DN of the entry to insert or update.
  26. objectClass:
  27. required: true
  28. description:
  29. - Must be a list of objectClass values to use when
  30. creating the entry. It can either be a string containing
  31. a comma-separated list of values, or an actual list of
  32. strings. When updating, these values are not used.
  33. '...':
  34. required: false
  35. description:
  36. - All additional arguments are taken to be LDAP attribute
  37. names like C(objectClass), with similar lists of values.
  38. If the entry exists, these attributes will be updated if
  39. necessary. Otherwise the newly created entry will have
  40. these attributes.
  41. server_uri:
  42. required: false
  43. default: ldapi:///
  44. description:
  45. - A URI to the LDAP server. The default value lets the underlying
  46. LDAP client library look for a UNIX domain socket in its default
  47. location.
  48. start_tls:
  49. required: false
  50. default: false
  51. description:
  52. - If true, we'll use the START_TLS LDAP extension.
  53. bind_dn:
  54. required: false
  55. description:
  56. - A DN to bind with. If this is omitted, we'll try a SASL bind with
  57. the EXTERNAL mechanism. If this is blank, we'll use an anonymous
  58. bind.
  59. bind_pw:
  60. required: false
  61. description:
  62. - The password to use with C(bind_dn).
  63. """
  64. EXAMPLES = """
  65. # Make sure we have a parent entry for users.
  66. - ldap_entry: dn='ou=users,dc=example,dc=com' objectClass=organizationalUnit description="My Org Unit"
  67. sudo: true
  68. # Make sure we have an admin user.
  69. - ldap_entry:
  70. dn: 'cn=admin,dc=example,dc=com'
  71. objectClass: simpleSecurityObject,organizationalRole
  72. description: An LDAP administrator
  73. userPassword: '{SSHA}pedsA5Y9wHbZ5R90pRdxTEZmn6qvPdzm'
  74. sudo: true
  75. """
  76. def main():
  77. module = AnsibleModule(
  78. argument_spec={
  79. 'dn': dict(required=True),
  80. 'server_uri': dict(default='ldapi:///'),
  81. 'start_tls': dict(default='false', choices=(list(BOOLEANS)+['True', True, 'False', False])),
  82. 'bind_dn': dict(default=None),
  83. 'bind_pw': dict(default='', no_log=True),
  84. },
  85. check_invalid_arguments=False,
  86. supports_check_mode=False,
  87. )
  88. try:
  89. LdapUpsert(module).main()
  90. except ldap.LDAPError, e:
  91. module.fail_json(msg=str(e), exc=format_exc())
  92. class LdapUpsert(object):
  93. def __init__(self, module):
  94. self.module = module
  95. # Parameters that we have to directly pass to python-ldap need
  96. # to converted to UTF-8 first, as python-ldap doesn't
  97. # understand unicode strings.
  98. # Server parameters
  99. self.server_uri = self.module.params['server_uri']
  100. self.start_tls = self.module.boolean(self.module.params['start_tls'])
  101. self.bind_dn = self._utf8_param('bind_dn')
  102. self.bind_pw = self._utf8_param('bind_pw')
  103. # Entry parameters
  104. self.dn = self._utf8_param('dn')
  105. self._load_attrs()
  106. if 'objectClass' not in self.attrs:
  107. self.module.fail_json(msg="At least one objectClass must be provided")
  108. def _force_utf8(self, value):
  109. """If value is Unicode, encode to UTF-8."""
  110. if isinstance(value, unicode):
  111. return value.encode('utf-8')
  112. return value
  113. def _utf8_param(self, name):
  114. """Extract a parameter as UTF-8."""
  115. return self._force_utf8(self.module.params[name])
  116. def _load_attrs(self):
  117. self.attrs = {}
  118. for name, raw in self.module.params.iteritems():
  119. if name not in self.module.argument_spec:
  120. self.attrs[name] = self._load_attr_values(name, raw)
  121. def _load_attr_values(self, name, raw):
  122. if isinstance(raw, basestring):
  123. values = raw.split(',')
  124. else:
  125. values = raw
  126. if not (isinstance(values, list) and all(isinstance(value, basestring) for value in values)):
  127. self.module.fail_json(msg="{} must be a string or list of strings.".format(name))
  128. return list(map(self._force_utf8, values))
  129. def main(self):
  130. if self.entry_exists():
  131. results = self.update_entry()
  132. self.module.exit_json(**results)
  133. else:
  134. insert_result = self.insert_entry()
  135. self.module.exit_json(changed=True, results=[insert_result])
  136. def entry_exists(self):
  137. try:
  138. results = self.connection.search_s(self.dn, ldap.SCOPE_BASE)
  139. for result in results:
  140. if result[0] == self.dn:
  141. return True
  142. except ldap.NO_SUCH_OBJECT:
  143. return False
  144. def insert_entry(self):
  145. modlist = ldap.modlist.addModlist(self.attrs)
  146. result = self.connection.add_s(self.dn, modlist)
  147. return result
  148. def update_entry(self):
  149. results = []
  150. for attr, values in self.attrs.iteritems():
  151. if attr == 'objectClass': continue
  152. check = self._attribute_values_check(attr, values)
  153. if check is False:
  154. op = ldap.MOD_REPLACE
  155. elif check is None:
  156. op = ldap.MOD_ADD
  157. else:
  158. op = None # Nothing to see here...
  159. if op is not None:
  160. result = self.connection.modify_s(self.dn, [(op, attr, values)])
  161. results.append(result)
  162. if len(results) == 0:
  163. return dict(changed=False)
  164. else:
  165. return dict(changed=True, results=results)
  166. def _attribute_values_check(self, attr, values):
  167. try:
  168. return all(self._attribute_value_check(attr, value) for value in values)
  169. except ldap.NO_SUCH_ATTRIBUTE:
  170. return None
  171. def _attribute_value_check(self, attr, value):
  172. return bool(self.connection.compare_s(self.dn, attr, value))
  173. #
  174. # LDAP Connection
  175. #
  176. _connection = None
  177. @property
  178. def connection(self):
  179. """ An authenticated connection to the LDAP server (cached). """
  180. if self._connection is None:
  181. self._connection = self._connect_to_ldap()
  182. return self._connection
  183. def _connect_to_ldap(self):
  184. connection = ldap.initialize(self.server_uri)
  185. if self.start_tls:
  186. connection.start_tls_s()
  187. if self.bind_dn is not None:
  188. connection.simple_bind_s(self.bind_dn, self.bind_pw)
  189. else:
  190. connection.sasl_interactive_bind_s('', ldap.sasl.external())
  191. return connection
  192. from ansible.module_utils.basic import * # noqa
  193. main()