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.

289 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=(list(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. # Parameters that we have to directly pass to python-ldap need
  135. # to converted to UTF-8 first, as python-ldap doesn't
  136. # understand unicode strings.
  137. # Server parameters
  138. self.server_uri = self.module.params['server_uri']
  139. self.start_tls = self.module.boolean(self.module.params['start_tls'])
  140. self.bind_dn = self._utf8_param('bind_dn')
  141. self.bind_pw = self._utf8_param('bind_pw')
  142. # Attribute parameters
  143. self.dn = self._utf8_param('dn')
  144. self.name = self._utf8_param('name')
  145. self.values = self._normalized_values()
  146. self.state = self.module.params['state']
  147. self._connection = None
  148. def _force_utf8(self, value):
  149. """If value is Unicode, encode to UTF-8."""
  150. if isinstance(value, unicode):
  151. return value.encode('utf-8')
  152. return value
  153. def _utf8_param(self, name):
  154. """Extract a parameter as UTF-8."""
  155. return self._force_utf8(self.module.params[name])
  156. def _normalized_values(self):
  157. """Parses the 'values' parameter into a list of UTF-8 strings."""
  158. values = self.module.params['values']
  159. if isinstance(values, basestring):
  160. if values == '':
  161. values = []
  162. else:
  163. values = [values]
  164. if not (isinstance(values, list) and all(isinstance(value, basestring) for value in values)):
  165. self.module.fail_json(msg="values must be a string or list of strings.")
  166. return list(map(self._force_utf8, values))
  167. def main(self):
  168. if self.state == 'present':
  169. modlist = self.handle_present()
  170. elif self.state == 'absent':
  171. modlist = self.handle_absent()
  172. elif self.state == 'exact':
  173. modlist = self.handle_exact()
  174. else:
  175. modlist = []
  176. if len(modlist) > 0:
  177. changed = True
  178. if not self.module.check_mode:
  179. self.connection.modify_s(self.dn, modlist)
  180. else:
  181. changed = False
  182. self.module.exit_json(changed=changed, modlist=modlist)
  183. #
  184. # State Implementations
  185. #
  186. def handle_present(self):
  187. values_to_add = filter(self.is_value_absent, self.values)
  188. if len(values_to_add) > 0:
  189. return [(ldap.MOD_ADD, self.name, values_to_add)]
  190. else:
  191. return []
  192. def handle_absent(self):
  193. values_to_delete = filter(self.is_value_present, self.values)
  194. if len(values_to_delete) > 0:
  195. return [(ldap.MOD_DELETE, self.name, values_to_delete)]
  196. else:
  197. return []
  198. def handle_exact(self):
  199. current = self.current_values()
  200. if frozenset(self.values) != frozenset(current):
  201. if len(current) == 0:
  202. return [(ldap.MOD_ADD, self.name, self.values)]
  203. elif len(self.values) == 0:
  204. return [(ldap.MOD_DELETE, self.name, None)]
  205. else:
  206. return [(ldap.MOD_REPLACE, self.name, self.values)]
  207. return []
  208. #
  209. # Util
  210. #
  211. def is_value_present(self, value):
  212. """True if the target attribute has the given value."""
  213. try:
  214. return bool(self.connection.compare_s(self.dn, self.name, value))
  215. except ldap.NO_SUCH_ATTRIBUTE:
  216. return False
  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()