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.

294 lines
9.3KB

  1. #!/usr/bin/env python
  2. from traceback import format_exc
  3. import ldap
  4. import ldap.sasl
  5. DOCUMENTATION = """
  6. ---
  7. module: ldap_attr
  8. short_description: Add or remove LDAP attribute values.
  9. description:
  10. - Add or remove LDAP attribute values.
  11. notes:
  12. - This only deals with attributes on existing entries. To add or remove
  13. whole entries, see M(ldap_entry).
  14. - The default authentication settings will attempt to use a SASL EXTERNAL
  15. bind over a UNIX domain socket. This works well with the default Ubuntu
  16. install for example, which includes a cn=peercred,cn=external,cn=auth ACL
  17. rule allowing root to modify the server configuration. If you need to use
  18. a simple bind to access your server, pass the credentials in C(bind_dn)
  19. and C(bind_pw).
  20. - For C(state=present) and C(state=absent), all value comparisons are
  21. performed on the server for maximum accuracy. For C(state=exact), values
  22. have to be compared in Python, which obviously ignores LDAP matching
  23. rules. This should work out in most cases, but it is theoretically
  24. possible to see spurious changes when target and actual values are
  25. semantically identical but lexically distinct.
  26. version_added: null
  27. author: Peter Sagerson
  28. requirements:
  29. - python-ldap
  30. options:
  31. dn:
  32. required: true
  33. description:
  34. - The DN of the entry to modify.
  35. name:
  36. required: true
  37. description:
  38. - The name of the attribute to modify.
  39. values:
  40. required: true
  41. description:
  42. - The value(s) to add or remove. This can be a string or a list of
  43. strings. The complex argument format is required in order to pass
  44. a list of strings (see examples).
  45. state:
  46. required: false
  47. choices: [present, absent, exact]
  48. default: present
  49. description:
  50. - The state of the attribute values. If C(present), all given
  51. values will be added if they're missing. If C(absent), all given
  52. values will be removed if present. If C(exact), the set of values
  53. will be forced to exactly those provided and no others. If
  54. C(state=exact) and C(values) is empty, all values for this
  55. attribute will be removed.
  56. server_uri:
  57. required: false
  58. default: ldapi:///
  59. description:
  60. - A URI to the LDAP server. The default value lets the underlying
  61. LDAP client library look for a UNIX domain socket in its default
  62. location.
  63. start_tls:
  64. required: false
  65. default: false
  66. description:
  67. - If true, we'll use the START_TLS LDAP extension.
  68. bind_dn:
  69. required: false
  70. description:
  71. - A DN to bind with. If this is omitted, we'll try a SASL bind with
  72. the EXTERNAL mechanism (see note). If this is blank, we'll use an
  73. anonymous bind.
  74. bind_pw:
  75. required: false
  76. description:
  77. - The password to use with C(bind_dn).
  78. """
  79. EXAMPLES = """
  80. # Configure directory number 1 for example.com.
  81. - ldap_attr: dn='olcDatabase={1}hdb,cn=config' name=olcSuffix values='dc=example,dc=com' state=exact
  82. sudo: true
  83. # Set up the ACL. The complex argument format is required here to pass a list
  84. # of ACL strings.
  85. - ldap_attr:
  86. sudo: true
  87. args:
  88. dn: olcDatabase={1}hdb,cn=config
  89. name: olcAccess
  90. values:
  91. - '{0}to attrs=userPassword,shadowLastChange
  92. by self write
  93. by anonymous auth
  94. by dn="cn=admin,dc=example,dc=com" write
  95. by * none'
  96. - '{1}to dn.base="dc=example,dc=com"
  97. by dn="cn=admin,dc=example,dc=com" write
  98. by * read'
  99. state: exact
  100. # Declare some indexes.
  101. - ldap_attr: dn='olcDatabase={1}hdb,cn=config' name=olcDbIndex values={{ item }}
  102. sudo: true
  103. with_items:
  104. - objectClass eq
  105. - uid eq
  106. # Set up a root user, which we can use later to bootstrap the directory.
  107. - ldap_attr: dn='olcDatabase={1}hdb,cn=config' name={{ item.key }} values={{ item.value }} state=exact
  108. sudo: true
  109. with_dict:
  110. olcRootDN: 'cn=root,dc=example,dc=com'
  111. olcRootPW: '{SSHA}mRskON0Stk+5wO5K+MMk2xmakKt8h7eJ'
  112. """
  113. def main():
  114. module = AnsibleModule(
  115. argument_spec={
  116. 'dn': dict(required=True),
  117. 'name': dict(required=True),
  118. 'values': dict(required=True),
  119. 'state': dict(default='present', choices=['present', 'absent', 'exact']),
  120. 'server_uri': dict(default='ldapi:///'),
  121. 'start_tls': dict(default='false', choices=(BOOLEANS+['True', True, 'False', False])),
  122. 'bind_dn': dict(default=None),
  123. 'bind_pw': dict(default='', no_log=True),
  124. },
  125. supports_check_mode=True,
  126. )
  127. try:
  128. LdapAttr(module).main()
  129. except ldap.LDAPError, e:
  130. module.fail_json(msg=str(e), exc=format_exc())
  131. class LdapAttr(object):
  132. def __init__(self, module):
  133. self.module = module
  134. # python-ldap doesn't understand unicode strings. Parameters that are
  135. # just going to get passed to python-ldap APIs are stored as utf-8.
  136. self.dn = self._utf8_param('dn')
  137. self.name = self._utf8_param('name')
  138. self.values = self._normalized_values()
  139. self.state = self.module.params['state']
  140. self.server_uri = self.module.params['server_uri']
  141. self.start_tls = self.module.boolean(self.module.params['start_tls'])
  142. self.bind_dn = self._utf8_param('bind_dn')
  143. self.bind_pw = self._utf8_param('bind_pw')
  144. self._connection = None
  145. def _utf8_param(self, name):
  146. return self._force_utf8(self.module.params[name])
  147. def _normalized_values(self):
  148. """ Parses the value parameter into a list of utf-8 strings. """
  149. values = self.module.params['values']
  150. if isinstance(values, basestring):
  151. if values == '':
  152. values = []
  153. else:
  154. values = [values]
  155. if not (isinstance(values, list) and all(isinstance(value, basestring) for value in values)):
  156. self.module.fail_json(msg="values must be a string or list of strings.")
  157. return map(self._force_utf8, values)
  158. def _force_utf8(self, value):
  159. """ If value is unicode, encode to utf-8. """
  160. if isinstance(value, unicode):
  161. value = value.encode('utf-8')
  162. return value
  163. def main(self):
  164. if self.state == 'present':
  165. modlist = self.handle_present()
  166. elif self.state == 'absent':
  167. modlist = self.handle_absent()
  168. elif self.state == 'exact':
  169. modlist = self.handle_exact()
  170. else:
  171. modlist = []
  172. if len(modlist) > 0:
  173. changed = True
  174. if not self.module.check_mode:
  175. self.connection.modify_s(self.dn, modlist)
  176. else:
  177. changed = False
  178. self.module.exit_json(changed=changed, modlist=modlist)
  179. #
  180. # State Implementations
  181. #
  182. def handle_present(self):
  183. values_to_add = filter(self.is_value_absent, self.values)
  184. if len(values_to_add) > 0:
  185. modlist = [(ldap.MOD_ADD, self.name, values_to_add)]
  186. else:
  187. modlist = []
  188. return modlist
  189. def handle_absent(self):
  190. values_to_delete = filter(self.is_value_present, self.values)
  191. if len(values_to_delete) > 0:
  192. modlist = [(ldap.MOD_DELETE, self.name, values_to_delete)]
  193. else:
  194. modlist = []
  195. return modlist
  196. def handle_exact(self):
  197. modlist = []
  198. current = self.current_values()
  199. if frozenset(self.values) != frozenset(current):
  200. if len(current) == 0:
  201. modlist = [(ldap.MOD_ADD, self.name, self.values)]
  202. elif len(self.values) == 0:
  203. modlist = [(ldap.MOD_DELETE, self.name, None)]
  204. else:
  205. modlist = [(ldap.MOD_REPLACE, self.name, self.values)]
  206. return modlist
  207. #
  208. # Util
  209. #
  210. def is_value_present(self, value):
  211. """ True if the target attribute has the given value. """
  212. try:
  213. is_present = bool(self.connection.compare_s(self.dn, self.name, value))
  214. except ldap.NO_SUCH_ATTRIBUTE:
  215. is_present = False
  216. return is_present
  217. def is_value_absent(self, value):
  218. """ True if the target attribute does not have the given value. """
  219. return (not self.is_value_present(value))
  220. def current_values(self):
  221. """ Returns the full list of values on the target attribute. """
  222. results = self.connection.search_s(self.dn, ldap.SCOPE_BASE, attrlist=[self.name])
  223. values = results[0][1].get(self.name, [])
  224. return values
  225. #
  226. # LDAP Connection
  227. #
  228. @property
  229. def connection(self):
  230. """ An authenticated connection to the LDAP server (cached). """
  231. if self._connection is None:
  232. self._connection = self._connect_to_ldap()
  233. return self._connection
  234. def _connect_to_ldap(self):
  235. connection = ldap.initialize(self.server_uri)
  236. if self.start_tls:
  237. connection.start_tls_s()
  238. if self.bind_dn is not None:
  239. connection.simple_bind_s(self.bind_dn, self.bind_pw)
  240. else:
  241. connection.sasl_interactive_bind_s('', ldap.sasl.external())
  242. return connection
  243. from ansible.module_utils.basic import * # noqa
  244. main()