PageRenderTime 66ms CodeModel.GetById 22ms RepoModel.GetById 1ms app.codeStats 0ms

/reviewboard/accounts/backends.py

http://github.com/reviewboard/reviewboard
Python | 1133 lines | 1098 code | 18 blank | 17 comment | 8 complexity | 1230046406650d952a9b585fdb1593f0 MD5 | raw file
Possible License(s): GPL-2.0
  1. from __future__ import unicode_literals
  2. import hashlib
  3. import logging
  4. import re
  5. import sre_constants
  6. import sys
  7. from warnings import warn
  8. from django.conf import settings
  9. from django.contrib.auth.backends import ModelBackend
  10. from django.contrib.auth.models import User
  11. from django.contrib.auth import get_backends
  12. from django.contrib.auth import hashers
  13. from django.utils import six
  14. from django.utils.translation import ugettext_lazy as _
  15. from djblets.db.query import get_object_or_none
  16. from djblets.siteconfig.models import SiteConfiguration
  17. try:
  18. from ldap.filter import filter_format
  19. except ImportError:
  20. pass
  21. from djblets.registries.registry import (ALREADY_REGISTERED, LOAD_ENTRY_POINT,
  22. NOT_REGISTERED, UNREGISTER)
  23. from reviewboard.accounts.forms.auth import (ActiveDirectorySettingsForm,
  24. LDAPSettingsForm,
  25. NISSettingsForm,
  26. StandardAuthSettingsForm,
  27. X509SettingsForm,
  28. HTTPBasicSettingsForm)
  29. from reviewboard.accounts.models import LocalSiteProfile
  30. from reviewboard.site.models import LocalSite
  31. from reviewboard.registries.registry import EntryPointRegistry
  32. _enabled_auth_backends = []
  33. _auth_backend_setting = None
  34. INVALID_USERNAME_CHAR_REGEX = re.compile(r'[^\w.@+-]')
  35. class AuthBackend(object):
  36. """The base class for Review Board authentication backends."""
  37. backend_id = None
  38. name = None
  39. settings_form = None
  40. supports_anonymous_user = True
  41. supports_object_permissions = True
  42. supports_registration = False
  43. supports_change_name = False
  44. supports_change_email = False
  45. supports_change_password = False
  46. login_instructions = None
  47. def authenticate(self, username, password):
  48. """Authenticate the user.
  49. This will authenticate the username and return the appropriate User
  50. object, or None.
  51. """
  52. raise NotImplementedError
  53. def get_or_create_user(self, username, request):
  54. """Get an existing user, or create one if it does not exist."""
  55. raise NotImplementedError
  56. def get_user(self, user_id):
  57. """Get an existing user, or None if it does not exist."""
  58. return get_object_or_none(User, pk=user_id)
  59. def update_password(self, user, password):
  60. """Update the user's password on the backend.
  61. Authentication backends can override this to update the password
  62. on the backend. This will only be called if
  63. :py:attr:`supports_change_password` is ``True``.
  64. By default, this will raise NotImplementedError.
  65. """
  66. raise NotImplementedError
  67. def update_name(self, user):
  68. """Update the user's name on the backend.
  69. The first name and last name will already be stored in the provided
  70. ``user`` object.
  71. Authentication backends can override this to update the name
  72. on the backend based on the values in ``user``. This will only be
  73. called if :py:attr:`supports_change_name` is ``True``.
  74. By default, this will do nothing.
  75. """
  76. pass
  77. def update_email(self, user):
  78. """Update the user's e-mail address on the backend.
  79. The e-mail address will already be stored in the provided
  80. ``user`` object.
  81. Authentication backends can override this to update the e-mail
  82. address on the backend based on the values in ``user``. This will only
  83. be called if :py:attr:`supports_change_email` is ``True``.
  84. By default, this will do nothing.
  85. """
  86. pass
  87. def query_users(self, query, request):
  88. """Search for users on the back end.
  89. This call is executed when the User List web API resource is called,
  90. before the database is queried.
  91. Authentication backends can override this to perform an external
  92. query. Results should be written to the database as standard
  93. Review Board users, which will be matched and returned by the web API
  94. call.
  95. The ``query`` parameter contains the value of the ``q`` search
  96. parameter of the web API call (e.g. /users/?q=foo), if any.
  97. Errors can be passed up to the web API layer by raising a
  98. reviewboard.accounts.errors.UserQueryError exception.
  99. By default, this will do nothing.
  100. """
  101. pass
  102. def search_users(self, query, request):
  103. """Custom user-database search.
  104. This call is executed when the User List web API resource is called
  105. and the ``q`` search parameter is provided, indicating a search
  106. query.
  107. It must return either a django.db.models.Q object or None. All
  108. enabled backends are called until a Q object is returned. If one
  109. isn't returned, a default search is executed.
  110. """
  111. return None
  112. class StandardAuthBackend(AuthBackend, ModelBackend):
  113. """Authenticate users against the local database.
  114. This will authenticate a user against their entry in the database, if
  115. the user has a local password stored. This is the default form of
  116. authentication in Review Board.
  117. This backend also handles permission checking for users on LocalSites.
  118. In Django, this is the responsibility of at least one auth backend in
  119. the list of configured backends.
  120. Regardless of the specific type of authentication chosen for the
  121. installation, StandardAuthBackend will always be provided in the list
  122. of configured backends. Because of this, it will always be able to
  123. handle authentication against locally added users and handle
  124. LocalSite-based permissions for all configurations.
  125. """
  126. backend_id = 'builtin'
  127. name = _('Standard Registration')
  128. settings_form = StandardAuthSettingsForm
  129. supports_registration = True
  130. supports_change_name = True
  131. supports_change_email = True
  132. supports_change_password = True
  133. _VALID_LOCAL_SITE_PERMISSIONS = [
  134. 'hostingsvcs.change_hostingserviceaccount',
  135. 'hostingsvcs.create_hostingserviceaccount',
  136. 'reviews.add_group',
  137. 'reviews.can_change_status',
  138. 'reviews.can_edit_reviewrequest',
  139. 'reviews.can_submit_as_another_user',
  140. 'reviews.change_default_reviewer',
  141. 'reviews.change_group',
  142. 'reviews.delete_file',
  143. 'reviews.delete_screenshot',
  144. 'scmtools.add_repository',
  145. 'scmtools.change_repository',
  146. ]
  147. def authenticate(self, username, password):
  148. """Authenticate the user.
  149. This will authenticate the username and return the appropriate User
  150. object, or None.
  151. """
  152. return ModelBackend.authenticate(self, username, password)
  153. def get_or_create_user(self, username, request):
  154. """Get an existing user, or create one if it does not exist."""
  155. return get_object_or_none(User, username=username)
  156. def update_password(self, user, password):
  157. """Update the given user's password."""
  158. user.password = hashers.make_password(password)
  159. def get_all_permissions(self, user, obj=None):
  160. """Get a list of all permissions for a user.
  161. If a LocalSite instance is passed as ``obj``, then the permissions
  162. returned will be those that the user has on that LocalSite. Otherwise,
  163. they will be their global permissions.
  164. It is not legal to pass any other object.
  165. """
  166. if obj is not None and not isinstance(obj, LocalSite):
  167. logging.error('Unexpected object %r passed to '
  168. 'StandardAuthBackend.get_all_permissions. '
  169. 'Returning an empty list.',
  170. obj)
  171. if settings.DEBUG:
  172. raise ValueError('Unexpected object %r' % obj)
  173. return set()
  174. if user.is_anonymous():
  175. return set()
  176. # First, get the list of all global permissions.
  177. #
  178. # Django's ModelBackend doesn't support passing an object, and will
  179. # return an empty set, so don't pass an object for this attempt.
  180. permissions = \
  181. super(StandardAuthBackend, self).get_all_permissions(user)
  182. if obj is not None:
  183. # We know now that this is a LocalSite, due to the assertion
  184. # above.
  185. if not hasattr(user, '_local_site_perm_cache'):
  186. user._local_site_perm_cache = {}
  187. if obj.pk not in user._local_site_perm_cache:
  188. perm_cache = set()
  189. try:
  190. site_profile = user.get_site_profile(obj)
  191. site_perms = site_profile.permissions or {}
  192. if site_perms:
  193. perm_cache = set([
  194. key
  195. for key, value in six.iteritems(site_perms)
  196. if value
  197. ])
  198. except LocalSiteProfile.DoesNotExist:
  199. pass
  200. user._local_site_perm_cache[obj.pk] = perm_cache
  201. permissions = permissions.copy()
  202. permissions.update(user._local_site_perm_cache[obj.pk])
  203. return permissions
  204. def has_perm(self, user, perm, obj=None):
  205. """Get whether or not a user has the given permission.
  206. If a LocalSite instance is passed as ``obj``, then the permissions
  207. checked will be those that the user has on that LocalSite. Otherwise,
  208. they will be their global permissions.
  209. It is not legal to pass any other object.
  210. """
  211. if obj is not None and not isinstance(obj, LocalSite):
  212. logging.error('Unexpected object %r passed to has_perm. '
  213. 'Returning False.', obj)
  214. if settings.DEBUG:
  215. raise ValueError('Unexpected object %r' % obj)
  216. return False
  217. if not user.is_active:
  218. return False
  219. if obj is not None:
  220. if not hasattr(user, '_local_site_admin_for'):
  221. user._local_site_admin_for = {}
  222. if obj.pk not in user._local_site_admin_for:
  223. user._local_site_admin_for[obj.pk] = obj.is_mutable_by(user)
  224. if user._local_site_admin_for[obj.pk]:
  225. return perm in self._VALID_LOCAL_SITE_PERMISSIONS
  226. return super(StandardAuthBackend, self).has_perm(user, perm, obj)
  227. class HTTPDigestBackend(AuthBackend):
  228. """Authenticate against a user in a digest password file."""
  229. backend_id = 'digest'
  230. name = _('HTTP Digest Authentication')
  231. settings_form = HTTPBasicSettingsForm
  232. login_instructions = \
  233. _('Use your standard username and password.')
  234. def authenticate(self, username, password):
  235. """Authenticate the user.
  236. This will authenticate the username and return the appropriate User
  237. object, or None.
  238. """
  239. username = username.strip()
  240. digest_text = '%s:%s:%s' % (username, settings.DIGEST_REALM, password)
  241. digest_password = hashlib.md5(digest_text).hexdigest()
  242. try:
  243. with open(settings.DIGEST_FILE_LOCATION, 'r') as passwd_file:
  244. for line_no, line in enumerate(passwd_file):
  245. try:
  246. user, realm, passwd = line.strip().split(':')
  247. if user == username and passwd == digest_password:
  248. return self.get_or_create_user(username, None)
  249. else:
  250. continue
  251. except ValueError as e:
  252. logging.error('Error parsing HTTP Digest password '
  253. 'file at line %d: %s',
  254. line_no, e, exc_info=True)
  255. break
  256. except IOError as e:
  257. logging.error('Could not open the HTTP Digest password file: %s',
  258. e, exc_info=True)
  259. return None
  260. def get_or_create_user(self, username, request):
  261. """Get an existing user, or create one if it does not exist."""
  262. try:
  263. user = User.objects.get(username=username)
  264. except User.DoesNotExist:
  265. user = User(username=username, password='')
  266. user.is_staff = False
  267. user.is_superuser = False
  268. user.set_unusable_password()
  269. user.save()
  270. return user
  271. class NISBackend(AuthBackend):
  272. """Authenticate against a user on an NIS server."""
  273. backend_id = 'nis'
  274. name = _('NIS')
  275. settings_form = NISSettingsForm
  276. login_instructions = \
  277. _('Use your standard NIS username and password.')
  278. def authenticate(self, username, password):
  279. """Authenticate the user.
  280. This will authenticate the username and return the appropriate User
  281. object, or None.
  282. """
  283. import crypt
  284. import nis
  285. username = username.strip()
  286. try:
  287. passwd = nis.match(username, 'passwd').split(':')
  288. original_crypted = passwd[1]
  289. new_crypted = crypt.crypt(password, original_crypted)
  290. if original_crypted == new_crypted:
  291. return self.get_or_create_user(username, None, passwd)
  292. except nis.error:
  293. # FIXME I'm not sure under what situations this would fail (maybe
  294. # if their NIS server is down), but it'd be nice to inform the
  295. # user.
  296. pass
  297. return None
  298. def get_or_create_user(self, username, request, passwd=None):
  299. """Get an existing user, or create one if it does not exist."""
  300. import nis
  301. username = username.strip()
  302. try:
  303. user = User.objects.get(username=username)
  304. except User.DoesNotExist:
  305. try:
  306. if not passwd:
  307. passwd = nis.match(username, 'passwd').split(':')
  308. names = passwd[4].split(',')[0].split(' ', 1)
  309. first_name = names[0]
  310. last_name = None
  311. if len(names) > 1:
  312. last_name = names[1]
  313. email = '%s@%s' % (username, settings.NIS_EMAIL_DOMAIN)
  314. user = User(username=username,
  315. password='',
  316. first_name=first_name,
  317. last_name=last_name or '',
  318. email=email)
  319. user.is_staff = False
  320. user.is_superuser = False
  321. user.set_unusable_password()
  322. user.save()
  323. except nis.error:
  324. pass
  325. return user
  326. class LDAPBackend(AuthBackend):
  327. """Authenticate against a user on an LDAP server."""
  328. backend_id = 'ldap'
  329. name = _('LDAP')
  330. settings_form = LDAPSettingsForm
  331. login_instructions = \
  332. _('Use your standard LDAP username and password.')
  333. def authenticate(self, username, password):
  334. """Authenticate the user.
  335. This will authenticate the username and return the appropriate User
  336. object, or None.
  337. """
  338. username = username.strip()
  339. uidfilter = "(%(userattr)s=%(username)s)" % {
  340. 'userattr': settings.LDAP_UID,
  341. 'username': username
  342. }
  343. # If the UID mask has been explicitly set, override
  344. # the standard search filter
  345. if settings.LDAP_UID_MASK:
  346. uidfilter = settings.LDAP_UID_MASK % username
  347. if len(password) == 0:
  348. # Don't try to bind using an empty password; the server will
  349. # return success, which doesn't mean we have authenticated.
  350. # http://tools.ietf.org/html/rfc4513#section-5.1.2
  351. # http://tools.ietf.org/html/rfc4513#section-6.3.1
  352. logging.warning("Empty password for: %s", username)
  353. return None
  354. if isinstance(username, six.text_type):
  355. username_bytes = username.encode('utf-8')
  356. else:
  357. username_bytes = username
  358. if isinstance(password, six.text_type):
  359. password = password.encode('utf-8')
  360. try:
  361. import ldap
  362. ldapo = ldap.initialize(settings.LDAP_URI)
  363. ldapo.set_option(ldap.OPT_REFERRALS, 0)
  364. ldapo.set_option(ldap.OPT_PROTOCOL_VERSION, 3)
  365. if settings.LDAP_TLS:
  366. ldapo.start_tls_s()
  367. if settings.LDAP_ANON_BIND_UID:
  368. # Log in as the service account before searching.
  369. ldapo.simple_bind_s(settings.LDAP_ANON_BIND_UID,
  370. settings.LDAP_ANON_BIND_PASSWD)
  371. else:
  372. # Bind anonymously to the server
  373. ldapo.simple_bind_s()
  374. # Search for the user with the given base DN and uid. If the user
  375. # is found, a fully qualified DN is returned. Authentication is
  376. # then done with bind using this fully qualified DN.
  377. search = ldapo.search_s(settings.LDAP_BASE_DN,
  378. ldap.SCOPE_SUBTREE,
  379. uidfilter)
  380. if not search:
  381. # No such user, return early, no need for bind attempts
  382. logging.warning("LDAP error: The specified object does "
  383. "not exist in the Directory: %s",
  384. username)
  385. return None
  386. else:
  387. userdn = search[0][0]
  388. # Now that we have the user, attempt to bind to verify
  389. # authentication
  390. logging.debug("Attempting to authenticate as %s"
  391. % userdn.decode('utf-8'))
  392. ldapo.bind_s(userdn, password)
  393. return self.get_or_create_user(username_bytes, None, ldapo, userdn)
  394. except ImportError:
  395. pass
  396. except ldap.INVALID_CREDENTIALS:
  397. logging.warning("LDAP error: The specified object does not exist "
  398. "in the Directory or provided invalid "
  399. "credentials: %s",
  400. username)
  401. except ldap.LDAPError as e:
  402. logging.warning("LDAP error: %s", e)
  403. except:
  404. # Fallback exception catch because
  405. # django.contrib.auth.authenticate() (our caller) catches only
  406. # TypeErrors
  407. logging.warning("An error while LDAP-authenticating: %r" %
  408. sys.exc_info()[1])
  409. return None
  410. def get_or_create_user(self, username, request, ldapo, userdn):
  411. """Get an existing user, or create one if it does not exist."""
  412. username = re.sub(INVALID_USERNAME_CHAR_REGEX, '', username).lower()
  413. try:
  414. user = User.objects.get(username=username)
  415. return user
  416. except User.DoesNotExist:
  417. try:
  418. import ldap
  419. # Perform a BASE search since we already know the DN of
  420. # the user
  421. search_result = ldapo.search_s(userdn,
  422. ldap.SCOPE_BASE)
  423. user_info = search_result[0][1]
  424. given_name_attr = getattr(
  425. settings, 'LDAP_GIVEN_NAME_ATTRIBUTE', 'givenName')
  426. first_name = user_info.get(given_name_attr, [username])[0]
  427. surname_attr = getattr(
  428. settings, 'LDAP_SURNAME_ATTRIBUTE', 'sn')
  429. last_name = user_info.get(surname_attr, [''])[0]
  430. # If a single ldap attribute is used to hold the full name of
  431. # a user, split it into two parts. Where to split was a coin
  432. # toss and I went with a left split for the first name and
  433. # dumped the remainder into the last name field. The system
  434. # admin can handle the corner cases.
  435. try:
  436. if settings.LDAP_FULL_NAME_ATTRIBUTE:
  437. full_name = \
  438. user_info[settings.LDAP_FULL_NAME_ATTRIBUTE][0]
  439. full_name = full_name.decode('utf-8')
  440. first_name, last_name = full_name.split(' ', 1)
  441. except AttributeError:
  442. pass
  443. if settings.LDAP_EMAIL_DOMAIN:
  444. email = '%s@%s' % (username, settings.LDAP_EMAIL_DOMAIN)
  445. elif settings.LDAP_EMAIL_ATTRIBUTE:
  446. try:
  447. email = user_info[settings.LDAP_EMAIL_ATTRIBUTE][0]
  448. except KeyError:
  449. logging.error('LDAP: could not get e-mail address for '
  450. 'user %s using attribute %s',
  451. username, settings.LDAP_EMAIL_ATTRIBUTE)
  452. email = ''
  453. else:
  454. logging.warning(
  455. 'LDAP: e-mail for user %s is not specified',
  456. username)
  457. email = ''
  458. user = User(username=username,
  459. password='',
  460. first_name=first_name,
  461. last_name=last_name,
  462. email=email)
  463. user.is_staff = False
  464. user.is_superuser = False
  465. user.set_unusable_password()
  466. user.save()
  467. return user
  468. except ImportError:
  469. pass
  470. except ldap.INVALID_CREDENTIALS:
  471. # FIXME I'd really like to warn the user that their
  472. # ANON_BIND_UID and ANON_BIND_PASSWD are wrong, but I don't
  473. # know how
  474. pass
  475. except ldap.NO_SUCH_OBJECT as e:
  476. logging.warning("LDAP error: %s settings.LDAP_BASE_DN: %s "
  477. "User DN: %s",
  478. e, settings.LDAP_BASE_DN, userdn,
  479. exc_info=1)
  480. except ldap.LDAPError as e:
  481. logging.warning("LDAP error: %s", e, exc_info=1)
  482. return None
  483. class ActiveDirectoryBackend(AuthBackend):
  484. """Authenticate a user against an Active Directory server."""
  485. backend_id = 'ad'
  486. name = _('Active Directory')
  487. settings_form = ActiveDirectorySettingsForm
  488. login_instructions = \
  489. _('Use your standard Active Directory username and password.')
  490. def get_domain_name(self):
  491. """Return the current AD domain name."""
  492. return six.text_type(settings.AD_DOMAIN_NAME)
  493. def get_ldap_search_root(self, userdomain=None):
  494. """Return the search root(s) for users in the LDAP server."""
  495. if getattr(settings, "AD_SEARCH_ROOT", None):
  496. root = [settings.AD_SEARCH_ROOT]
  497. else:
  498. if userdomain is None:
  499. userdomain = self.get_domain_name()
  500. root = ['dc=%s' % x for x in userdomain.split('.')]
  501. if settings.AD_OU_NAME:
  502. root = ['ou=%s' % settings.AD_OU_NAME] + root
  503. return ','.join(root)
  504. def search_ad(self, con, filterstr, userdomain=None):
  505. """Run a search on the given LDAP server."""
  506. import ldap
  507. search_root = self.get_ldap_search_root(userdomain)
  508. logging.debug('Search root ' + search_root)
  509. return con.search_s(search_root, scope=ldap.SCOPE_SUBTREE,
  510. filterstr=filterstr)
  511. def find_domain_controllers_from_dns(self, userdomain=None):
  512. """Find and return the active domain controllers using DNS."""
  513. import DNS
  514. DNS.Base.DiscoverNameServers()
  515. q = '_ldap._tcp.%s' % (userdomain or self.get_domain_name())
  516. req = DNS.Base.DnsRequest(q, qtype=DNS.Type.SRV).req()
  517. return [x['data'][-2:] for x in req.answers]
  518. def can_recurse(self, depth):
  519. """Return whether the given recursion depth is too big."""
  520. return (settings.AD_RECURSION_DEPTH == -1 or
  521. depth <= settings.AD_RECURSION_DEPTH)
  522. def get_member_of(self, con, search_results, seen=None, depth=0):
  523. """Get the LDAP groups for the given users.
  524. This iterates over the users specified in ``search_results`` and
  525. returns a set of groups of which those users are members.
  526. """
  527. depth += 1
  528. if seen is None:
  529. seen = set()
  530. for name, data in search_results:
  531. if name is None:
  532. continue
  533. member_of = data.get('memberOf', [])
  534. new_groups = [x.split(b',')[0].split(b'=')[1] for x in member_of]
  535. old_seen = seen.copy()
  536. seen.update(new_groups)
  537. # collect groups recursively
  538. if self.can_recurse(depth):
  539. for group in new_groups:
  540. if group in old_seen:
  541. continue
  542. # Search for groups with the specified CN. Use the CN
  543. # rather than The sAMAccountName so that behavior is
  544. # correct when the values differ (e.g. if a
  545. # "pre-Windows 2000" group name is set in AD)
  546. group_data = self.search_ad(
  547. con,
  548. filter_format('(&(objectClass=group)(cn=%s))',
  549. (group,)))
  550. seen.update(self.get_member_of(con, group_data,
  551. seen=seen, depth=depth))
  552. else:
  553. logging.warning('ActiveDirectory recursive group check '
  554. 'reached maximum recursion depth.')
  555. return seen
  556. def get_ldap_connections(self, userdomain=None):
  557. """Get a set of connections to LDAP servers.
  558. This returns an iterable of connections to the LDAP servers specified
  559. in AD_DOMAIN_CONTROLLER.
  560. """
  561. import ldap
  562. if settings.AD_FIND_DC_FROM_DNS:
  563. dcs = self.find_domain_controllers_from_dns(userdomain)
  564. else:
  565. dcs = []
  566. for dc_entry in settings.AD_DOMAIN_CONTROLLER.split():
  567. if ':' in dc_entry:
  568. host, port = dc_entry.split(':')
  569. else:
  570. host = dc_entry
  571. port = '389'
  572. dcs.append([port, host])
  573. for dc in dcs:
  574. port, host = dc
  575. ldap_uri = 'ldap://%s:%s' % (host, port)
  576. con = ldap.initialize(ldap_uri)
  577. if settings.AD_USE_TLS:
  578. try:
  579. con.start_tls_s()
  580. except ldap.UNAVAILABLE:
  581. logging.warning('Active Directory: Domain controller '
  582. '%s:%d for domain %s unavailable',
  583. host, int(port), userdomain)
  584. continue
  585. except ldap.CONNECT_ERROR:
  586. logging.warning("Active Directory: Could not connect "
  587. "to domain controller %s:%d for domain "
  588. "%s, possibly the certificate wasn't "
  589. "verifiable",
  590. host, int(port), userdomain)
  591. continue
  592. con.set_option(ldap.OPT_REFERRALS, 0)
  593. yield con
  594. def authenticate(self, username, password):
  595. """Authenticate the user.
  596. This will authenticate the username and return the appropriate User
  597. object, or None.
  598. """
  599. import ldap
  600. username = username.strip()
  601. user_subdomain = ''
  602. if '@' in username:
  603. username, user_subdomain = username.split('@', 1)
  604. elif '\\' in username:
  605. user_subdomain, username = username.split('\\', 1)
  606. userdomain = self.get_domain_name()
  607. if user_subdomain:
  608. userdomain = "%s.%s" % (user_subdomain, userdomain)
  609. connections = self.get_ldap_connections(userdomain)
  610. required_group = settings.AD_GROUP_NAME
  611. if isinstance(required_group, six.text_type):
  612. required_group = required_group.encode('utf-8')
  613. if isinstance(username, six.text_type):
  614. username_bytes = username.encode('utf-8')
  615. else:
  616. username_bytes = username
  617. if isinstance(user_subdomain, six.text_type):
  618. user_subdomain = user_subdomain.encode('utf-8')
  619. if isinstance(password, six.text_type):
  620. password = password.encode('utf-8')
  621. for con in connections:
  622. try:
  623. bind_username = b'%s@%s' % (username_bytes, userdomain)
  624. logging.debug("User %s is trying to log in via AD",
  625. bind_username.decode('utf-8'))
  626. con.simple_bind_s(bind_username, password)
  627. user_data = self.search_ad(
  628. con,
  629. filter_format('(&(objectClass=user)(sAMAccountName=%s))',
  630. (username_bytes,)),
  631. userdomain)
  632. if not user_data:
  633. return None
  634. if required_group:
  635. try:
  636. group_names = self.get_member_of(con, user_data)
  637. except Exception as e:
  638. logging.error("Active Directory error: failed getting"
  639. "groups for user '%s': %s",
  640. username, e, exc_info=1)
  641. return None
  642. if required_group not in group_names:
  643. logging.warning("Active Directory: User %s is not in "
  644. "required group %s",
  645. username, required_group)
  646. return None
  647. return self.get_or_create_user(username, None, user_data)
  648. except ldap.SERVER_DOWN:
  649. logging.warning('Active Directory: Domain controller is down')
  650. continue
  651. except ldap.INVALID_CREDENTIALS:
  652. logging.warning('Active Directory: Failed login for user %s',
  653. username)
  654. return None
  655. logging.error('Active Directory error: Could not contact any domain '
  656. 'controller servers')
  657. return None
  658. def get_or_create_user(self, username, request, ad_user_data):
  659. """Get an existing user, or create one if it does not exist."""
  660. username = re.sub(INVALID_USERNAME_CHAR_REGEX, '', username).lower()
  661. try:
  662. user = User.objects.get(username=username)
  663. return user
  664. except User.DoesNotExist:
  665. try:
  666. user_info = ad_user_data[0][1]
  667. first_name = user_info.get('givenName', [username])[0]
  668. last_name = user_info.get('sn', [""])[0]
  669. email = user_info.get(
  670. 'mail',
  671. ['%s@%s' % (username, settings.AD_DOMAIN_NAME)])[0]
  672. user = User(username=username,
  673. password='',
  674. first_name=first_name,
  675. last_name=last_name,
  676. email=email)
  677. user.is_staff = False
  678. user.is_superuser = False
  679. user.set_unusable_password()
  680. user.save()
  681. return user
  682. except:
  683. return None
  684. class X509Backend(AuthBackend):
  685. """Authenticate a user from a X.509 client certificate.
  686. The certificate is passed in by the browser. This backend relies on the
  687. X509AuthMiddleware to extract a username field from the client certificate.
  688. """
  689. backend_id = 'x509'
  690. name = _('X.509 Public Key')
  691. settings_form = X509SettingsForm
  692. supports_change_password = True
  693. def authenticate(self, x509_field=""):
  694. """Authenticate the user.
  695. This will extract the username from the provided certificate and return
  696. the appropriate User object.
  697. """
  698. username = self.clean_username(x509_field)
  699. return self.get_or_create_user(username, None)
  700. def clean_username(self, username):
  701. """Validate the 'username' field.
  702. This checks to make sure that the contents of the username field are
  703. valid for X509 authentication.
  704. """
  705. username = username.strip()
  706. if settings.X509_USERNAME_REGEX:
  707. try:
  708. m = re.match(settings.X509_USERNAME_REGEX, username)
  709. if m:
  710. username = m.group(1)
  711. else:
  712. logging.warning("X509Backend: username '%s' didn't match "
  713. "regex.", username)
  714. except sre_constants.error as e:
  715. logging.error("X509Backend: Invalid regex specified: %s",
  716. e, exc_info=1)
  717. return username
  718. def get_or_create_user(self, username, request):
  719. """Get an existing user, or create one if it does not exist."""
  720. user = None
  721. username = username.strip()
  722. try:
  723. user = User.objects.get(username=username)
  724. except User.DoesNotExist:
  725. # TODO Add the ability to get the first and last names in a
  726. # configurable manner; not all X.509 certificates will have
  727. # the same format.
  728. if getattr(settings, 'X509_AUTOCREATE_USERS', False):
  729. user = User(username=username, password='')
  730. user.is_staff = False
  731. user.is_superuser = False
  732. user.set_unusable_password()
  733. user.save()
  734. return user
  735. def get_enabled_auth_backends():
  736. """Get all authentication backends being used by Review Board.
  737. The returned list contains every authentication backend that Review Board
  738. will try, in order.
  739. """
  740. global _enabled_auth_backends
  741. global _auth_backend_setting
  742. if (not _enabled_auth_backends or
  743. _auth_backend_setting != settings.AUTHENTICATION_BACKENDS):
  744. _enabled_auth_backends = []
  745. for backend in get_backends():
  746. if not isinstance(backend, AuthBackend):
  747. warn('Authentication backends should inherit from '
  748. 'reviewboard.accounts.backends.AuthBackend. Please '
  749. 'update %s.' % backend.__class__)
  750. for field, default in (('name', None),
  751. ('supports_registration', False),
  752. ('supports_change_name', False),
  753. ('supports_change_email', False),
  754. ('supports_change_password', False)):
  755. if not hasattr(backend, field):
  756. warn("Authentication backends should define a '%s' "
  757. "attribute. Please define it in %s or inherit "
  758. "from AuthBackend." % (field, backend.__class__))
  759. setattr(backend, field, False)
  760. _enabled_auth_backends.append(backend)
  761. _auth_backend_setting = settings.AUTHENTICATION_BACKENDS
  762. return _enabled_auth_backends
  763. def set_enabled_auth_backend(backend_id):
  764. """Set the authentication backend to be used."""
  765. siteconfig = SiteConfiguration.objects.get_current()
  766. siteconfig.set('auth_backend', backend_id)
  767. class AuthBackendRegistry(EntryPointRegistry):
  768. """A registry for managing authentication backends."""
  769. entry_point = 'reviewboard.auth.backends'
  770. lookup_attrs = ('backend_id',)
  771. errors = {
  772. ALREADY_REGISTERED: _(
  773. '"%(item)r" is already a registered authentication backend.'
  774. ),
  775. LOAD_ENTRY_POINT: _(
  776. 'Error loading authentication backend %(entry_point)s: %(error)s'
  777. ),
  778. NOT_REGISTERED: _(
  779. 'No authentication backend registered with %(attr_name)s = '
  780. '%(attr_value)s.'
  781. ),
  782. UNREGISTER: _(
  783. '"%(item)r is not a registered authentication backend.'
  784. ),
  785. }
  786. def process_value_from_entry_point(self, entry_point):
  787. """Load the class from the entry point.
  788. If the class lacks a ``backend_id``, it will be set as the entry
  789. point's name.
  790. Args:
  791. entry_point (pkg_resources.EntryPoint):
  792. The entry point.
  793. Returns:
  794. type:
  795. The :py:class:`AuthBackend` subclass.
  796. """
  797. cls = entry_point.load()
  798. if not cls.backend_id:
  799. logging.warning('The authentication backend %r did not provide '
  800. 'a backend_id attribute. Setting it to the '
  801. 'entry point name ("%s")',
  802. cls, entry_point.name)
  803. cls.backend_id = entry_point.name
  804. return cls
  805. def get_defaults(self):
  806. """Yield the authentication backends.
  807. This will make sure the StandardAuthBackend is always registered.
  808. Yields:
  809. type:
  810. The :py:class:`~reviewboard.accounts.backends.AuthBackend`
  811. subclasses.
  812. """
  813. yield StandardAuthBackend
  814. for value in super(AuthBackendRegistry, self).get_defaults():
  815. yield value
  816. def unregister(self, backend_class):
  817. """Unregister the requested authentication backend.
  818. Args:
  819. backend_class (type):
  820. The class of the backend to unregister.
  821. Raises:
  822. djblets.registries.errors.ItemLookupError:
  823. Raised when the class cannot be found.
  824. """
  825. self.populate()
  826. try:
  827. super(AuthBackendRegistry, self).unregister(backend_class)
  828. except self.lookup_error_class as e:
  829. logging.error('Failed to unregister unknown authentication '
  830. 'backend "%s".',
  831. backend_class.backend_id)
  832. raise e
  833. def get_auth_backend(self, auth_backend_id):
  834. """Return the requested authentication backend, if it exists.
  835. Args:
  836. auth_backend_id (unicode):
  837. The unique ID of the :py:class:`AuthBackend` class.
  838. Returns:
  839. type:
  840. The :py:class:`AuthBackend` subclass, or ``None`` if it is not
  841. registered.
  842. """
  843. return self.get('auth_backend_id', auth_backend_id)
  844. auth_backends = AuthBackendRegistry()
  845. def get_registered_auth_backends():
  846. """Yield all registered Review Board authentication backends.
  847. This will return all backends provided both by Review Board and by
  848. third parties that have properly registered with the
  849. "reviewboard.auth_backends" entry point.
  850. Yields:
  851. type:
  852. The :py:class:`~reviewboard.accounts.backends.AuthBackend`
  853. subclasses.
  854. .. deprecated:: 3.0
  855. Iterate over :py:data:`~reviewboard.accounts.auth_backends` instead.
  856. """
  857. warn('reviewboard.accounts.get_registered_auth_backends is deprecated. '
  858. 'Iterate over reviewboard.accounts.auth_backends instead.',
  859. DeprecationWarning)
  860. for backend in auth_backends:
  861. yield backend
  862. def get_registered_auth_backend(backend_id):
  863. """Return the authentication backends with the specified ID.
  864. If the authentication backend could not be found, this will return None.
  865. .. deprecated:: 3.0
  866. Use the :py:func:`~AuthBackendRegistry.get_auth_backend` method of
  867. :py:data:`~reviewboard.accounts.auth_backends` instead.
  868. """
  869. warn('reviewboard.accounts.get_registered_auth_backend is deprecated. Use'
  870. 'reviewboard.accounts.auth_backends.register instead.',
  871. DeprecationWarning)
  872. return auth_backends.get('backend_id', backend_id)
  873. def register_auth_backend(backend_cls):
  874. """Register an authentication backend.
  875. This backend will appear in the list of available backends.
  876. The backend class must have a backend_id attribute set, and can only
  877. be registered once. A KeyError will be thrown if attempting to register
  878. a second time.
  879. .. deprecated:: 3.0
  880. Use the :py:func:`~AuthBackendRegistry.register` method of
  881. :py:data:`~reviewboard.accounts.auth_backends` instead.
  882. """
  883. warn('reviewboard.accounts.register_auth_backend is deprecated. Use'
  884. 'reviewboard.accounts.auth_backends.register instead.',
  885. DeprecationWarning)
  886. auth_backends.register(backend_cls)
  887. def unregister_auth_backend(backend_cls):
  888. """Unregister a previously registered authentication backend.
  889. .. deprecated:: 3.0
  890. Use the :py:func:`~AuthBackendRegistry.unregister` method of
  891. :py:data:`~reviewboard.accounts.auth_backends` instead.
  892. """
  893. warn('reviewboard.accounts.unregister_auth_backend is deprecated. Use '
  894. 'reviewboard.accounts.auth_backends.unregister instead.',
  895. DeprecationWarning)
  896. auth_backends.unregister(backend_cls)