ldap_access.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482
  1. #!/usr/bin/env python
  2. # Licensed to Cloudera, Inc. under one
  3. # or more contributor license agreements. See the NOTICE file
  4. # distributed with this work for additional information
  5. # regarding copyright ownership. Cloudera, Inc. licenses this file
  6. # to you under the Apache License, Version 2.0 (the
  7. # "License"); you may not use this file except in compliance
  8. # with the License. You may obtain a copy of the License at
  9. #
  10. # http://www.apache.org/licenses/LICENSE-2.0
  11. #
  12. # Unless required by applicable law or agreed to in writing, software
  13. # distributed under the License is distributed on an "AS IS" BASIS,
  14. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  15. # See the License for the specific language governing permissions and
  16. # limitations under the License.
  17. """
  18. This module provides access to LDAP servers, along with some basic functionality required for Hue and
  19. User Admin to work seamlessly with LDAP.
  20. """
  21. from builtins import str, object
  22. import logging
  23. LOG = logging.getLogger(__name__)
  24. try:
  25. import ldap
  26. import ldap.filter
  27. from ldap import SCOPE_SUBTREE
  28. except ImportError:
  29. LOG.warn('ldap module not found')
  30. SCOPE_SUBTREE = None
  31. import re
  32. from django.utils.encoding import smart_text
  33. import desktop.conf
  34. from desktop.lib.python_util import CaseInsensitiveDict
  35. from useradmin.models import User
  36. CACHED_LDAP_CONN = None
  37. class LdapBindException(Exception):
  38. pass
  39. class LdapSearchException(Exception):
  40. pass
  41. def get_connection_from_server(server=None):
  42. ldap_servers = desktop.conf.LDAP.LDAP_SERVERS.get()
  43. if server and ldap_servers:
  44. ldap_config = ldap_servers[server]
  45. else:
  46. ldap_config = desktop.conf.LDAP
  47. return get_connection(ldap_config)
  48. def get_connection(ldap_config):
  49. global CACHED_LDAP_CONN
  50. if CACHED_LDAP_CONN is not None:
  51. return CACHED_LDAP_CONN
  52. ldap_url = ldap_config.LDAP_URL.get()
  53. username = ldap_config.BIND_DN.get()
  54. password = desktop.conf.get_ldap_bind_password(ldap_config)
  55. ldap_cert = ldap_config.LDAP_CERT.get()
  56. search_bind_authentication = ldap_config.SEARCH_BIND_AUTHENTICATION.get()
  57. if ldap_url is None:
  58. raise Exception('No LDAP URL was specified')
  59. if search_bind_authentication:
  60. return LdapConnection(ldap_config, ldap_url, username, password, ldap_cert)
  61. else:
  62. return LdapConnection(ldap_config, ldap_url, get_ldap_username(username, ldap_config.NT_DOMAIN.get()), password, ldap_cert)
  63. def get_auth(ldap_config):
  64. ldap_url = ldap_config.LDAP_URL.get()
  65. if ldap_url is None:
  66. raise Exception('No LDAP URL was specified')
  67. username = ldap_config.BIND_DN.get()
  68. password = ldap_config.BIND_PASSWORD.get()
  69. if not password:
  70. password = ldap_config.BIND_PASSWORD_SCRIPT.get()
  71. ldap_cert = ldap_config.LDAP_CERT.get()
  72. search_bind_authentication = ldap_config.SEARCH_BIND_AUTHENTICATION.get()
  73. if search_bind_authentication:
  74. ldap_conf = (ldap_url, username, password, ldap_cert)
  75. else:
  76. ldap_conf = (ldap_url, get_ldap_username(username, ldap_config.NT_DOMAIN.get()), password, ldap_cert)
  77. return ldap_conf
  78. def get_ldap_username(username, nt_domain):
  79. if nt_domain:
  80. return '%s@%s' % (username, nt_domain)
  81. else:
  82. return username
  83. def get_ldap_user_kwargs(username):
  84. if desktop.conf.LDAP.IGNORE_USERNAME_CASE.get():
  85. return {
  86. 'username__iexact': username
  87. }
  88. else:
  89. return {
  90. 'username': username
  91. }
  92. def get_ldap_user(username):
  93. username_kwargs = get_ldap_user_kwargs(username)
  94. return User.objects.get(**username_kwargs)
  95. def get_or_create_ldap_user(username):
  96. username_kwargs = get_ldap_user_kwargs(username)
  97. users = User.objects.filter(**username_kwargs)
  98. if users.exists():
  99. return User.objects.get(**username_kwargs), False
  100. else:
  101. if desktop.conf.LDAP.FORCE_USERNAME_LOWERCASE.get():
  102. username = username.lower()
  103. elif desktop.conf.LDAP.FORCE_USERNAME_UPPERCASE.get():
  104. username = username.upper()
  105. return User.objects.create(username=username), True
  106. class LdapConnection(object):
  107. """
  108. Constructor creates LDAP connection. Contains methods
  109. to easily query an LDAP server.
  110. """
  111. def __init__(self, ldap_config, ldap_url, bind_user=None, bind_password=None, cert_file=None):
  112. """
  113. Constructor initializes the LDAP connection
  114. """
  115. self.ldap_config = ldap_config
  116. self._ldap_url = ldap_url
  117. self._username = bind_user
  118. self._ldap_cert = cert_file
  119. # Certificate-related config settings
  120. if ldap_config.LDAP_CERT.get():
  121. ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_DEMAND)
  122. ldap.set_option(ldap.OPT_X_TLS_CACERTFILE, ldap_config.LDAP_CERT.get())
  123. else:
  124. ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_NEVER)
  125. if self.ldap_config.FOLLOW_REFERRALS.get():
  126. ldap.set_option(ldap.OPT_REFERRALS, 1)
  127. else:
  128. ldap.set_option(ldap.OPT_REFERRALS, 0)
  129. if ldap_config.DEBUG.get():
  130. ldap.set_option(ldap.OPT_DEBUG_LEVEL, ldap_config.DEBUG_LEVEL.get())
  131. self.ldap_handle = ldap.initialize(uri=ldap_url, trace_level=ldap_config.TRACE_LEVEL.get())
  132. if self.ldap_config.USE_START_TLS.get() and not ldap_url.lower().startswith('ldaps'):
  133. self.ldap_handle.start_tls_s()
  134. if bind_user:
  135. try:
  136. self.ldap_handle.simple_bind_s(bind_user, bind_password)
  137. except Exception as e:
  138. self.handle_bind_exception(e, bind_user)
  139. else:
  140. try:
  141. # Do anonymous bind
  142. self.ldap_handle.simple_bind_s('', '')
  143. except Exception as e:
  144. self.handle_bind_exception(e)
  145. def handle_bind_exception(self, exception, bind_user=None):
  146. LOG.error("LDAP access bind error: %s" % exception)
  147. if 'Can\'t contact LDAP server' in str(exception):
  148. msg = "Can\'t contact LDAP server"
  149. else:
  150. if bind_user:
  151. msg = "Failed to bind to LDAP server as user %s" % bind_user
  152. else:
  153. msg = "Failed to bind to LDAP server anonymously"
  154. raise LdapBindException(msg)
  155. def _get_search_params(self, name, attr, find_by_dn=False):
  156. """
  157. if we are to find this ldap object by full distinguished name,
  158. then search by setting search_dn to the 'name'
  159. rather than by filtering by 'attr'.
  160. """
  161. base_dn = self._get_root_dn()
  162. if find_by_dn:
  163. search_dn = re.sub(r'(\w+=)', lambda match: match.group(0).upper(), name)
  164. if not search_dn.upper().endswith(base_dn.upper()):
  165. raise LdapSearchException("Distinguished Name provided does not contain configured Base DN. Base DN: %(base_dn)s, DN: %(dn)s" % {
  166. 'base_dn': base_dn,
  167. 'dn': search_dn
  168. })
  169. return (search_dn, '')
  170. else:
  171. return (base_dn, '(' + attr + '=' + name + ')')
  172. @classmethod
  173. def _transform_find_user_results(cls, result_data, user_name_attr):
  174. """
  175. :param result_data: List of dictionaries that have ldap attributes and their associated values.
  176. Generally the result list from an ldapsearch request.
  177. :param user_name_attr: The ldap attribute that is returned by the server to map to ``username``
  178. in the return dictionary.
  179. :returns list of dictionaries that take on the following form: {
  180. 'dn': <distinguished name of entry>,
  181. 'username': <ldap attribute associated with user_name_attr>
  182. 'first': <first name>
  183. 'last': <last name>
  184. 'email': <email>
  185. 'groups': <list of DNs of groups that user is a member of>
  186. }
  187. """
  188. user_info = []
  189. if result_data:
  190. for dn, data in result_data:
  191. # Skip Active Directory # refldap entries.
  192. if dn is not None:
  193. # Case insensitivity
  194. data = CaseInsensitiveDict.from_dict(data)
  195. # Skip unnamed entries.
  196. if user_name_attr not in data:
  197. LOG.warn('Could not find %s in ldap attributes' % user_name_attr)
  198. continue
  199. ldap_info = {
  200. 'dn': dn,
  201. 'username': smart_text(data[user_name_attr][0])
  202. }
  203. if 'givenName' in data:
  204. first_name = smart_text(data['givenName'][0])
  205. if len(first_name) > 30:
  206. LOG.warn('First name is truncated to 30 characters for [<User: %s>].' % ldap_info['username'])
  207. ldap_info['first'] = first_name[:30]
  208. if 'sn' in data:
  209. last_name = smart_text(data['sn'][0])
  210. if len(last_name) > 30:
  211. LOG.warn('Last name is truncated to 30 characters for [<User: %s>].' % ldap_info['username'])
  212. ldap_info['last'] = last_name[:30]
  213. if 'mail' in data:
  214. ldap_info['email'] = smart_text(data['mail'][0])
  215. # memberOf and isMemberOf should be the same if they both exist
  216. if 'memberOf' in data:
  217. ldap_info['groups'] = [smart_text(member) for member in data['memberOf']]
  218. if 'isMemberOf' in data:
  219. ldap_info['groups'] = [smart_text(member) for member in data['isMemberOf']]
  220. user_info.append(ldap_info)
  221. return user_info
  222. def _transform_find_group_results(self, result_data, group_name_attr, group_member_attr):
  223. group_info = []
  224. if result_data:
  225. for dn, data in result_data:
  226. # Skip Active Directory # refldap entries.
  227. if dn is not None:
  228. # Case insensitivity
  229. data = CaseInsensitiveDict.from_dict(data)
  230. # Skip unnamed entries.
  231. if group_name_attr not in data:
  232. LOG.warn('Could not find %s in ldap attributes' % group_name_attr)
  233. continue
  234. group_name = data[group_name_attr][0]
  235. if desktop.conf.LDAP.FORCE_USERNAME_LOWERCASE.get():
  236. group_name = group_name.lower()
  237. elif desktop.conf.LDAP.FORCE_USERNAME_UPPERCASE.get():
  238. group_name = group_name.upper()
  239. ldap_info = {
  240. 'dn': dn,
  241. 'name': group_name
  242. }
  243. if group_member_attr in data and group_member_attr.lower() != 'memberuid':
  244. ldap_info['members'] = data[group_member_attr]
  245. else:
  246. LOG.warn('Skipping import of non-posix users from group %s since group_member_attr '
  247. 'is memberUid or group did not contain any members' % group_name)
  248. ldap_info['members'] = []
  249. if 'posixgroup' in (item.lower() for item in data['objectClass']) and 'memberUid' in data:
  250. ldap_info['posix_members'] = data['memberUid']
  251. else:
  252. LOG.warn('Skipping import of posix users from group %s since posixGroup '
  253. 'not an objectClass or no memberUids found' % group_name)
  254. ldap_info['posix_members'] = []
  255. group_info.append(ldap_info)
  256. return group_info
  257. def find_users(self, username_pattern, search_attr=None, user_name_attr=None, user_filter=None, find_by_dn=False, scope=SCOPE_SUBTREE):
  258. """
  259. LDAP search helper method finding users. This supports searching for users
  260. by distinguished name, or the configured username attribute.
  261. :param username_pattern: The pattern to match ``search_attr`` against. Defaults to ``search_attr`` if none.
  262. :param search_attr: The ldap attribute to search for ``username_pattern``. Defaults to LDAP -> USERS -> USER_NAME_ATTR config value.
  263. :param user_name_attr: The ldap attribute that is returned by the server to map to ``username`` in the return dictionary.
  264. :param find_by_dn: Search by distinguished name.
  265. :param scope: ldapsearch scope.
  266. :returns: List of dictionaries that take on the following form: {
  267. 'dn': <distinguished name of entry>,
  268. 'username': <ldap attribute associated with user_name_attr>
  269. 'first': <first name>
  270. 'last': <last name>
  271. 'email': <email>
  272. 'groups': <list of DNs of groups that user is a member of>
  273. }
  274. ``
  275. """
  276. if not search_attr:
  277. search_attr = self.ldap_config.USERS.USER_NAME_ATTR.get()
  278. if not user_name_attr:
  279. user_name_attr = search_attr
  280. if not user_filter:
  281. user_filter = self.ldap_config.USERS.USER_FILTER.get()
  282. if not user_filter.startswith('('):
  283. user_filter = '(' + user_filter + ')'
  284. # Allow wild cards on non distinguished names
  285. sanitized_name = ldap.filter.escape_filter_chars(username_pattern).replace(r'\2a', r'*')
  286. # Fix issue where \, is converted to \5c,
  287. sanitized_name = sanitized_name.replace(r'\5c,', r'\2c')
  288. search_dn, user_name_filter = self._get_search_params(sanitized_name, search_attr, find_by_dn)
  289. ldap_filter = user_filter
  290. if user_name_filter:
  291. if ldap_filter.lower() in ('(objectclass=*)', 'objectclass=*'):
  292. ldap_filter = ''
  293. ldap_filter = '(&' + ldap_filter + user_name_filter + ')'
  294. attrlist = ['objectClass', 'isMemberOf', 'memberOf', 'givenName', 'sn', 'mail', 'dn', user_name_attr]
  295. self._search_dn = search_dn
  296. self._ldap_filter = ldap_filter
  297. self._attrlist = attrlist
  298. try:
  299. ldap_result_id = self.ldap_handle.search(search_dn, scope, ldap_filter, attrlist)
  300. result_type, result_data = self.ldap_handle.result(ldap_result_id)
  301. if result_type == ldap.RES_SEARCH_RESULT:
  302. return self._transform_find_user_results(result_data, user_name_attr)
  303. else:
  304. return []
  305. except ldap.LDAPError as e:
  306. LOG.warn("LDAP Error: %s" % e)
  307. return None
  308. def find_groups(self, groupname_pattern, search_attr=None, group_name_attr=None,
  309. group_member_attr=None, group_filter=None, find_by_dn=False, scope=SCOPE_SUBTREE):
  310. """
  311. LDAP search helper method for finding groups
  312. :param groupname_pattern: The pattern to match ``search_attr`` against. Defaults to ``search_attr`` if none.
  313. :param search_attr: The ldap attribute to search for ``groupname_pattern``. Defaults to LDAP -> GROUPS -> GROUP_NAME_ATTR config value.
  314. :param group_name_attr: The ldap attribute that is returned by the server to map to ``name`` in the return dictionary.
  315. :param find_by_dn: Search by distinguished name.
  316. :param scope: ldapsearch scope.
  317. :returns: List of dictionaries that take on the following form: {
  318. 'dn': <distinguished name of entry>,
  319. 'name': <ldap attribute associated with group_name_attr>
  320. 'first': <first name>
  321. 'last': <last name>
  322. 'email': <email>
  323. 'groups': <list of DNs of groups that user is a member of>
  324. }
  325. """
  326. if not search_attr:
  327. search_attr = self.ldap_config.GROUPS.GROUP_NAME_ATTR.get()
  328. if not group_name_attr:
  329. group_name_attr = search_attr
  330. if not group_member_attr:
  331. group_member_attr = self.ldap_config.GROUPS.GROUP_MEMBER_ATTR.get()
  332. if not group_filter:
  333. group_filter = self.ldap_config.GROUPS.GROUP_FILTER.get()
  334. if not group_filter.startswith('('):
  335. group_filter = '(' + group_filter + ')'
  336. # Allow wild cards on non distinguished names
  337. sanitized_name = ldap.filter.escape_filter_chars(groupname_pattern).replace(r'\2a', r'*')
  338. # Fix issue where \, is converted to \5c,
  339. sanitized_name = sanitized_name.replace(r'\5c,', r'\2c')
  340. search_dn, group_name_filter = self._get_search_params(sanitized_name, search_attr, find_by_dn)
  341. ldap_filter = '(&' + group_filter + group_name_filter + ')'
  342. attrlist = ['objectClass', 'dn', 'memberUid', group_member_attr, group_name_attr]
  343. self._search_dn = search_dn
  344. self._ldap_filter = ldap_filter
  345. self._attrlist = attrlist
  346. ldap_result_id = self.ldap_handle.search(search_dn, scope, ldap_filter, attrlist)
  347. result_type, result_data = self.ldap_handle.result(ldap_result_id)
  348. if result_type == ldap.RES_SEARCH_RESULT:
  349. return self._transform_find_group_results(result_data, group_name_attr, group_member_attr)
  350. else:
  351. return []
  352. def find_members_of_group(self, dn, search_attr, ldap_filter, scope=SCOPE_SUBTREE):
  353. if ldap_filter and not ldap_filter.startswith('('):
  354. ldap_filter = '(' + ldap_filter + ')'
  355. # Allow wild cards on non distinguished names
  356. dn = ldap.filter.escape_filter_chars(dn).replace(r'\2a', r'*')
  357. # Fix issue where \, is converted to \5c,
  358. dn = dn.replace(r'\5c,', r'\2c')
  359. search_dn, _ = self._get_search_params(dn, search_attr)
  360. ldap_filter = '(&%(ldap_filter)s(|(isMemberOf=%(group_dn)s)(memberOf=%(group_dn)s)))' % {'group_dn': dn, 'ldap_filter': ldap_filter}
  361. attrlist = ['objectClass', 'isMemberOf', 'memberOf', 'givenName', 'sn', 'mail', 'dn', search_attr]
  362. self._search_dn = search_dn
  363. self._ldap_filter = ldap_filter
  364. self._attrlist = attrlist
  365. ldap_result_id = self.ldap_handle.search(search_dn, scope, ldap_filter, attrlist)
  366. result_type, result_data = self.ldap_handle.result(ldap_result_id)
  367. if result_type == ldap.RES_SEARCH_RESULT:
  368. return result_data
  369. else:
  370. return []
  371. def find_users_of_group(self, dn):
  372. ldap_filter = self.ldap_config.USERS.USER_FILTER.get()
  373. name_attr = self.ldap_config.USERS.USER_NAME_ATTR.get()
  374. result_data = self.find_members_of_group(dn, name_attr, ldap_filter)
  375. return self._transform_find_user_results(result_data, name_attr)
  376. def find_groups_of_group(self, dn):
  377. ldap_filter = self.ldap_config.GROUPS.GROUP_FILTER.get()
  378. name_attr = self.ldap_config.GROUPS.GROUP_NAME_ATTR.get()
  379. member_attr = self.ldap_config.GROUPS.GROUP_MEMBER_ATTR.get()
  380. result_data = self.find_members_of_group(dn, name_attr, ldap_filter)
  381. return self._transform_find_group_results(result_data, name_attr, member_attr)
  382. def _get_root_dn(self):
  383. return self.ldap_config.BASE_DN.get()
  384. def ldapsearch_cmd(self):
  385. ldapsearch = 'ldapsearch -x -LLL -H {ldap_url} -D "{binddn}" -w "********" -b "{base}" ' \
  386. '"{filterstring}" {attr}'.format(ldap_url=self._ldap_url,
  387. binddn=self._username,
  388. base=self._search_dn,
  389. filterstring=self._ldap_filter,
  390. attr=" ".join(self._attrlist))
  391. return ldapsearch