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.

237 lines
7.1KB

  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_entry
  9. short_description: Add or remove LDAP entries.
  10. description:
  11. - Add or remove LDAP entries. This module only asserts the existence or
  12. non-existence of an LDAP entry, not its attributes. To assert the
  13. attribute values of an entry, see M(ldap_attr).
  14. notes: []
  15. version_added: null
  16. author: Peter Sagerson
  17. requirements:
  18. - python-ldap
  19. options:
  20. dn:
  21. required: true
  22. description:
  23. - The DN of the entry to add or remove.
  24. state:
  25. required: false
  26. choices: [present, absent]
  27. default: present
  28. description:
  29. - The target state of the entry.
  30. objectClass:
  31. required: false
  32. description:
  33. - If C(state=present), this must be a list of objectClass values to
  34. use when creating the entry. It can either be a string containing
  35. a comma-separated list of values, or an actual list of strings.
  36. '...':
  37. required: false
  38. description:
  39. - If C(state=present), all additional arguments are taken to be
  40. LDAP attribute names like C(objectClass), with similar
  41. lists of values. These should only be used to
  42. provide the minimum attributes necessary for creating an entry;
  43. existing entries are never modified. To assert specific attribute
  44. values on an existing entry, see M(ldap_attr).
  45. server_uri:
  46. required: false
  47. default: ldapi:///
  48. description:
  49. - A URI to the LDAP server. The default value lets the underlying
  50. LDAP client library look for a UNIX domain socket in its default
  51. location.
  52. start_tls:
  53. required: false
  54. default: false
  55. description:
  56. - If true, we'll use the START_TLS LDAP extension.
  57. bind_dn:
  58. required: false
  59. description:
  60. - A DN to bind with. If this is omitted, we'll try a SASL bind with
  61. the EXTERNAL mechanism. If this is blank, we'll use an anonymous
  62. bind.
  63. bind_pw:
  64. required: false
  65. description:
  66. - The password to use with C(bind_dn).
  67. """
  68. EXAMPLES = """
  69. # Make sure we have a parent entry for users.
  70. - ldap_entry: dn='ou=users,dc=example,dc=com' objectClass=organizationalUnit
  71. sudo: true
  72. # Make sure we have an admin user.
  73. - ldap_entry:
  74. dn: 'cn=admin,dc=example,dc=com'
  75. objectClass: simpleSecurityObject,organizationalRole
  76. description: An LDAP administrator
  77. userPassword: '{SSHA}pedsA5Y9wHbZ5R90pRdxTEZmn6qvPdzm'
  78. sudo: true
  79. # Get rid of an old entry.
  80. - ldap_entry: dn='ou=stuff,dc=example,dc=com' state=absent server_uri='ldap://localhost/' bind_dn='cn=admin,dc=example,dc=com' bind_pw=password
  81. """
  82. def main():
  83. module = AnsibleModule(
  84. argument_spec={
  85. 'dn': dict(required=True),
  86. 'state': dict(default='present', choices=['present', 'absent']),
  87. 'server_uri': dict(default='ldapi:///'),
  88. 'start_tls': dict(default='false', choices=(list(BOOLEANS)+['True', True, 'False', False])),
  89. 'bind_dn': dict(default=None),
  90. 'bind_pw': dict(default='', no_log=True),
  91. },
  92. check_invalid_arguments=False,
  93. supports_check_mode=True,
  94. )
  95. try:
  96. LdapEntry(module).main()
  97. except ldap.LDAPError, e:
  98. module.fail_json(msg=str(e), exc=format_exc())
  99. class LdapEntry(object):
  100. _connection = None
  101. def __init__(self, module):
  102. self.module = module
  103. # python-ldap doesn't understand unicode strings. Parameters that are
  104. # just going to get passed to python-ldap APIs are stored as utf-8.
  105. self.dn = self._utf8_param('dn')
  106. self.state = self.module.params['state']
  107. self.server_uri = self.module.params['server_uri']
  108. self.start_tls = self.module.boolean(self.module.params['start_tls'])
  109. self.bind_dn = self._utf8_param('bind_dn')
  110. self.bind_pw = self._utf8_param('bind_pw')
  111. self.attrs = {}
  112. self._load_attrs()
  113. if (self.state == 'present') and ('objectClass' not in self.attrs):
  114. self.module.fail_json(msg="When state=present, at least one objectClass must be provided")
  115. def _utf8_param(self, name):
  116. return self._force_utf8(self.module.params[name])
  117. def _load_attrs(self):
  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 _force_utf8(self, value):
  130. """ If value is unicode, encode to utf-8. """
  131. if isinstance(value, unicode):
  132. value = value.encode('utf-8')
  133. return value
  134. def main(self):
  135. if self.state == 'present':
  136. action = self.handle_present()
  137. elif self.state == 'absent':
  138. action = self.handle_absent()
  139. else:
  140. action = None
  141. if (action is not None) and (not self.module.check_mode):
  142. action()
  143. self.module.exit_json(changed=(action is not None))
  144. #
  145. # State Implementations
  146. #
  147. def handle_present(self):
  148. """ If self.dn does not exist, returns a callable that will add it. """
  149. if not self.is_entry_present():
  150. modlist = ldap.modlist.addModlist(self.attrs)
  151. action = lambda: self.connection.add_s(self.dn, modlist)
  152. else:
  153. action = None
  154. return action
  155. def handle_absent(self):
  156. """ If self.dn exists, returns a callable that will delete it. """
  157. if self.is_entry_present():
  158. action = lambda: self.connection.delete_s(self.dn)
  159. else:
  160. action = None
  161. return action
  162. #
  163. # Util
  164. #
  165. def is_entry_present(self):
  166. try:
  167. self.connection.search_s(self.dn, ldap.SCOPE_BASE)
  168. except ldap.NO_SUCH_OBJECT:
  169. is_present = False
  170. else:
  171. is_present = True
  172. return is_present
  173. #
  174. # LDAP Connection
  175. #
  176. @property
  177. def connection(self):
  178. """ An authenticated connection to the LDAP server (cached). """
  179. if self._connection is None:
  180. self._connection = self._connect_to_ldap()
  181. return self._connection
  182. def _connect_to_ldap(self):
  183. connection = ldap.initialize(self.server_uri)
  184. if self.start_tls:
  185. connection.start_tls_s()
  186. if self.bind_dn is not None:
  187. connection.simple_bind_s(self.bind_dn, self.bind_pw)
  188. else:
  189. connection.sasl_interactive_bind_s('', ldap.sasl.external())
  190. return connection
  191. from ansible.module_utils.basic import * # noqa
  192. main()