Advertisement
Guest User

Untitled

a guest
Jun 16th, 2016
74
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 24.00 KB | None | 0 0
  1. # -*- coding: utf-8 -*-
  2. #
  3. # Copyright (C) 2007 John Hampton <pacopablo@pacopablo.com>
  4. # All rights reserved.
  5. #
  6. # This software is licensed as described in the file COPYING, which
  7. # you should have received as part of this distribution.
  8. #
  9. # Author: John Hampton <pacopablo@pacopablo.com>
  10. # Extended: Branson Matheson <branson.matheson@nasa.gov>
  11. # changed/added lines 308, 356 361 - Alvaro Reguly
  12.  
  13. import cPickle
  14. import hashlib
  15. import ldap
  16. import time
  17.  
  18. from trac.config import IntOption, Option
  19. from trac.core import Component, TracError, implements
  20. from trac.perm import IPermissionGroupProvider
  21. from trac.util.text import to_unicode
  22. from trac.util.translation import _
  23.  
  24. from acct_mgr.api import IPasswordStore
  25.  
  26. GROUP_PREFIX = '@'
  27. NOCACHE = 0
  28.  
  29. __all__ = ['DirAuthStore']
  30.  
  31.  
  32. def to_utf8(text):
  33. # Account for poor behavior of to_utf8 in Trac < 1.0.2
  34. if isinstance(text, unicode):
  35. return text.encode('utf-8')
  36. else:
  37. from trac.util.text import to_utf8
  38. return to_utf8(text)
  39.  
  40.  
  41. class DirAuthStore(Component):
  42. """Directory Password Store for Account Manager """
  43. implements(IPasswordStore, IPermissionGroupProvider)
  44.  
  45. dir_uri = Option('account-manager', 'dir_uri', 'ldap://localhost',
  46. "URI of the LDAP or Active Directory Server")
  47.  
  48. dir_charset = Option('account-manager', 'dir_charset', 'utf-8',
  49. "Text encoding used by the LDAP or Active "
  50. "Directory Server")
  51.  
  52. dir_scope = IntOption('account-manager', 'dir_scope', 1,
  53. "0=Base, 1=OneLevel, 2=Subtree")
  54.  
  55. dir_binddn = Option('account-manager', 'dir_binddn', '',
  56. "DN used to bind to AD, leave blank for "
  57. "anonymous bind")
  58.  
  59. dir_bindpw = Option('account-manager', 'dir_bindpw', '',
  60. "Password used when binding to AD, leave blank for "
  61. "anonymous bind")
  62.  
  63. dir_timeout = IntOption('account-manager', 'dir_timeout', 5,
  64. "ldap response timeout in seconds")
  65.  
  66. dir_basedn = Option('account-manager', 'dir_basedn', None,
  67. "Base DN used for account searches")
  68.  
  69. user_attr = Option('account-manager', 'user_attr', 'sAMAccountName',
  70. "Attribute of the user in the directory")
  71.  
  72. name_attr = Option('account-manager', 'name_attr', 'displayName',
  73. "Attribute of the users name in the directory")
  74.  
  75. email_attr = Option('account-manager', 'email_attr', 'mail',
  76. "Attribute of the users email in the directory")
  77.  
  78. proxy_attr = Option('account-manager', 'proxy_attr', 'proxyAddress',
  79. "Attribute of the users proxyAddress in the directory")
  80.  
  81. member_attr = Option('account-manager', 'member_attr', 'member',
  82. "Attribute to determine members of a group")
  83.  
  84. group_class_attr = Option('account-manager', 'group_class_attr', 'group',
  85. "Attribute of the group class")
  86.  
  87. group_basedn = Option('account-manager', 'group_basedn', None,
  88. "Base DN used for group searches")
  89.  
  90. group_validusers = Option('account-manager', 'group_validusers', None,
  91. "CN of group containing valid users. If None, "
  92. "any AD user is valid")
  93.  
  94. group_tracadmin = Option('account-manager', 'group_tracadmin', None,
  95. "CN of group containing TRAC_ADMIN users (can "
  96. "also assign TRAC_ADMIN to an LDAP group.)")
  97.  
  98. group_expand = IntOption('account-manager', 'group_expand', 1,
  99. "binary: expand ldap_groups into trac groups.")
  100.  
  101. cache_ttl = IntOption('account-manager', 'cache_timeout', 60,
  102. "cache timeout in seconds")
  103.  
  104. cache_memsize = IntOption('account-manager', 'cache_memsize', 400,
  105. "size of memcache in entries, zero to disable")
  106.  
  107. cache_memprune = IntOption('account-manager', 'cache_memprune', 5,
  108. "percent of entries to prune")
  109.  
  110. cache_memsize_warn = IntOption('account-manager', 'cache_memsize_warn',
  111. 300,
  112. "warning message for cache pruning in "
  113. "seconds")
  114.  
  115. def __init__(self, ldap=None):
  116. self._ldap = ldap
  117. self._cache = {}
  118.  
  119. # IPasswordStore methods
  120.  
  121. def config_key(self):
  122. """Deprecated"""
  123. raise NotImplementedError
  124.  
  125. def get_users(self):
  126. """Grab a list of users from the session store."""
  127. all_users = self._cache_get('allusers')
  128. if all_users:
  129. return all_users
  130.  
  131. # Cache miss
  132. lcnx = self._bind_dir()
  133. self.log.info('get users')
  134. if lcnx:
  135. if self.group_validusers:
  136. userinfo = self.expand_group_users(lcnx, self.group_validusers)
  137. else:
  138. users = lcnx.search_s(self.dir_basedn, ldap.SCOPE_SUBTREE,
  139. "objectClass=person",
  140. [to_utf8(self.user_attr),
  141. to_utf8(self.email_attr),
  142. to_utf8(self.proxy_attr),
  143. to_utf8(self.name_attr)])
  144. userinfo = [self._get_userinfo(u[1]) for u in users]
  145. else:
  146. raise TracError('Unable to bind to Active Directory')
  147. self.log.info('get users: ' + str(userinfo))
  148. return [u[0] for u in userinfo]
  149.  
  150. def expand_group_users(self, cnx, group):
  151. """Given a group name, enumerate all members"""
  152. if group.startswith('@'):
  153. group = group[1:]
  154. self.log.debug("search groups cn=%s,%s"
  155. % (group, self.group_basedn))
  156. g = cnx.search_s("cn=%s,%s" % (group.replace('_', ' '), self.group_basedn),
  157. ldap.SCOPE_BASE,
  158. attrlist=[to_utf8(self.member_attr)])
  159. self.log.debug(g)
  160. if g and self.member_attr in g[0][1]:
  161. users = []
  162. for m in g[0][1][str(self.member_attr)]:
  163. self.log.debug("group expand: " + m)
  164. try:
  165. e = cnx.search_s(m, ldap.SCOPE_BASE)
  166. if e:
  167. if 'person' in e[0][1]['objectClass']:
  168. users.append(self._get_userinfo(e[0][1]))
  169. elif str(self.group_class_attr) in e[0][1]['objectClass']:
  170. users.extend(self.expand_group_users(cnx, e[0][0]))
  171. else:
  172. self.log.debug('The group member (%s) is neither a group nor a person' % e[0][0])
  173. else:
  174. self.log.debug('Unable to find user listed in group: %s' % str(m))
  175. self.log.debug('This is very strange and you should probably check '
  176. 'the consistency of your LDAP directory.' % str(m))
  177. except Exception:
  178. self.log.debug('Unable to find ldap user listed in group: %s' % str(m))
  179. # users.append(m)
  180. return users
  181. else:
  182. self.log.debug('Unable to find any members of the group %s' % group)
  183. return []
  184.  
  185. def has_user(self, user):
  186. users = self.get_users()
  187. if user in users:
  188. return True
  189. else:
  190. return False
  191.  
  192. def check_password(self, user, password):
  193. """Checks the password against LDAP."""
  194. success = None
  195. msg = "User Login: %s" % user
  196.  
  197. if not user or not password:
  198. msg += " username or password can't be empty!"
  199. self.log.info(msg)
  200. return success
  201.  
  202. user_dn = self._get_user_dn(user, NOCACHE)
  203. if user_dn:
  204. success = self._bind_dir(user_dn, password.encode(self.dir_charset)) or False
  205. if success:
  206. msg += " Password Verified"
  207. success = True
  208. elif success is False:
  209. msg += " Password Failed"
  210. self.log.info(msg)
  211. else:
  212. msg += " does not exist, deferring authentication"
  213. self.log.info(msg)
  214. return success
  215.  
  216. # Check the user is part of the right group, we don't use the cache
  217. # Here as this is part of 'authentication' vs 'authorization'
  218. if self.group_validusers:
  219. usergroups = self._expand_user_groups(user, NOCACHE)
  220. if self.group_validusers not in usergroups:
  221. msg += " but user is not in %s" % self.group_validusers
  222. self.log.info(msg)
  223. return False
  224.  
  225. # Update the session data at each login,
  226. # Note the use of NoCache to force the update(s)
  227. attrs = [self.user_attr, self.email_attr, self.proxy_attr, self.name_attr]
  228. lfilter = '(&(%s=%s)(objectClass=person))' % (self.user_attr, user)
  229. users = self._dir_search(self.dir_basedn, self.dir_scope,
  230. lfilter, attrs, NOCACHE)
  231.  
  232. if not users:
  233. raise TracError(_("Authenticated, but didn't find the user with "
  234. "filter: %(filter)s (%(users)s)",
  235. filter=filter, users=users))
  236.  
  237. # Update the session table to make this a valid user.
  238. user_info = self._get_userinfo(users[0][1])
  239. self._populate_user_session(user_info)
  240.  
  241. # Update the users by doing a search w/o cache
  242. self.get_users()
  243.  
  244. return success
  245.  
  246. def delete_user(self, user):
  247. """Can't delete from LDAP."""
  248. raise NotImplementedError(_("Deleting users is not supported."))
  249.  
  250. def get_user_groups(self, user):
  251. """Returns all groups for a user."""
  252. return self._expand_user_groups(user)
  253.  
  254. def get_permission_groups(self, username):
  255. """Return a list of names of the groups that the user with the
  256. specified name is a member of."""
  257. return self._expand_user_groups(username)
  258.  
  259. # Internal methods
  260.  
  261. def _bind_dir(self, user_dn=None, passwd=None):
  262.  
  263. if not self.dir_uri:
  264. raise TracError(_("The dir_uri ini option must be set."))
  265.  
  266. if not self.dir_uri.lower().startswith('ldap'):
  267. raise TracError(_("The dir_uri URI must start with ldaps."))
  268.  
  269. if user_dn and passwd:
  270. user_ldap = ldap.ldapobject.ReconnectLDAPObject(self.dir_uri, 0,
  271. '', 0, 2, 1)
  272.  
  273. self.log.debug("_bind_dir: attempting specific bind to %s as %s",
  274. self.dir_uri, unicode(user_dn, 'utf8'))
  275. try:
  276. user_ldap.simple_bind_s(user_dn, passwd)
  277. except Exception, e:
  278. self.log.error("_bind_dir: binding failed. %s", e)
  279. return None
  280. return 1
  281.  
  282. # Return cached handle for default use.
  283. if self._ldap:
  284. return self._ldap
  285.  
  286. self._ldap = ldap.ldapobject.ReconnectLDAPObject(self.dir_uri,
  287. retry_max=5,
  288. retry_delay=1)
  289.  
  290. if self.dir_binddn:
  291. self.log.debug("_bind_dir: attempting general bind to %s as %s",
  292. self.dir_uri, self.dir_binddn)
  293. else:
  294. self.log.debug("_bind_dir: attempting general bind to %s "
  295. "anonymously", self.dir_uri)
  296.  
  297. try:
  298. self._ldap.simple_bind_s(self.dir_binddn, self.dir_bindpw)
  299. except ldap.LDAPError, e:
  300. raise TracError("cannot bind to %s: %s" % (self.dir_uri, e))
  301.  
  302. self.log.info("Bound to %s correctly.", self.dir_uri)
  303.  
  304. # Allow restarting.
  305. self._ldap.set_option(ldap.OPT_RESTART, 1)
  306. self._ldap.set_option(ldap.OPT_TIMEOUT, self.dir_timeout)
  307. # Disable LDAP referrals
  308. self._ldap.set_option(ldap.OPT_REFERRALS,0)
  309.  
  310.  
  311. return self._ldap
  312.  
  313. # ## searches
  314. def _get_user_dn(self, user, cache=1):
  315. """Get users dn."""
  316.  
  317. dn = self._cache_get('dn: %s' % user)
  318. if dn:
  319. return dn
  320.  
  321. u = self._dir_search(self.dir_basedn, self.dir_scope,
  322. "(&(%s=%s)(objectClass=person))"
  323. % (self.user_attr, user),
  324. [self.user_attr], cache)
  325.  
  326. if not u:
  327. self.log.debug("user not found: %s", user)
  328. dn = None
  329. else:
  330. dn = u[0][0]
  331. self._cache_set('dn: %s' % user, dn)
  332. self.log.debug("user %s has dn: %s", user, dn)
  333. return dn
  334.  
  335. def _expand_user_groups(self, user, use_cache=1):
  336. """Get a list of all groups this user belongs to. This recurses up
  337. to make sure we get them all.
  338. """
  339.  
  340. if use_cache:
  341. groups = self._cache_get('usergroups:%s' % user)
  342. if groups:
  343. return groups
  344.  
  345. groups = []
  346. user_dn = self._get_user_dn(user)
  347.  
  348. if not user_dn:
  349. self.log.debug("username: %s has no dn.", user)
  350. return []
  351.  
  352. basedn = self.group_basedn or self.dir_basedn
  353. group_filter = ('(&(objectClass=%s)(%s=%s))') % (self.group_class_attr, self.member_attr, user_dn)
  354. # Added replace filter to escape \ on CN
  355. user_groups = self._dir_search(basedn, self.dir_scope,
  356. group_filter.replace('\\', '\\\\'), ['cn'])
  357. for entry in user_groups:
  358. groupdn = entry[0]
  359. group = entry[1]['cn'][0]
  360. # Return spaces to the group names so matching works
  361. group = '%s%s' % (GROUP_PREFIX, group.replace(' ', '_').lower())
  362. groups.append(group) # dn
  363. if group not in groups:
  364. groups.append(self._get_parent_groups(groups, groupdn))
  365.  
  366. self._cache_set('usergroups:%s' % user, groups)
  367. if groups:
  368. self.log.debug("username %s has groups %s", user, ', '.join(groups))
  369. return sorted(groups)
  370. else:
  371. self.log.info("username: %s has no groups.", user)
  372. return []
  373.  
  374. def _get_parent_groups(self, groups, group_dn):
  375. group_filter = '(&(objectClass=%s)(%s=%s)' % (self.group_class_attr, self.member_attr, group_dn)
  376. basedn = self.group_basedn or self.dir_basedn
  377. ldap_groups = self._dir_search(basedn, self.dir_scope,
  378. group_filter, ['cn'])
  379. if ldap_groups:
  380. for entry in ldap_groups:
  381. groupdn = entry[0]
  382. group = entry[1]['cn'][0]
  383. group = group.replace(' ', '_').lower()
  384. if group not in groups:
  385. groups.append(group)
  386. groups.append(self._get_parent_groups(groups, groupdn))
  387. return groups
  388.  
  389. def _get_userinfo(self, attrs):
  390. """Extract the userinfo tuple from the LDAP search result."""
  391. user_name = attrs[self.user_attr][0].lower()
  392. display_name = attrs.get(self.name_attr, [''])[0]
  393. email = ''
  394. if self.email_attr in attrs:
  395. email = attrs[self.email_attr][0].lower()
  396. elif 'proxyAddresses' in attrs:
  397. for e in attrs['proxyAddresses']:
  398. if e.startswith('SMTP:'):
  399. email = e[5:]
  400. continue
  401. return user_name, display_name, email
  402.  
  403. def _populate_user_session(self, userinfo):
  404. """Create user session entries and populate email and last visit."""
  405.  
  406. # Kind of ugly. First try to insert a new session record. If it
  407. # fails, don't worry, means it's already there. Second, insert the
  408. # email address session attribute. If it fails, don't worry, it's
  409. # already there.
  410. uname, displayname, email = userinfo
  411.  
  412. db = self.env.get_db_cnx()
  413. cur = db.cursor()
  414. try:
  415. cur.execute("""
  416. DELETE FROM session
  417. WHERE sid=%s AND authenticated=1
  418. """, (uname,))
  419. cur.execute("""
  420. INSERT INTO session
  421. (sid, authenticated, last_visit)
  422. VALUES (%s, 1, %s)""", (uname, 0))
  423. except:
  424. self.log.debug("Session for %s exists.", uname)
  425.  
  426. # Assume enabled if we get this far self.env.get_known_users()
  427. # needs this..
  428. # TODO need to have it updated by the get_dn stuff long term so the
  429. # db matches the auth source.
  430. cur = db.cursor()
  431. try:
  432. cur.execute("""
  433. DELETE FROM session_attribute
  434. WHERE sid=%s AND authenticated=1 AND name='enabled'
  435. """, (uname,))
  436. cur.execute("""
  437. INSERT INTO session_attribute
  438. (sid, authenticated, name, value)
  439. VALUES (%s, 1, 'enabled', '1')
  440. """, (uname,))
  441. except:
  442. self.log.debug("Session for %s exists.", uname)
  443.  
  444. if email:
  445. cur = db.cursor()
  446. cur.execute("""
  447. DELETE FROM session_attribute
  448. WHERE sid=%s AND authenticated=1 AND name='email'
  449. """, (uname,))
  450. cur.execute("""
  451. INSERT INTO session_attribute
  452. (sid, authenticated, name, value)
  453. VALUES (%s, 1, 'email', %s)
  454. """, (uname, to_unicode(email)))
  455. self.log.info("updating user session email info for %s (%s)",
  456. uname, to_unicode(email))
  457.  
  458. if displayname:
  459. cur = db.cursor()
  460. cur.execute("""
  461. DELETE FROM session_attribute
  462. WHERE sid=%s AND authenticated=1 AND name='name'
  463. """, (uname,))
  464. cur.execute("""
  465. INSERT INTO session_attribute
  466. (sid, authenticated, name, value)
  467. VALUES (%s, 1, 'name', %s)
  468. """, (uname, to_unicode(displayname)))
  469. self.log.info("updating user session displayname info for %s (%s)",
  470. uname, to_unicode(displayname))
  471. db.commit()
  472. return db.close()
  473.  
  474. def _cache_get(self, key=None, ttl=None):
  475. """Get an item from memory cache"""
  476. cache_ttl = ttl or self.cache_ttl
  477. if not self.cache_memsize:
  478. return None
  479.  
  480. now = time.time()
  481.  
  482. if key in self._cache:
  483. lut, data = self._cache[key]
  484. if lut + cache_ttl >= now:
  485. self.log.debug("memcache hit for %s", key)
  486. return data
  487. else:
  488. del self._cache[key]
  489. return None
  490.  
  491. def _cache_set(self, key=None, data=None, cache_time=None):
  492. if not self.cache_memsize:
  493. return None
  494. now = time.time()
  495. if not cache_time:
  496. cache_time = now
  497.  
  498. # Prune if we need to.
  499. if len(self._cache) > self.cache_memsize:
  500. # Warn if too frequent.
  501. if 'last_prune' in self._cache:
  502. last_prune, data = self._cache['last_prune']
  503. if last_prune + self.cache_memsize_warn > now:
  504. self.log.info("pruning memcache in less than %d seconds, "
  505. "you might increase cache_memsize.",
  506. self.cache_memsize_warn)
  507.  
  508. self.log.debug("pruning memcache by %d: (current: %d > max: %d )",
  509. self.cache_memprune, len(self._cache),
  510. self.cache_memsize)
  511. cache_keys = self._cache.keys()
  512. cache_keys.sort(lambda x, y: cmp(self._cache[x][0],
  513. self._cache[y][0]))
  514. # Discards the 10% oldest.
  515. upper = self.cache_memprune * self.cache_memsize / 100
  516. old_keys = cache_keys[:upper]
  517. for k in old_keys:
  518. del self._cache[k]
  519. self._cache['last_prune'] = [now, []]
  520.  
  521. self._cache[key] = [cache_time, data]
  522. return data
  523.  
  524. def _dir_search(self, basedn, scope, lfilter, attrs=None, check_cache=1):
  525. current_time = time.time()
  526.  
  527. attrs = self._decode_list(attrs or [])
  528.  
  529. if not basedn:
  530. raise TracError(_("basedn not defined!"))
  531. if not lfilter:
  532. raise TracError(_("filter not defined!"))
  533.  
  534. # Create unique key from the filter and the attributes.
  535. keystr = ','.join([basedn, str(scope), lfilter, ':'.join(attrs)])
  536. key = hashlib.md5(keystr).hexdigest()
  537. self.log.debug("_dir_search: searching %s for %s(%s)",
  538. basedn, lfilter, key)
  539.  
  540. db = self.env.get_db_cnx()
  541.  
  542. # Check mem cache.
  543. if check_cache:
  544. ret = self._cache_get(key)
  545. if ret:
  546. return ret
  547.  
  548. # -- Check database
  549. cur = db.cursor()
  550. cur.execute("""
  551. SELECT lut,data FROM dir_cache WHERE id=%s
  552. """, (key,))
  553. row = cur.fetchone()
  554. if row:
  555. lut, data = row
  556.  
  557. if current_time < lut + self.cache_ttl:
  558. self.log.debug("dbcache hit for %s", lfilter)
  559. ret = cPickle.loads(str(data))
  560. self._cache_set(key, ret, lut)
  561. return ret
  562. else:
  563. # Old data, delete it and anything else that's old.
  564. lut = current_time - self.cache_ttl
  565. cur.execute("""
  566. DELETE FROM dir_cache WHERE lut < %s
  567. """, (lut,))
  568. db.commit()
  569. else:
  570. self.log.debug("_dir_search: skipping cache.")
  571.  
  572. d = self._bind_dir()
  573. self.log.debug("_dir_search: starting LDAP search of %s %s using %s "
  574. "for %s", self.dir_uri, basedn, lfilter, attrs)
  575.  
  576. res = []
  577. try:
  578. res = d.search_s(basedn.encode(self.dir_charset), scope,
  579. lfilter, attrs)
  580. except ldap.LDAPError, e:
  581. self.log.error("Error searching %s using %s: %s",
  582. basedn, lfilter, e)
  583.  
  584. if res:
  585. self.log.debug("_dir_search: dir hit, %d entries.", len(res))
  586. else:
  587. self.log.debug("_dir_search: dir miss.")
  588.  
  589. if not check_cache:
  590. return res
  591.  
  592. # Set the db cache for the next search, even if results are empty.
  593. res_str = cPickle.dumps(res, 0)
  594. try:
  595. cur = db.cursor()
  596. cur.execute("""
  597. DELETE FROM dir_cache WHERE id=%s
  598. """, (key,))
  599. self.log.debug("INSERT VALUES (%s, %s, %s)"
  600. % (key, current_time, buffer(res_str)))
  601. cur.execute("""
  602. INSERT INTO dir_cache (id, lut, data)
  603. VALUES (%s, %s, %s)
  604. """, (key, current_time, buffer(res_str)))
  605. db.commit()
  606. except Exception, e:
  607. db.rollback()
  608. self.log.warn("_dir_search: db cache update failed. %s" % e)
  609.  
  610. self._cache_set(key, res)
  611.  
  612. return res
  613.  
  614. # helper method for UserExtensiblePermissionStore
  615. def get_all_groups(self):
  616. """Get all groups. Returns an array containing arrays [dn, cn]
  617. """
  618.  
  619. basedn = self.group_basedn or self.dir_basedn
  620. group_filter = ('(objectClass=%s)') % self.group_class_attr
  621. all_groups = self._dir_search(basedn, self.dir_scope, group_filter, ['cn'])
  622.  
  623. self.log.debug("all groups: %s" % all_groups)
  624. return all_groups
  625.  
  626. def get_group_users(self, groupdn):
  627. """Grab a list of users from the session store."""
  628.  
  629. lcnx = self._bind_dir()
  630. self.log.info('get users')
  631. if lcnx:
  632. userinfo = self.expand_group_users(lcnx, groupdn)
  633. else:
  634. raise TracError('Unable to bind to Active Directory')
  635. self.log.debug('get users: ' + str(userinfo))
  636. return [u[0] for u in userinfo]
  637.  
  638. @staticmethod
  639. def _decode_list(l=None):
  640. newlist = []
  641. if not l:
  642. return l
  643. for val in l:
  644. newlist.append(val.encode('ascii', 'ignore'))
  645. return newlist
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement