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.
 
 
 
 

1234 lines
46 KiB

  1. """passlib.ext.django.utils - helper functions used by this plugin"""
  2. #=============================================================================
  3. # imports
  4. #=============================================================================
  5. # core
  6. from functools import update_wrapper, wraps
  7. import logging; log = logging.getLogger(__name__)
  8. import sys
  9. import weakref
  10. from warnings import warn
  11. # site
  12. try:
  13. from django import VERSION as DJANGO_VERSION
  14. log.debug("found django %r installation", DJANGO_VERSION)
  15. except ImportError:
  16. log.debug("django installation not found")
  17. DJANGO_VERSION = ()
  18. # pkg
  19. from passlib import exc, registry
  20. from passlib.context import CryptContext
  21. from passlib.exc import PasslibRuntimeWarning
  22. from passlib.utils.compat import get_method_function, iteritems, OrderedDict, unicode
  23. from passlib.utils.decor import memoized_property
  24. # local
  25. __all__ = [
  26. "DJANGO_VERSION",
  27. "MIN_DJANGO_VERSION",
  28. "get_preset_config",
  29. "get_django_hasher",
  30. ]
  31. #: minimum version supported by passlib.ext.django
  32. MIN_DJANGO_VERSION = (1, 8)
  33. #=============================================================================
  34. # default policies
  35. #=============================================================================
  36. # map preset names -> passlib.app attrs
  37. _preset_map = {
  38. "django-1.0": "django10_context",
  39. "django-1.4": "django14_context",
  40. "django-1.6": "django16_context",
  41. "django-latest": "django_context",
  42. }
  43. def get_preset_config(name):
  44. """Returns configuration string for one of the preset strings
  45. supported by the ``PASSLIB_CONFIG`` setting.
  46. Currently supported presets:
  47. * ``"passlib-default"`` - default config used by this release of passlib.
  48. * ``"django-default"`` - config matching currently installed django version.
  49. * ``"django-latest"`` - config matching newest django version (currently same as ``"django-1.6"``).
  50. * ``"django-1.0"`` - config used by stock Django 1.0 - 1.3 installs
  51. * ``"django-1.4"`` - config used by stock Django 1.4 installs
  52. * ``"django-1.6"`` - config used by stock Django 1.6 installs
  53. """
  54. # TODO: add preset which includes HASHERS + PREFERRED_HASHERS,
  55. # after having imported any custom hashers. e.g. "django-current"
  56. if name == "django-default":
  57. if not DJANGO_VERSION:
  58. raise ValueError("can't resolve django-default preset, "
  59. "django not installed")
  60. name = "django-1.6"
  61. if name == "passlib-default":
  62. return PASSLIB_DEFAULT
  63. try:
  64. attr = _preset_map[name]
  65. except KeyError:
  66. raise ValueError("unknown preset config name: %r" % name)
  67. import passlib.apps
  68. return getattr(passlib.apps, attr).to_string()
  69. # default context used by passlib 1.6
  70. PASSLIB_DEFAULT = """
  71. [passlib]
  72. ; list of schemes supported by configuration
  73. ; currently all django 1.6, 1.4, and 1.0 hashes,
  74. ; and three common modular crypt format hashes.
  75. schemes =
  76. django_pbkdf2_sha256, django_pbkdf2_sha1, django_bcrypt, django_bcrypt_sha256,
  77. django_salted_sha1, django_salted_md5, django_des_crypt, hex_md5,
  78. sha512_crypt, bcrypt, phpass
  79. ; default scheme to use for new hashes
  80. default = django_pbkdf2_sha256
  81. ; hashes using these schemes will automatically be re-hashed
  82. ; when the user logs in (currently all django 1.0 hashes)
  83. deprecated =
  84. django_pbkdf2_sha1, django_salted_sha1, django_salted_md5,
  85. django_des_crypt, hex_md5
  86. ; sets some common options, including minimum rounds for two primary hashes.
  87. ; if a hash has less than this number of rounds, it will be re-hashed.
  88. sha512_crypt__min_rounds = 80000
  89. django_pbkdf2_sha256__min_rounds = 10000
  90. ; set somewhat stronger iteration counts for ``User.is_staff``
  91. staff__sha512_crypt__default_rounds = 100000
  92. staff__django_pbkdf2_sha256__default_rounds = 12500
  93. ; and even stronger ones for ``User.is_superuser``
  94. superuser__sha512_crypt__default_rounds = 120000
  95. superuser__django_pbkdf2_sha256__default_rounds = 15000
  96. """
  97. #=============================================================================
  98. # helpers
  99. #=============================================================================
  100. #: prefix used to shoehorn passlib's handler names into django hasher namespace
  101. PASSLIB_WRAPPER_PREFIX = "passlib_"
  102. #: prefix used by all the django-specific hash formats in passlib;
  103. #: all of these hashes should have a ``.django_name`` attribute.
  104. DJANGO_COMPAT_PREFIX = "django_"
  105. #: set of hashes w/o "django_" prefix, but which also expose ``.django_name``.
  106. _other_django_hashes = set(["hex_md5"])
  107. def _wrap_method(method):
  108. """wrap method object in bare function"""
  109. @wraps(method)
  110. def wrapper(*args, **kwds):
  111. return method(*args, **kwds)
  112. return wrapper
  113. #=============================================================================
  114. # translator
  115. #=============================================================================
  116. class DjangoTranslator(object):
  117. """
  118. Object which helps translate passlib hasher objects / names
  119. to and from django hasher objects / names.
  120. These methods are wrapped in a class so that results can be cached,
  121. but with the ability to have independant caches, since django hasher
  122. names may / may not correspond to the same instance (or even class).
  123. """
  124. #=============================================================================
  125. # instance attrs
  126. #=============================================================================
  127. #: CryptContext instance
  128. #: (if any -- generally only set by DjangoContextAdapter subclass)
  129. context = None
  130. #: internal cache of passlib hasher -> django hasher instance.
  131. #: key stores weakref to passlib hasher.
  132. _django_hasher_cache = None
  133. #: special case -- unsalted_sha1
  134. _django_unsalted_sha1 = None
  135. #: internal cache of django name -> passlib hasher
  136. #: value stores weakrefs to passlib hasher.
  137. _passlib_hasher_cache = None
  138. #=============================================================================
  139. # init
  140. #=============================================================================
  141. def __init__(self, context=None, **kwds):
  142. super(DjangoTranslator, self).__init__(**kwds)
  143. if context is not None:
  144. self.context = context
  145. self._django_hasher_cache = weakref.WeakKeyDictionary()
  146. self._passlib_hasher_cache = weakref.WeakValueDictionary()
  147. def reset_hashers(self):
  148. self._django_hasher_cache.clear()
  149. self._passlib_hasher_cache.clear()
  150. self._django_unsalted_sha1 = None
  151. def _get_passlib_hasher(self, passlib_name):
  152. """
  153. resolve passlib hasher by name, using context if available.
  154. """
  155. context = self.context
  156. if context is None:
  157. return registry.get_crypt_handler(passlib_name)
  158. else:
  159. return context.handler(passlib_name)
  160. #=============================================================================
  161. # resolve passlib hasher -> django hasher
  162. #=============================================================================
  163. def passlib_to_django_name(self, passlib_name):
  164. """
  165. Convert passlib hasher / name to Django hasher name.
  166. """
  167. return self.passlib_to_django(passlib_name).algorithm
  168. # XXX: add option (in class, or call signature) to always return a wrapper,
  169. # rather than native builtin -- would let HashersTest check that
  170. # our own wrapper + implementations are matching up with their tests.
  171. def passlib_to_django(self, passlib_hasher, cached=True):
  172. """
  173. Convert passlib hasher / name to Django hasher.
  174. :param passlib_hasher:
  175. passlib hasher / name
  176. :returns:
  177. django hasher instance
  178. """
  179. # resolve names to hasher
  180. if not hasattr(passlib_hasher, "name"):
  181. passlib_hasher = self._get_passlib_hasher(passlib_hasher)
  182. # check cache
  183. if cached:
  184. cache = self._django_hasher_cache
  185. try:
  186. return cache[passlib_hasher]
  187. except KeyError:
  188. pass
  189. result = cache[passlib_hasher] = \
  190. self.passlib_to_django(passlib_hasher, cached=False)
  191. return result
  192. # find native equivalent, and return wrapper if there isn't one
  193. django_name = getattr(passlib_hasher, "django_name", None)
  194. if django_name:
  195. return self._create_django_hasher(django_name)
  196. else:
  197. return _PasslibHasherWrapper(passlib_hasher)
  198. _builtin_django_hashers = dict(
  199. md5="MD5PasswordHasher",
  200. )
  201. def _create_django_hasher(self, django_name):
  202. """
  203. helper to create new django hasher by name.
  204. wraps underlying django methods.
  205. """
  206. # if we haven't patched django, can use it directly
  207. module = sys.modules.get("passlib.ext.django.models")
  208. if module is None or not module.adapter.patched:
  209. from django.contrib.auth.hashers import get_hasher
  210. return get_hasher(django_name)
  211. # We've patched django's get_hashers(), so calling django's get_hasher()
  212. # or get_hashers_by_algorithm() would only land us back here.
  213. # As non-ideal workaround, have to use original get_hashers(),
  214. get_hashers = module.adapter._manager.getorig("django.contrib.auth.hashers:get_hashers").__wrapped__
  215. for hasher in get_hashers():
  216. if hasher.algorithm == django_name:
  217. return hasher
  218. # hardcode a few for cases where get_hashers() look won't work.
  219. path = self._builtin_django_hashers.get(django_name)
  220. if path:
  221. if "." not in path:
  222. path = "django.contrib.auth.hashers." + path
  223. from django.utils.module_loading import import_string
  224. return import_string(path)()
  225. raise ValueError("unknown hasher: %r" % django_name)
  226. #=============================================================================
  227. # reverse django -> passlib
  228. #=============================================================================
  229. def django_to_passlib_name(self, django_name):
  230. """
  231. Convert Django hasher / name to Passlib hasher name.
  232. """
  233. return self.django_to_passlib(django_name).name
  234. def django_to_passlib(self, django_name, cached=True):
  235. """
  236. Convert Django hasher / name to Passlib hasher / name.
  237. If present, CryptContext will be checked instead of main registry.
  238. :param django_name:
  239. Django hasher class or algorithm name.
  240. "default" allowed if context provided.
  241. :raises ValueError:
  242. if can't resolve hasher.
  243. :returns:
  244. passlib hasher or name
  245. """
  246. # check for django hasher
  247. if hasattr(django_name, "algorithm"):
  248. # check for passlib adapter
  249. if isinstance(django_name, _PasslibHasherWrapper):
  250. return django_name.passlib_handler
  251. # resolve django hasher -> name
  252. django_name = django_name.algorithm
  253. # check cache
  254. if cached:
  255. cache = self._passlib_hasher_cache
  256. try:
  257. return cache[django_name]
  258. except KeyError:
  259. pass
  260. result = cache[django_name] = \
  261. self.django_to_passlib(django_name, cached=False)
  262. return result
  263. # check if it's an obviously-wrapped name
  264. if django_name.startswith(PASSLIB_WRAPPER_PREFIX):
  265. passlib_name = django_name[len(PASSLIB_WRAPPER_PREFIX):]
  266. return self._get_passlib_hasher(passlib_name)
  267. # resolve default
  268. if django_name == "default":
  269. context = self.context
  270. if context is None:
  271. raise TypeError("can't determine default scheme w/ context")
  272. return context.handler()
  273. # special case: Django uses a separate hasher for "sha1$$digest"
  274. # hashes (unsalted_sha1) and "sha1$salt$digest" (sha1);
  275. # but passlib uses "django_salted_sha1" for both of these.
  276. if django_name == "unsalted_sha1":
  277. django_name = "sha1"
  278. # resolve name
  279. # XXX: bother caching these lists / mapping?
  280. # not needed in long-term due to cache above.
  281. context = self.context
  282. if context is None:
  283. # check registry
  284. # TODO: should make iteration via registry easier
  285. candidates = (
  286. registry.get_crypt_handler(passlib_name)
  287. for passlib_name in registry.list_crypt_handlers()
  288. if passlib_name.startswith(DJANGO_COMPAT_PREFIX) or
  289. passlib_name in _other_django_hashes
  290. )
  291. else:
  292. # check context
  293. candidates = context.schemes(resolve=True)
  294. for handler in candidates:
  295. if getattr(handler, "django_name", None) == django_name:
  296. return handler
  297. # give up
  298. # NOTE: this should only happen for custom django hashers that we don't
  299. # know the equivalents for. _HasherHandler (below) is work in
  300. # progress that would allow us to at least return a wrapper.
  301. raise ValueError("can't translate django name to passlib name: %r" %
  302. (django_name,))
  303. #=============================================================================
  304. # django hasher lookup
  305. #=============================================================================
  306. def resolve_django_hasher(self, django_name, cached=True):
  307. """
  308. Take in a django algorithm name, return django hasher.
  309. """
  310. # check for django hasher
  311. if hasattr(django_name, "algorithm"):
  312. return django_name
  313. # resolve to passlib hasher
  314. passlib_hasher = self.django_to_passlib(django_name, cached=cached)
  315. # special case: Django uses a separate hasher for "sha1$$digest"
  316. # hashes (unsalted_sha1) and "sha1$salt$digest" (sha1);
  317. # but passlib uses "django_salted_sha1" for both of these.
  318. # XXX: this isn't ideal way to handle this. would like to do something
  319. # like pass "django_variant=django_name" into passlib_to_django(),
  320. # and have it cache separate hasher there.
  321. # but that creates a LOT of complication in it's cache structure,
  322. # for what is just one special case.
  323. if django_name == "unsalted_sha1" and passlib_hasher.name == "django_salted_sha1":
  324. if not cached:
  325. return self._create_django_hasher(django_name)
  326. result = self._django_unsalted_sha1
  327. if result is None:
  328. result = self._django_unsalted_sha1 = self._create_django_hasher(django_name)
  329. return result
  330. # lookup corresponding django hasher
  331. return self.passlib_to_django(passlib_hasher, cached=cached)
  332. #=============================================================================
  333. # eoc
  334. #=============================================================================
  335. #=============================================================================
  336. # adapter
  337. #=============================================================================
  338. class DjangoContextAdapter(DjangoTranslator):
  339. """
  340. Object which tries to adapt a Passlib CryptContext object,
  341. using a Django-hasher compatible API.
  342. When installed in django, :mod:`!passlib.ext.django` will create
  343. an instance of this class, and then monkeypatch the appropriate
  344. methods into :mod:`!django.contrib.auth` and other appropriate places.
  345. """
  346. #=============================================================================
  347. # instance attrs
  348. #=============================================================================
  349. #: CryptContext instance we're wrapping
  350. context = None
  351. #: ref to original make_password(),
  352. #: needed to generate usuable passwords that match django
  353. _orig_make_password = None
  354. #: ref to django helper of this name -- not monkeypatched
  355. is_password_usable = None
  356. #: PatchManager instance used to track installation
  357. _manager = None
  358. #: whether config=disabled flag was set
  359. enabled = True
  360. #: patch status
  361. patched = False
  362. #=============================================================================
  363. # init
  364. #=============================================================================
  365. def __init__(self, context=None, get_user_category=None, **kwds):
  366. # init log
  367. self.log = logging.getLogger(__name__ + ".DjangoContextAdapter")
  368. # init parent, filling in default context object
  369. if context is None:
  370. context = CryptContext()
  371. super(DjangoContextAdapter, self).__init__(context=context, **kwds)
  372. # setup user category
  373. if get_user_category:
  374. assert callable(get_user_category)
  375. self.get_user_category = get_user_category
  376. # install lru cache wrappers
  377. from django.utils.lru_cache import lru_cache
  378. self.get_hashers = lru_cache()(self.get_hashers)
  379. # get copy of original make_password
  380. from django.contrib.auth.hashers import make_password
  381. if make_password.__module__.startswith("passlib."):
  382. make_password = _PatchManager.peek_unpatched_func(make_password)
  383. self._orig_make_password = make_password
  384. # get other django helpers
  385. from django.contrib.auth.hashers import is_password_usable
  386. self.is_password_usable = is_password_usable
  387. # init manager
  388. mlog = logging.getLogger(__name__ + ".DjangoContextAdapter._manager")
  389. self._manager = _PatchManager(log=mlog)
  390. def reset_hashers(self):
  391. """
  392. Wrapper to manually reset django's hasher lookup cache
  393. """
  394. # resets cache for .get_hashers() & .get_hashers_by_algorithm()
  395. from django.contrib.auth.hashers import reset_hashers
  396. reset_hashers(setting="PASSWORD_HASHERS")
  397. # reset internal caches
  398. super(DjangoContextAdapter, self).reset_hashers()
  399. #=============================================================================
  400. # django hashers helpers -- hasher lookup
  401. #=============================================================================
  402. # lru_cache()'ed by init
  403. def get_hashers(self):
  404. """
  405. Passlib replacement for get_hashers() --
  406. Return list of available django hasher classes
  407. """
  408. passlib_to_django = self.passlib_to_django
  409. return [passlib_to_django(hasher)
  410. for hasher in self.context.schemes(resolve=True)]
  411. def get_hasher(self, algorithm="default"):
  412. """
  413. Passlib replacement for get_hasher() --
  414. Return django hasher by name
  415. """
  416. return self.resolve_django_hasher(algorithm)
  417. def identify_hasher(self, encoded):
  418. """
  419. Passlib replacement for identify_hasher() --
  420. Identify django hasher based on hash.
  421. """
  422. handler = self.context.identify(encoded, resolve=True, required=True)
  423. if handler.name == "django_salted_sha1" and encoded.startswith("sha1$$"):
  424. # Django uses a separate hasher for "sha1$$digest" hashes, but
  425. # passlib identifies it as belonging to "sha1$salt$digest" handler.
  426. # We want to resolve to correct django hasher.
  427. return self.get_hasher("unsalted_sha1")
  428. return self.passlib_to_django(handler)
  429. #=============================================================================
  430. # django.contrib.auth.hashers helpers -- password helpers
  431. #=============================================================================
  432. def make_password(self, password, salt=None, hasher="default"):
  433. """
  434. Passlib replacement for make_password()
  435. """
  436. if password is None:
  437. return self._orig_make_password(None)
  438. # NOTE: relying on hasher coming from context, and thus having
  439. # context-specific config baked into it.
  440. passlib_hasher = self.django_to_passlib(hasher)
  441. if "salt" not in passlib_hasher.setting_kwds:
  442. # ignore salt param even if preset
  443. pass
  444. elif hasher.startswith("unsalted_"):
  445. # Django uses a separate 'unsalted_sha1' hasher for "sha1$$digest",
  446. # but passlib just reuses it's "sha1" handler ("sha1$salt$digest"). To make
  447. # this work, have to explicitly tell the sha1 handler to use an empty salt.
  448. passlib_hasher = passlib_hasher.using(salt="")
  449. elif salt:
  450. # Django make_password() autogenerates a salt if salt is bool False (None / ''),
  451. # so we only pass the keyword on if there's actually a fixed salt.
  452. passlib_hasher = passlib_hasher.using(salt=salt)
  453. return passlib_hasher.hash(password)
  454. def check_password(self, password, encoded, setter=None, preferred="default"):
  455. """
  456. Passlib replacement for check_password()
  457. """
  458. # XXX: this currently ignores "preferred" keyword, since its purpose
  459. # was for hash migration, and that's handled by the context.
  460. if password is None or not self.is_password_usable(encoded):
  461. return False
  462. # verify password
  463. context = self.context
  464. correct = context.verify(password, encoded)
  465. if not (correct and setter):
  466. return correct
  467. # check if we need to rehash
  468. if preferred == "default":
  469. if not context.needs_update(encoded, secret=password):
  470. return correct
  471. else:
  472. # Django's check_password() won't call setter() on a
  473. # 'preferred' alg, even if it's otherwise deprecated. To try and
  474. # replicate this behavior if preferred is set, we look up the
  475. # passlib hasher, and call it's original needs_update() method.
  476. # TODO: Solve redundancy that verify() call
  477. # above is already identifying hash.
  478. hasher = self.django_to_passlib(preferred)
  479. if (hasher.identify(encoded) and
  480. not hasher.needs_update(encoded, secret=password)):
  481. # alg is 'preferred' and hash itself doesn't need updating,
  482. # so nothing to do.
  483. return correct
  484. # else: either hash isn't preferred, or it needs updating.
  485. # call setter to rehash
  486. setter(password)
  487. return correct
  488. #=============================================================================
  489. # django users helpers
  490. #=============================================================================
  491. def user_check_password(self, user, password):
  492. """
  493. Passlib replacement for User.check_password()
  494. """
  495. if password is None:
  496. return False
  497. hash = user.password
  498. if not self.is_password_usable(hash):
  499. return False
  500. cat = self.get_user_category(user)
  501. ok, new_hash = self.context.verify_and_update(password, hash,
  502. category=cat)
  503. if ok and new_hash is not None:
  504. # migrate to new hash if needed.
  505. user.password = new_hash
  506. user.save()
  507. return ok
  508. def user_set_password(self, user, password):
  509. """
  510. Passlib replacement for User.set_password()
  511. """
  512. if password is None:
  513. user.set_unusable_password()
  514. else:
  515. cat = self.get_user_category(user)
  516. user.password = self.context.hash(password, category=cat)
  517. def get_user_category(self, user):
  518. """
  519. Helper for hashing passwords per-user --
  520. figure out the CryptContext category for specified Django user object.
  521. .. note::
  522. This may be overridden via PASSLIB_GET_CATEGORY django setting
  523. """
  524. if user.is_superuser:
  525. return "superuser"
  526. elif user.is_staff:
  527. return "staff"
  528. else:
  529. return None
  530. #=============================================================================
  531. # patch control
  532. #=============================================================================
  533. HASHERS_PATH = "django.contrib.auth.hashers"
  534. MODELS_PATH = "django.contrib.auth.models"
  535. USER_CLASS_PATH = MODELS_PATH + ":User"
  536. FORMS_PATH = "django.contrib.auth.forms"
  537. #: list of locations to patch
  538. patch_locations = [
  539. #
  540. # User object
  541. # NOTE: could leave defaults alone, but want to have user available
  542. # so that we can support get_user_category()
  543. #
  544. (USER_CLASS_PATH + ".check_password", "user_check_password", dict(method=True)),
  545. (USER_CLASS_PATH + ".set_password", "user_set_password", dict(method=True)),
  546. #
  547. # Hashers module
  548. #
  549. (HASHERS_PATH + ":", "check_password"),
  550. (HASHERS_PATH + ":", "make_password"),
  551. (HASHERS_PATH + ":", "get_hashers"),
  552. (HASHERS_PATH + ":", "get_hasher"),
  553. (HASHERS_PATH + ":", "identify_hasher"),
  554. #
  555. # Patch known imports from hashers module
  556. #
  557. (MODELS_PATH + ":", "check_password"),
  558. (MODELS_PATH + ":", "make_password"),
  559. (FORMS_PATH + ":", "get_hasher"),
  560. (FORMS_PATH + ":", "identify_hasher"),
  561. ]
  562. def install_patch(self):
  563. """
  564. Install monkeypatch to replace django hasher framework.
  565. """
  566. # don't reapply
  567. log = self.log
  568. if self.patched:
  569. log.warning("monkeypatching already applied, refusing to reapply")
  570. return False
  571. # version check
  572. if DJANGO_VERSION < MIN_DJANGO_VERSION:
  573. raise RuntimeError("passlib.ext.django requires django >= %s" %
  574. (MIN_DJANGO_VERSION,))
  575. # log start
  576. log.debug("preparing to monkeypatch django ...")
  577. # run through patch locations
  578. manager = self._manager
  579. for record in self.patch_locations:
  580. if len(record) == 2:
  581. record += ({},)
  582. target, source, opts = record
  583. if target.endswith((":", ",")):
  584. target += source
  585. value = getattr(self, source)
  586. if opts.get("method"):
  587. # have to wrap our method in a function,
  588. # since we're installing it in a class *as* a method
  589. # XXX: make this a flag for .patch()?
  590. value = _wrap_method(value)
  591. manager.patch(target, value)
  592. # reset django's caches (e.g. get_hash_by_algorithm)
  593. self.reset_hashers()
  594. # done!
  595. self.patched = True
  596. log.debug("... finished monkeypatching django")
  597. return True
  598. def remove_patch(self):
  599. """
  600. Remove monkeypatch from django hasher framework.
  601. As precaution in case there are lingering refs to context,
  602. context object will be wiped.
  603. .. warning::
  604. This may cause problems if any other Django modules have imported
  605. their own copies of the patched functions, though the patched
  606. code has been designed to throw an error as soon as possible in
  607. this case.
  608. """
  609. log = self.log
  610. manager = self._manager
  611. if self.patched:
  612. log.debug("removing django monkeypatching...")
  613. manager.unpatch_all(unpatch_conflicts=True)
  614. self.context.load({})
  615. self.patched = False
  616. self.reset_hashers()
  617. log.debug("...finished removing django monkeypatching")
  618. return True
  619. if manager.isactive(): # pragma: no cover -- sanity check
  620. log.warning("reverting partial monkeypatching of django...")
  621. manager.unpatch_all()
  622. self.context.load({})
  623. self.reset_hashers()
  624. log.debug("...finished removing django monkeypatching")
  625. return True
  626. log.debug("django not monkeypatched")
  627. return False
  628. #=============================================================================
  629. # loading config
  630. #=============================================================================
  631. def load_model(self):
  632. """
  633. Load configuration from django, and install patch.
  634. """
  635. self._load_settings()
  636. if self.enabled:
  637. try:
  638. self.install_patch()
  639. except:
  640. # try to undo what we can
  641. self.remove_patch()
  642. raise
  643. else:
  644. if self.patched: # pragma: no cover -- sanity check
  645. log.error("didn't expect monkeypatching would be applied!")
  646. self.remove_patch()
  647. log.debug("passlib.ext.django loaded")
  648. def _load_settings(self):
  649. """
  650. Update settings from django
  651. """
  652. from django.conf import settings
  653. # TODO: would like to add support for inheriting config from a preset
  654. # (or from existing hasher state) and letting PASSLIB_CONFIG
  655. # be an update, not a replacement.
  656. # TODO: wrap and import any custom hashers as passlib handlers,
  657. # so they could be used in the passlib config.
  658. # load config from settings
  659. _UNSET = object()
  660. config = getattr(settings, "PASSLIB_CONFIG", _UNSET)
  661. if config is _UNSET:
  662. # XXX: should probably deprecate this alias
  663. config = getattr(settings, "PASSLIB_CONTEXT", _UNSET)
  664. if config is _UNSET:
  665. config = "passlib-default"
  666. if config is None:
  667. warn("setting PASSLIB_CONFIG=None is deprecated, "
  668. "and support will be removed in Passlib 1.8, "
  669. "use PASSLIB_CONFIG='disabled' instead.",
  670. DeprecationWarning)
  671. config = "disabled"
  672. elif not isinstance(config, (unicode, bytes, dict)):
  673. raise exc.ExpectedTypeError(config, "str or dict", "PASSLIB_CONFIG")
  674. # load custom category func (if any)
  675. get_category = getattr(settings, "PASSLIB_GET_CATEGORY", None)
  676. if get_category and not callable(get_category):
  677. raise exc.ExpectedTypeError(get_category, "callable", "PASSLIB_GET_CATEGORY")
  678. # check if we've been disabled
  679. if config == "disabled":
  680. self.enabled = False
  681. return
  682. else:
  683. self.__dict__.pop("enabled", None)
  684. # resolve any preset aliases
  685. if isinstance(config, str) and '\n' not in config:
  686. config = get_preset_config(config)
  687. # setup category func
  688. if get_category:
  689. self.get_user_category = get_category
  690. else:
  691. self.__dict__.pop("get_category", None)
  692. # setup context
  693. self.context.load(config)
  694. self.reset_hashers()
  695. #=============================================================================
  696. # eof
  697. #=============================================================================
  698. #=============================================================================
  699. # wrapping passlib handlers as django hashers
  700. #=============================================================================
  701. _GEN_SALT_SIGNAL = "--!!!generate-new-salt!!!--"
  702. class ProxyProperty(object):
  703. """helper that proxies another attribute"""
  704. def __init__(self, attr):
  705. self.attr = attr
  706. def __get__(self, obj, cls):
  707. if obj is None:
  708. cls = obj
  709. return getattr(obj, self.attr)
  710. def __set__(self, obj, value):
  711. setattr(obj, self.attr, value)
  712. def __delete__(self, obj):
  713. delattr(obj, self.attr)
  714. class _PasslibHasherWrapper(object):
  715. """
  716. adapter which which wraps a :cls:`passlib.ifc.PasswordHash` class,
  717. and provides an interface compatible with the Django hasher API.
  718. :param passlib_handler:
  719. passlib hash handler (e.g. :cls:`passlib.hash.sha256_crypt`.
  720. """
  721. #=====================================================================
  722. # instance attrs
  723. #=====================================================================
  724. #: passlib handler that we're adapting.
  725. passlib_handler = None
  726. # NOTE: 'rounds' attr will store variable rounds, IF handler supports it.
  727. # 'iterations' will act as proxy, for compatibility with django pbkdf2 hashers.
  728. # rounds = None
  729. # iterations = None
  730. #=====================================================================
  731. # init
  732. #=====================================================================
  733. def __init__(self, passlib_handler):
  734. # init handler
  735. if getattr(passlib_handler, "django_name", None):
  736. raise ValueError("handlers that reflect an official django "
  737. "hasher shouldn't be wrapped: %r" %
  738. (passlib_handler.name,))
  739. if passlib_handler.is_disabled:
  740. # XXX: could this be implemented?
  741. raise ValueError("can't wrap disabled-hash handlers: %r" %
  742. (passlib_handler.name))
  743. self.passlib_handler = passlib_handler
  744. # init rounds support
  745. if self._has_rounds:
  746. self.rounds = passlib_handler.default_rounds
  747. self.iterations = ProxyProperty("rounds")
  748. #=====================================================================
  749. # internal methods
  750. #=====================================================================
  751. def __repr__(self):
  752. return "<PasslibHasherWrapper handler=%r>" % self.passlib_handler
  753. #=====================================================================
  754. # internal properties
  755. #=====================================================================
  756. @memoized_property
  757. def __name__(self):
  758. return "Passlib_%s_PasswordHasher" % self.passlib_handler.name.title()
  759. @memoized_property
  760. def _has_rounds(self):
  761. return "rounds" in self.passlib_handler.setting_kwds
  762. @memoized_property
  763. def _translate_kwds(self):
  764. """
  765. internal helper for safe_summary() --
  766. used to translate passlib hash options -> django keywords
  767. """
  768. out = dict(checksum="hash")
  769. if self._has_rounds and "pbkdf2" in self.passlib_handler.name:
  770. out['rounds'] = 'iterations'
  771. return out
  772. #=====================================================================
  773. # hasher properties
  774. #=====================================================================
  775. @memoized_property
  776. def algorithm(self):
  777. return PASSLIB_WRAPPER_PREFIX + self.passlib_handler.name
  778. #=====================================================================
  779. # hasher api
  780. #=====================================================================
  781. def salt(self):
  782. # NOTE: passlib's handler.hash() should generate new salt each time,
  783. # so this just returns a special constant which tells
  784. # encode() (below) not to pass a salt keyword along.
  785. return _GEN_SALT_SIGNAL
  786. def verify(self, password, encoded):
  787. return self.passlib_handler.verify(password, encoded)
  788. def encode(self, password, salt=None, rounds=None, iterations=None):
  789. kwds = {}
  790. if salt is not None and salt != _GEN_SALT_SIGNAL:
  791. kwds['salt'] = salt
  792. if self._has_rounds:
  793. if rounds is not None:
  794. kwds['rounds'] = rounds
  795. elif iterations is not None:
  796. kwds['rounds'] = iterations
  797. else:
  798. kwds['rounds'] = self.rounds
  799. elif rounds is not None or iterations is not None:
  800. warn("%s.hash(): 'rounds' and 'iterations' are ignored" % self.__name__)
  801. handler = self.passlib_handler
  802. if kwds:
  803. handler = handler.using(**kwds)
  804. return handler.hash(password)
  805. def safe_summary(self, encoded):
  806. from django.contrib.auth.hashers import mask_hash
  807. from django.utils.translation import ugettext_noop as _
  808. handler = self.passlib_handler
  809. items = [
  810. # since this is user-facing, we're reporting passlib's name,
  811. # without the distracting PASSLIB_HASHER_PREFIX prepended.
  812. (_('algorithm'), handler.name),
  813. ]
  814. if hasattr(handler, "parsehash"):
  815. kwds = handler.parsehash(encoded, sanitize=mask_hash)
  816. for key, value in iteritems(kwds):
  817. key = self._translate_kwds.get(key, key)
  818. items.append((_(key), value))
  819. return OrderedDict(items)
  820. def must_update(self, encoded):
  821. # TODO: would like access CryptContext, would need caller to pass it to get_passlib_hasher().
  822. # for now (as of passlib 1.6.6), replicating django policy that this returns True
  823. # if 'encoded' hash has different rounds value from self.rounds
  824. if self._has_rounds:
  825. # XXX: could cache this subclass somehow (would have to intercept writes to self.rounds)
  826. # TODO: always call subcls/handler.needs_update() in case there's other things to check
  827. subcls = self.passlib_handler.using(min_rounds=self.rounds, max_rounds=self.rounds)
  828. if subcls.needs_update(encoded):
  829. return True
  830. return False
  831. #=====================================================================
  832. # eoc
  833. #=====================================================================
  834. #=============================================================================
  835. # adapting django hashers -> passlib handlers
  836. #=============================================================================
  837. # TODO: this code probably halfway works, mainly just needs
  838. # a routine to read HASHERS and PREFERRED_HASHER.
  839. ##from passlib.registry import register_crypt_handler
  840. ##from passlib.utils import classproperty, to_native_str, to_unicode
  841. ##from passlib.utils.compat import unicode
  842. ##
  843. ##
  844. ##class _HasherHandler(object):
  845. ## "helper for wrapping Hasher instances as passlib handlers"
  846. ## # FIXME: this generic wrapper doesn't handle custom settings
  847. ## # FIXME: genconfig / genhash not supported.
  848. ##
  849. ## def __init__(self, hasher):
  850. ## self.django_hasher = hasher
  851. ## if hasattr(hasher, "iterations"):
  852. ## # assume encode() accepts an "iterations" parameter.
  853. ## # fake min/max rounds
  854. ## self.min_rounds = 1
  855. ## self.max_rounds = 0xFFFFffff
  856. ## self.default_rounds = self.django_hasher.iterations
  857. ## self.setting_kwds += ("rounds",)
  858. ##
  859. ## # hasher instance - filled in by constructor
  860. ## django_hasher = None
  861. ##
  862. ## setting_kwds = ("salt",)
  863. ## context_kwds = ()
  864. ##
  865. ## @property
  866. ## def name(self):
  867. ## # XXX: need to make sure this wont' collide w/ builtin django hashes.
  868. ## # maybe by renaming this to django compatible aliases?
  869. ## return DJANGO_PASSLIB_PREFIX + self.django_name
  870. ##
  871. ## @property
  872. ## def django_name(self):
  873. ## # expose this so hasher_to_passlib_name() extracts original name
  874. ## return self.django_hasher.algorithm
  875. ##
  876. ## @property
  877. ## def ident(self):
  878. ## # this should always be correct, as django relies on ident prefix.
  879. ## return unicode(self.django_name + "$")
  880. ##
  881. ## @property
  882. ## def identify(self, hash):
  883. ## # this should always work, as django relies on ident prefix.
  884. ## return to_unicode(hash, "latin-1", "hash").startswith(self.ident)
  885. ##
  886. ## @property
  887. ## def hash(self, secret, salt=None, **kwds):
  888. ## # NOTE: from how make_password() is coded, all hashers
  889. ## # should have salt param. but only some will have
  890. ## # 'iterations' parameter.
  891. ## opts = {}
  892. ## if 'rounds' in self.setting_kwds and 'rounds' in kwds:
  893. ## opts['iterations'] = kwds.pop("rounds")
  894. ## if kwds:
  895. ## raise TypeError("unexpected keyword arguments: %r" % list(kwds))
  896. ## if isinstance(secret, unicode):
  897. ## secret = secret.encode("utf-8")
  898. ## if salt is None:
  899. ## salt = self.django_hasher.salt()
  900. ## return to_native_str(self.django_hasher(secret, salt, **opts))
  901. ##
  902. ## @property
  903. ## def verify(self, secret, hash):
  904. ## hash = to_native_str(hash, "utf-8", "hash")
  905. ## if isinstance(secret, unicode):
  906. ## secret = secret.encode("utf-8")
  907. ## return self.django_hasher.verify(secret, hash)
  908. ##
  909. ##def register_hasher(hasher):
  910. ## handler = _HasherHandler(hasher)
  911. ## register_crypt_handler(handler)
  912. ## return handler
  913. #=============================================================================
  914. # monkeypatch helpers
  915. #=============================================================================
  916. # private singleton indicating lack-of-value
  917. _UNSET = object()
  918. class _PatchManager(object):
  919. """helper to manage monkeypatches and run sanity checks"""
  920. # NOTE: this could easily use a dict interface,
  921. # but keeping it distinct to make clear that it's not a dict,
  922. # since it has important side-effects.
  923. #===================================================================
  924. # init and support
  925. #===================================================================
  926. def __init__(self, log=None):
  927. # map of key -> (original value, patched value)
  928. # original value may be _UNSET
  929. self.log = log or logging.getLogger(__name__ + "._PatchManager")
  930. self._state = {}
  931. def isactive(self):
  932. return bool(self._state)
  933. # bool value tests if any patches are currently applied.
  934. # NOTE: this behavior is deprecated in favor of .isactive
  935. __bool__ = __nonzero__ = isactive
  936. def _import_path(self, path):
  937. """retrieve obj and final attribute name from resource path"""
  938. name, attr = path.split(":")
  939. obj = __import__(name, fromlist=[attr], level=0)
  940. while '.' in attr:
  941. head, attr = attr.split(".", 1)
  942. obj = getattr(obj, head)
  943. return obj, attr
  944. @staticmethod
  945. def _is_same_value(left, right):
  946. """check if two values are the same (stripping method wrappers, etc)"""
  947. return get_method_function(left) == get_method_function(right)
  948. #===================================================================
  949. # reading
  950. #===================================================================
  951. def _get_path(self, key, default=_UNSET):
  952. obj, attr = self._import_path(key)
  953. return getattr(obj, attr, default)
  954. def get(self, path, default=None):
  955. """return current value for path"""
  956. return self._get_path(path, default)
  957. def getorig(self, path, default=None):
  958. """return original (unpatched) value for path"""
  959. try:
  960. value, _= self._state[path]
  961. except KeyError:
  962. value = self._get_path(path)
  963. return default if value is _UNSET else value
  964. def check_all(self, strict=False):
  965. """run sanity check on all keys, issue warning if out of sync"""
  966. same = self._is_same_value
  967. for path, (orig, expected) in iteritems(self._state):
  968. if same(self._get_path(path), expected):
  969. continue
  970. msg = "another library has patched resource: %r" % path
  971. if strict:
  972. raise RuntimeError(msg)
  973. else:
  974. warn(msg, PasslibRuntimeWarning)
  975. #===================================================================
  976. # patching
  977. #===================================================================
  978. def _set_path(self, path, value):
  979. obj, attr = self._import_path(path)
  980. if value is _UNSET:
  981. if hasattr(obj, attr):
  982. delattr(obj, attr)
  983. else:
  984. setattr(obj, attr, value)
  985. def patch(self, path, value, wrap=False):
  986. """monkeypatch object+attr at <path> to have <value>, stores original"""
  987. assert value != _UNSET
  988. current = self._get_path(path)
  989. try:
  990. orig, expected = self._state[path]
  991. except KeyError:
  992. self.log.debug("patching resource: %r", path)
  993. orig = current
  994. else:
  995. self.log.debug("modifying resource: %r", path)
  996. if not self._is_same_value(current, expected):
  997. warn("overridding resource another library has patched: %r"
  998. % path, PasslibRuntimeWarning)
  999. if wrap:
  1000. assert callable(value)
  1001. wrapped = orig
  1002. wrapped_by = value
  1003. def wrapper(*args, **kwds):
  1004. return wrapped_by(wrapped, *args, **kwds)
  1005. update_wrapper(wrapper, value)
  1006. value = wrapper
  1007. if callable(value):
  1008. # needed by DjangoContextAdapter init
  1009. get_method_function(value)._patched_original_value = orig
  1010. self._set_path(path, value)
  1011. self._state[path] = (orig, value)
  1012. @classmethod
  1013. def peek_unpatched_func(cls, value):
  1014. return value._patched_original_value
  1015. ##def patch_many(self, **kwds):
  1016. ## "override specified resources with new values"
  1017. ## for path, value in iteritems(kwds):
  1018. ## self.patch(path, value)
  1019. def monkeypatch(self, parent, name=None, enable=True, wrap=False):
  1020. """function decorator which patches function of same name in <parent>"""
  1021. def builder(func):
  1022. if enable:
  1023. sep = "." if ":" in parent else ":"
  1024. path = parent + sep + (name or func.__name__)
  1025. self.patch(path, func, wrap=wrap)
  1026. return func
  1027. if callable(name):
  1028. # called in non-decorator mode
  1029. func = name
  1030. name = None
  1031. builder(func)
  1032. return None
  1033. return builder
  1034. #===================================================================
  1035. # unpatching
  1036. #===================================================================
  1037. def unpatch(self, path, unpatch_conflicts=True):
  1038. try:
  1039. orig, expected = self._state[path]
  1040. except KeyError:
  1041. return
  1042. current = self._get_path(path)
  1043. self.log.debug("unpatching resource: %r", path)
  1044. if not self._is_same_value(current, expected):
  1045. if unpatch_conflicts:
  1046. warn("reverting resource another library has patched: %r"
  1047. % path, PasslibRuntimeWarning)
  1048. else:
  1049. warn("not reverting resource another library has patched: %r"
  1050. % path, PasslibRuntimeWarning)
  1051. del self._state[path]
  1052. return
  1053. self._set_path(path, orig)
  1054. del self._state[path]
  1055. def unpatch_all(self, **kwds):
  1056. for key in list(self._state):
  1057. self.unpatch(key, **kwds)
  1058. #===================================================================
  1059. # eoc
  1060. #===================================================================
  1061. #=============================================================================
  1062. # eof
  1063. #=============================================================================