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.

240 lines
7.5KB

  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=(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. self.server_uri = self.module.params['server_uri']
  96. self.start_tls = self._boolean_param('start_tls')
  97. self.bind_dn = self._utf8_param('bind_dn')
  98. self.bind_pw = self._utf8_param('bind_pw')
  99. self.dn = self._utf8_param('dn')
  100. self._load_attrs()
  101. if 'objectClass' not in self.attrs:
  102. self.module.fail_json(msg="At least one objectClass must be provided")
  103. def _boolean_param(self, name):
  104. return self.module.boolean(self.module.params[name])
  105. def _utf8_param(self, name):
  106. return self._force_utf8(self.module.params[name])
  107. def _load_attr_values(self, name, raw):
  108. if isinstance(raw, basestring):
  109. values = raw.split(',')
  110. else:
  111. values = raw
  112. if not (isinstance(values, list) and all(isinstance(value, basestring) for value in values)):
  113. self.module.fail_json(msg="{} must be a string or list of strings.".format(name))
  114. return map(self._force_utf8, values)
  115. def _force_utf8(self, value):
  116. """ If value is unicode, encode to utf-8. """
  117. if isinstance(value, unicode):
  118. value = value.encode('utf-8')
  119. return value
  120. def _load_attrs(self):
  121. self.attrs = {}
  122. for name, raw in self.module.params.iteritems():
  123. if name not in self.module.argument_spec:
  124. self.attrs[name] = self._load_attr_values(name, raw)
  125. def main(self):
  126. if self.entry_exists():
  127. results = self.update_entry()
  128. self.module.exit_json(**results)
  129. else:
  130. insert_result = self.insert_entry()
  131. self.module.exit_json(changed=True, results=[insert_result])
  132. def entry_exists(self):
  133. try:
  134. results = self.connection.search_s(self.dn, ldap.SCOPE_BASE)
  135. for result in results:
  136. if result[0] == self.dn:
  137. return True
  138. except ldap.NO_SUCH_OBJECT:
  139. return False
  140. def insert_entry(self):
  141. modlist = ldap.modlist.addModlist(self.attrs)
  142. result = self.connection.add_s(self.dn, modlist)
  143. return result
  144. def update_entry(self):
  145. results = []
  146. for attr, value in self.attrs.iteritems():
  147. if attr == 'objectClass': continue
  148. value = self._extract_value(value)
  149. check = self._attribute_value_check(attr, value)
  150. if check is False:
  151. op = ldap.MOD_REPLACE
  152. elif check is None:
  153. op = ldap.MOD_ADD
  154. else:
  155. op = None # Nothing to see here...
  156. if op is not None:
  157. result = self.connection.modify_s(self.dn, [(op, attr, value)])
  158. results.append(result)
  159. if len(results) == 0:
  160. return dict(changed=False)
  161. else:
  162. return dict(changed=True, results=results)
  163. def _attribute_value_check(self, attr, value):
  164. try:
  165. return bool(self.connection.compare_s(self.dn, attr, value))
  166. except ldap.NO_SUCH_ATTRIBUTE:
  167. return None
  168. def _extract_value(self, values):
  169. if isinstance(values, basestring):
  170. if values == '':
  171. values = []
  172. else:
  173. values = [values]
  174. if not (isinstance(values, list) and all(isinstance(value, basestring) for value in values)):
  175. self.module.fail_json(msg="Attribute values must be strings or lists of strings.")
  176. values = map(self._force_utf8, values)
  177. if len(values) == 1:
  178. return values[0]
  179. else:
  180. return values
  181. #
  182. # LDAP Connection
  183. #
  184. _connection = None
  185. @property
  186. def connection(self):
  187. """ An authenticated connection to the LDAP server (cached). """
  188. if self._connection is None:
  189. self._connection = self._connect_to_ldap()
  190. return self._connection
  191. def _connect_to_ldap(self):
  192. connection = ldap.initialize(self.server_uri)
  193. if self.start_tls:
  194. connection.start_tls_s()
  195. if self.bind_dn is not None:
  196. connection.simple_bind_s(self.bind_dn, self.bind_pw)
  197. else:
  198. connection.sasl_interactive_bind_s('', ldap.sasl.external())
  199. return connection
  200. from ansible.module_utils.basic import * # noqa
  201. main()