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.

232 lines
7.2KB

  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_search
  9. short_description: Return the results of an LDAP search.
  10. description:
  11. - Return the results of an LDAP search. Use in combination with
  12. Ansible's 'register' statement.
  13. notes: []
  14. version_added: null
  15. author: Dhruv Bansal
  16. requirements:
  17. - python-ldap
  18. options:
  19. base:
  20. required: true
  21. description:
  22. - The base to search from.
  23. scope:
  24. required: false
  25. choices: [base, onelevel, subordinate, children]
  26. default: base
  27. description:
  28. - The LDAP scope to use when searching.
  29. filter:
  30. required: false
  31. default: '(objectClass=*)'
  32. description:
  33. - The filter to apply to the search.
  34. attrs:
  35. required: false
  36. default: none
  37. description:
  38. - A list of attrs to limit the results to. Can be an
  39. actual list or just a comma-separated string.
  40. schema:
  41. required: false
  42. default: false
  43. description:
  44. - Return the full attribute schema of entries, not their
  45. attribute values. Overrides C(attrs) when given.
  46. server_uri:
  47. required: false
  48. default: ldapi:///
  49. description:
  50. - A URI to the LDAP server. The default value lets the underlying
  51. LDAP client library look for a UNIX domain socket in its default
  52. location.
  53. start_tls:
  54. required: false
  55. default: false
  56. description:
  57. - If true, we'll use the START_TLS LDAP extension.
  58. bind_dn:
  59. required: false
  60. description:
  61. - A DN to bind with. If this is omitted, we'll try a SASL bind with
  62. the EXTERNAL mechanism. If this is blank, we'll use an anonymous
  63. bind.
  64. bind_pw:
  65. required: false
  66. description:
  67. - The password to use with C(bind_dn).
  68. """
  69. EXAMPLES = """
  70. # Return all entries within the 'groups' organizational unit.
  71. - ldap_search: base='ou=groups,dc=example,dc=com'
  72. register: ldap_groups
  73. sudo: true
  74. # Return GIDs for all groups
  75. - ldap_entry: base='ou=groups,dc=example,dc=com' scope=onelevel attrs="gidNumber"
  76. register: ldap_group_gids
  77. sudo: true
  78. """
  79. def main():
  80. module = AnsibleModule(
  81. argument_spec={
  82. 'base': dict(required=True),
  83. 'scope': dict(default='base', choices=['base', 'onelevel', 'subordinate', 'children']),
  84. 'filter': dict(default='(objectClass=*)'),
  85. 'attrs': dict(default=None),
  86. 'schema': dict(default=False, choices=(list(BOOLEANS)+['True', True, 'False', False])),
  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=False,
  94. )
  95. try:
  96. LdapSearch(module).main()
  97. except ldap.LDAPError, e:
  98. module.fail_json(msg=str(e), exc=format_exc())
  99. class LdapSearch(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.base = self._utf8_param('base')
  106. self.filterstr = self._utf8_param('filter')
  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.attrlist = []
  112. self._load_scope()
  113. self._load_attrs()
  114. self._load_schema()
  115. # if (self.state == 'present') and ('objectClass' not in self.attrs):
  116. # self.module.fail_json(msg="When state=present, at least one objectClass must be provided")
  117. def _utf8_param(self, name):
  118. return self._force_utf8(self.module.params[name])
  119. def _load_schema(self):
  120. self.schema = self.module.boolean(self.module.params['schema'])
  121. if self.schema:
  122. self.attrsonly = 1
  123. else:
  124. self.attrsonly = 0
  125. def _load_scope(self):
  126. scope = self.module.params['scope']
  127. if scope == 'base': self.scope = ldap.SCOPE_BASE
  128. elif scope == 'onelevel': self.scope = ldap.SCOPE_ONELEVEL
  129. elif scope == 'subordinate': self.scope = ldap.SCOPE_SUBORDINATE
  130. elif scope == 'children': self.scope = ldap.SCOPE_SUBTREE
  131. else:
  132. self.module.fail_json(msg="scope must be one of: base, onelevel, subordinate, children")
  133. def _load_attrs(self):
  134. if self.module.params['attrs'] is None:
  135. self.attrlist = None
  136. else:
  137. attrs = self._load_attr_values(self.module.params['attrs'])
  138. if len(attrs) > 0:
  139. self.attrlist = attrs
  140. else:
  141. self.attrlist = None
  142. def _load_attr_values(self, raw):
  143. if isinstance(raw, basestring):
  144. values = raw.split(',')
  145. else:
  146. values = raw
  147. if not (isinstance(values, list) and all(isinstance(value, basestring) for value in values)):
  148. self.module.fail_json(msg="attrs must be a string or list of strings.")
  149. return map(self._force_utf8, values)
  150. def _force_utf8(self, value):
  151. """ If value is unicode, encode to utf-8. """
  152. if isinstance(value, unicode):
  153. value = value.encode('utf-8')
  154. return value
  155. def main(self):
  156. results = self.perform_search()
  157. self.module.exit_json(changed=True, results=results)
  158. def perform_search(self):
  159. try:
  160. results = self.connection.search_s(self.base, self.scope, filterstr=self.filterstr, attrlist=self.attrlist, attrsonly=self.attrsonly)
  161. if self.schema:
  162. return [dict(dn=result[0],attrs=result[1].keys()) for result in results]
  163. else:
  164. return [self._extract_entry(result[0], result[1]) for result in results]
  165. except ldap.NO_SUCH_OBJECT:
  166. self.module.fail_json(msg="Base not found: {}".format(self.base))
  167. def _extract_entry(self, dn, attrs):
  168. extracted = {'dn': dn}
  169. for attr, val in attrs.iteritems():
  170. if len(val) == 1:
  171. extracted[attr] = val[0]
  172. else:
  173. extracted[attr] = val
  174. return extracted
  175. #
  176. # LDAP Connection
  177. #
  178. @property
  179. def connection(self):
  180. """ An authenticated connection to the LDAP server (cached). """
  181. if self._connection is None:
  182. self._connection = self._connect_to_ldap()
  183. return self._connection
  184. def _connect_to_ldap(self):
  185. connection = ldap.initialize(self.server_uri)
  186. if self.start_tls:
  187. connection.start_tls_s()
  188. if self.bind_dn is not None:
  189. connection.simple_bind_s(self.bind_dn, self.bind_pw)
  190. else:
  191. connection.sasl_interactive_bind_s('', ldap.sasl.external())
  192. return connection
  193. from ansible.module_utils.basic import * # noqa
  194. main()