utils.py 48 KB

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