Author: jezdez
Date: 2011-06-26 09:51:46 -0700 (Sun, 26 Jun 2011)
New Revision: 16456

Added:
   django/trunk/django/contrib/auth/utils.py
Modified:
   django/trunk/django/contrib/auth/models.py
   django/trunk/django/contrib/auth/tests/__init__.py
   django/trunk/django/contrib/auth/tests/basic.py
   django/trunk/docs/releases/1.4.txt
   django/trunk/docs/topics/auth.txt
Log:
Fixed #14390 and #16262 -- Moved password related functions from auth models to 
utils module and stopped check_password from throwing an exception. Thanks, 
subsume and lrekucki.

Modified: django/trunk/django/contrib/auth/models.py
===================================================================
--- django/trunk/django/contrib/auth/models.py  2011-06-26 16:51:34 UTC (rev 
16455)
+++ django/trunk/django/contrib/auth/models.py  2011-06-26 16:51:46 UTC (rev 
16456)
@@ -1,64 +1,20 @@
 import datetime
-import hashlib
-import random
 import urllib
 
-from django.contrib import auth
-from django.contrib.auth.signals import user_logged_in
 from django.core.exceptions import ImproperlyConfigured
 from django.core.mail import send_mail
 from django.db import models
 from django.db.models.manager import EmptyManager
-from django.contrib.contenttypes.models import ContentType
 from django.utils.encoding import smart_str
 from django.utils.translation import ugettext_lazy as _
-from django.utils.crypto import constant_time_compare
 
+from django.contrib import auth
+from django.contrib.auth.signals import user_logged_in
+from django.contrib.auth.utils import (get_hexdigest, make_password,
+                                       check_password, is_password_usable,
+                                       get_random_string, UNUSABLE_PASSWORD)
+from django.contrib.contenttypes.models import ContentType
 
-UNUSABLE_PASSWORD = '!' # This will never be a valid hash
-
-def get_hexdigest(algorithm, salt, raw_password):
-    """
-    Returns a string of the hexdigest of the given plaintext password and salt
-    using the given algorithm ('md5', 'sha1' or 'crypt').
-    """
-    raw_password, salt = smart_str(raw_password), smart_str(salt)
-    if algorithm == 'crypt':
-        try:
-            import crypt
-        except ImportError:
-            raise ValueError('"crypt" password algorithm not supported in this 
environment')
-        return crypt.crypt(raw_password, salt)
-
-    if algorithm == 'md5':
-        return hashlib.md5(salt + raw_password).hexdigest()
-    elif algorithm == 'sha1':
-        return hashlib.sha1(salt + raw_password).hexdigest()
-    raise ValueError("Got unknown password algorithm type in password.")
-
-def get_random_string(length=12, 
allowed_chars='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'):
-    """
-    Returns a random string of length characters from the set of a-z, A-Z, 0-9
-    for use as a salt.
-
-    The default length of 12 with the a-z, A-Z, 0-9 character set returns
-    a 71-bit salt. log_2((26+26+10)^12) =~ 71 bits
-    """
-    import random
-    try:
-        random = random.SystemRandom()
-    except NotImplementedError:
-        pass
-    return ''.join([random.choice(allowed_chars) for i in range(length)])
-
-def check_password(raw_password, enc_password):
-    """
-    Returns a boolean of whether the raw_password was correct. Handles
-    encryption formats behind the scenes.
-    """
-    algo, salt, hsh = enc_password.split('$')
-    return constant_time_compare(hsh, get_hexdigest(algo, salt, raw_password))
-
 def update_last_login(sender, user, **kwargs):
     """
     A signal receiver which updates the last_login date for
@@ -270,13 +226,7 @@
         return full_name.strip()
 
     def set_password(self, raw_password):
-        if raw_password is None:
-            self.set_unusable_password()
-        else:
-            algo = 'sha1'
-            salt = get_random_string()
-            hsh = get_hexdigest(algo, salt, raw_password)
-            self.password = '%s$%s$%s' % (algo, salt, hsh)
+        self.password = make_password('sha1', raw_password)
 
     def check_password(self, raw_password):
         """
@@ -296,14 +246,10 @@
 
     def set_unusable_password(self):
         # Sets a value that will never be a valid hash
-        self.password = UNUSABLE_PASSWORD
+        self.password = make_password('sha1', None)
 
     def has_usable_password(self):
-        if self.password is None \
-            or self.password == UNUSABLE_PASSWORD:
-            return False
-        else:
-            return True
+        return is_password_usable(self.password)
 
     def get_group_permissions(self, obj=None):
         """

Modified: django/trunk/django/contrib/auth/tests/__init__.py
===================================================================
--- django/trunk/django/contrib/auth/tests/__init__.py  2011-06-26 16:51:34 UTC 
(rev 16455)
+++ django/trunk/django/contrib/auth/tests/__init__.py  2011-06-26 16:51:46 UTC 
(rev 16456)
@@ -1,7 +1,7 @@
 from django.contrib.auth.tests.auth_backends import (BackendTest,
     RowlevelBackendTest, AnonymousUserBackendTest, NoAnonymousUserBackendTest,
     NoBackendsTest, InActiveUserBackendTest, NoInActiveUserBackendTest)
-from django.contrib.auth.tests.basic import BasicTestCase
+from django.contrib.auth.tests.basic import BasicTestCase, 
PasswordUtilsTestCase
 from django.contrib.auth.tests.context_processors import 
AuthContextProcessorTests
 from django.contrib.auth.tests.decorators import LoginRequiredTestCase
 from django.contrib.auth.tests.forms import (UserCreationFormTest,

Modified: django/trunk/django/contrib/auth/tests/basic.py
===================================================================
--- django/trunk/django/contrib/auth/tests/basic.py     2011-06-26 16:51:34 UTC 
(rev 16455)
+++ django/trunk/django/contrib/auth/tests/basic.py     2011-06-26 16:51:46 UTC 
(rev 16456)
@@ -1,8 +1,16 @@
 from django.test import TestCase
+from django.utils.unittest import skipUnless
 from django.contrib.auth.models import User, AnonymousUser
+from django.contrib.auth import utils
 from django.core.management import call_command
 from StringIO import StringIO
 
+try:
+    import crypt as crypt_module
+except ImportError:
+    crypt_module = None
+
+
 class BasicTestCase(TestCase):
     def test_user(self):
         "Check that users can be created and can set their password"
@@ -93,3 +101,29 @@
         self.assertEqual(u.email, '[email protected]')
         self.assertFalse(u.has_usable_password())
 
+
+class PasswordUtilsTestCase(TestCase):
+
+    def _test_make_password(self, algo):
+        password = utils.make_password(algo, "foobar")
+        self.assertTrue(utils.is_password_usable(password))
+        self.assertTrue(utils.check_password("foobar", password))
+
+    def test_make_unusable(self):
+        "Check that you can create an unusable password."
+        password = utils.make_password("any", None)
+        self.assertFalse(utils.is_password_usable(password))
+        self.assertFalse(utils.check_password("foobar", password))
+
+    def test_make_password_sha1(self):
+        "Check creating passwords with SHA1 algorithm."
+        self._test_make_password("sha1")
+
+    def test_make_password_md5(self):
+        "Check creating passwords with MD5 algorithm."
+        self._test_make_password("md5")
+
+    @skipUnless(crypt_module, "no crypt module to generate password.")
+    def test_make_password_crypt(self):
+        "Check creating passwords with CRYPT algorithm."
+        self._test_make_password("crypt")

Added: django/trunk/django/contrib/auth/utils.py
===================================================================
--- django/trunk/django/contrib/auth/utils.py                           (rev 0)
+++ django/trunk/django/contrib/auth/utils.py   2011-06-26 16:51:46 UTC (rev 
16456)
@@ -0,0 +1,63 @@
+import hashlib
+from django.utils.encoding import smart_str
+from django.utils.crypto import constant_time_compare
+
+UNUSABLE_PASSWORD = '!' # This will never be a valid hash
+
+def get_hexdigest(algorithm, salt, raw_password):
+    """
+    Returns a string of the hexdigest of the given plaintext password and salt
+    using the given algorithm ('md5', 'sha1' or 'crypt').
+    """
+    raw_password, salt = smart_str(raw_password), smart_str(salt)
+    if algorithm == 'crypt':
+        try:
+            import crypt
+        except ImportError:
+            raise ValueError('"crypt" password algorithm not supported in this 
environment')
+        return crypt.crypt(raw_password, salt)
+
+    if algorithm == 'md5':
+        return hashlib.md5(salt + raw_password).hexdigest()
+    elif algorithm == 'sha1':
+        return hashlib.sha1(salt + raw_password).hexdigest()
+    raise ValueError("Got unknown password algorithm type in password.")
+
+def get_random_string(length=12, 
allowed_chars='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'):
+    """
+    Returns a random string of length characters from the set of a-z, A-Z, 0-9
+    for use as a salt.
+
+    The default length of 12 with the a-z, A-Z, 0-9 character set returns
+    a 71-bit salt. log_2((26+26+10)^12) =~ 71 bits
+    """
+    import random
+    try:
+        random = random.SystemRandom()
+    except NotImplementedError:
+        pass
+    return ''.join([random.choice(allowed_chars) for i in range(length)])
+
+def check_password(raw_password, enc_password):
+    """
+    Returns a boolean of whether the raw_password was correct. Handles
+    encryption formats behind the scenes.
+    """
+    parts = enc_password.split('$')
+    if len(parts) != 3:
+        return False
+    algo, salt, hsh = parts
+    return constant_time_compare(hsh, get_hexdigest(algo, salt, raw_password))
+
+def is_password_usable(encoded_password):
+    return encoded_password is not None and encoded_password != 
UNUSABLE_PASSWORD
+
+def make_password(algo, raw_password):
+    """
+    Produce a new password string in this format: algorithm$salt$hash
+    """
+    if raw_password is None:
+        return UNUSABLE_PASSWORD
+    salt = get_random_string()
+    hsh = get_hexdigest(algo, salt, raw_password)
+    return '%s$%s$%s' % (algo, salt, hsh)

Modified: django/trunk/docs/releases/1.4.txt
===================================================================
--- django/trunk/docs/releases/1.4.txt  2011-06-26 16:51:34 UTC (rev 16455)
+++ django/trunk/docs/releases/1.4.txt  2011-06-26 16:51:46 UTC (rev 16456)
@@ -192,6 +192,11 @@
 * In the documentation, a helpful :doc:`security overview </topics/security>`
   page.
 
+* Function :func:`django.contrib.auth.models.check_password` has been moved
+  to the :mod:`django.contrib.auth.utils` module. Importing it from the old
+  location will still work, but you should update your imports.
+
+
 .. _backwards-incompatible-changes-1.4:
 
 Backwards incompatible changes in 1.4

Modified: django/trunk/docs/topics/auth.txt
===================================================================
--- django/trunk/docs/topics/auth.txt   2011-06-26 16:51:34 UTC (rev 16455)
+++ django/trunk/docs/topics/auth.txt   2011-06-26 16:51:46 UTC (rev 16456)
@@ -627,20 +627,45 @@
 
 .. _backends documentation: #other-authentication-sources
 
-Manually checking a user's password
+Manually managing a user's password
 -----------------------------------
 
-.. currentmodule:: django.contrib.auth.models
+.. currentmodule:: django.contrib.auth.utils
 
+.. versionadded:: 1.4
+
+    The :mod:`django.contrib.auth.utils` module provides a set of functions
+    to create and validate hashed password. You can use them independently
+    from the ``User`` model.
+
 .. function:: check_password()
 
     If you'd like to manually authenticate a user by comparing a plain-text
     password to the hashed password in the database, use the convenience
-    function :func:`django.contrib.auth.models.check_password`. It takes two
-    arguments: the plain-text password to check, and the full value of a user's
-    ``password`` field in the database to check against, and returns ``True``
-    if they match, ``False`` otherwise.
+    function :func:`django.contrib.auth.utils.check_password`. It takes two
+    arguments: the plain-text password to check, and the full value of a
+    user's ``password`` field in the database to check against, and returns
+    ``True`` if they match, ``False`` otherwise.
 
+.. function:: make_password()
+
+    .. versionadded:: 1.4
+
+    Creates a hashed password in the format used by this application. It takes
+    two arguments: hashing algorithm to use and the password in plain-text.
+    Currently supported algorithms are: ``'sha1'``, ``'md5'`` and ``'crypt'``
+    if you have the ``crypt`` library installed. If the second argument is
+    ``None``, an unusable password is returned (a one that will be never
+    accepted by :func:`django.contrib.auth.utils.check_password`).
+
+.. function:: is_password_usable()
+
+    .. versionadded:: 1.4
+
+   Checks if the given string is a hashed password that has a chance
+   of being verified against :func:`django.contrib.auth.utils.check_password`.
+
+
 How to log a user out
 ---------------------
 

-- 
You received this message because you are subscribed to the Google Groups 
"Django updates" group.
To post to this group, send email to [email protected].
To unsubscribe from this group, send email to 
[email protected].
For more options, visit this group at 
http://groups.google.com/group/django-updates?hl=en.

Reply via email to