[sources/native] automatically update passwords using deprecated hashes on login

Closes #2465904

authorJulien Cristau <julien.cristau@logilab.fr>
changeset3d2038d6f20d
branchstable
phasepublic
hiddenno
parent revision#eb7a171cec72 [repo pyro] fix previous commit: should not import Pyro in remoterql module/base class, it may be missing
child revision#f23ac525ddd1 [datafeed] properly call hooks for inlined relations on entity creation. Closes #2481156
files modified by this revision
server/sources/native.py
server/test/unittest_security.py
server/utils.py
# HG changeset patch
# User Julien Cristau <julien.cristau@logilab.fr>
# Date 1347283030 -7200
# Mon Sep 10 15:17:10 2012 +0200
# Branch stable
# Node ID 3d2038d6f20dea3b6f3cfa5211db2ed93fb26104
# Parent eb7a171cec72b56c235edeb2f18227bb9399c114
[sources/native] automatically update passwords using deprecated hashes on login

Closes #2465904

diff --git a/server/sources/native.py b/server/sources/native.py
@@ -59,11 +59,11 @@
1  from cubicweb import transaction as tx, server, neg_role
2  from cubicweb.utils import QueryCache
3  from cubicweb.schema import VIRTUAL_RTYPES
4  from cubicweb.cwconfig import CubicWebNoAppConfiguration
5  from cubicweb.server import hook
6 -from cubicweb.server.utils import crypt_password, eschema_eid
7 +from cubicweb.server.utils import crypt_password, eschema_eid, verify_and_update
8  from cubicweb.server.sqlutils import SQL_PREFIX, SQLAdapterMixIn
9  from cubicweb.server.rqlannotation import set_qdata
10  from cubicweb.server.hook import CleanupDeletedEidsCacheOp
11  from cubicweb.server.edition import EditedEntity
12  from cubicweb.server.sources import AbstractSource, dbg_st_search, dbg_results
@@ -1627,11 +1627,26 @@
13              # passwords are stored using the Bytes type, so we get a StringIO
14              args['pwd'] = Binary(crypt_password(password, pwd.getvalue()))
15          # get eid from login and (crypted) password
16          rset = self.source.syntax_tree_search(session, self._auth_rqlst, args)
17          try:
18 -            return rset[0][0]
19 +            user = rset[0][0]
20 +            # If the stored hash uses a deprecated scheme (e.g. DES or MD5 used
21 +            # before 3.14.7), update with a fresh one
22 +            if pwd.getvalue():
23 +                verify, newhash = verify_and_update(password, pwd.getvalue())
24 +                if not verify: # should not happen, but...
25 +                    raise AuthenticationError('bad password')
26 +                if newhash:
27 +                    session.system_sql("UPDATE %s SET %s=%%(newhash)s WHERE %s=%%(login)s" % (
28 +                                        SQL_PREFIX + 'CWUser',
29 +                                        SQL_PREFIX + 'upassword',
30 +                                        SQL_PREFIX + 'login'),
31 +                                       {'newhash': self.source._binary(newhash),
32 +                                        'login': login})
33 +                    session.commit(free_cnxset=False)
34 +            return user
35          except IndexError:
36              raise AuthenticationError('bad password')
37 
38 
39  class EmailPasswordAuthentifier(BaseAuthentifier):
diff --git a/server/test/unittest_security.py b/server/test/unittest_security.py
@@ -23,21 +23,23 @@
40  from logilab.common.testlib import unittest_main, TestCase
41 
42  from rql import RQLException
43 
44  from cubicweb.devtools.testlib import CubicWebTC
45 -from cubicweb import Unauthorized, ValidationError, QueryError
46 +from cubicweb import Unauthorized, ValidationError, QueryError, Binary
47  from cubicweb.schema import ERQLExpression
48  from cubicweb.server.querier import check_read_access
49 +from cubicweb.server.utils import _CRYPTO_CTX
50 
51 
52  class BaseSecurityTC(CubicWebTC):
53 
54      def setup_database(self):
55          super(BaseSecurityTC, self).setup_database()
56          self.create_user(self.request(), 'iaminusersgrouponly')
57 -
58 +        hash = _CRYPTO_CTX.encrypt('oldpassword', scheme='des_crypt')
59 +        self.create_user(self.request(), 'oldpassword', password=Binary(hash))
60 
61  class LowLevelSecurityFunctionTC(BaseSecurityTC):
62 
63      def test_check_read_access(self):
64          rql = u'Personne U where U nom "managers"'
@@ -58,10 +60,22 @@
65          self.rollback()
66          with self.login('iaminusersgrouponly') as cu:
67              self.assertRaises(Unauthorized,
68                                cu.execute, 'Any X,P WHERE X is CWUser, X upassword P')
69 
70 +    def test_update_password(self):
71 +        """Ensure that if a user's password is stored with a deprecated hash, it will be updated on next login"""
72 +        oldhash = str(self.session.system_sql("SELECT cw_upassword FROM cw_CWUser WHERE cw_login = 'oldpassword'").fetchone()[0])
73 +        with self.login('oldpassword') as cu:
74 +            pass
75 +        newhash = str(self.session.system_sql("SELECT cw_upassword FROM cw_CWUser WHERE cw_login = 'oldpassword'").fetchone()[0])
76 +        self.assertNotEqual(oldhash, newhash)
77 +        self.assertTrue(newhash.startswith('$6$'))
78 +        with self.login('oldpassword') as cu:
79 +            pass
80 +        self.assertEqual(newhash, str(self.session.system_sql("SELECT cw_upassword FROM cw_CWUser WHERE cw_login = 'oldpassword'").fetchone()[0]))
81 +
82 
83  class SecurityRewritingTC(BaseSecurityTC):
84      def hijack_source_execute(self):
85          def syntax_tree_search(*args, **kwargs):
86              self.query = (args, kwargs)
diff --git a/server/utils.py b/server/utils.py
@@ -50,11 +50,13 @@
87      # passlib 1.5 wants calc_checksum, 1.6 wants _calc_checksum
88      def calc_checksum(self, secret):
89          return md5crypt(secret, self.salt.encode('ascii')).decode('utf-8')
90      _calc_checksum = calc_checksum
91 
92 -_CRYPTO_CTX = CryptContext(['sha512_crypt', CustomMD5Crypt, 'des_crypt', 'ldap_salted_sha1'])
93 +_CRYPTO_CTX = CryptContext(['sha512_crypt', CustomMD5Crypt, 'des_crypt', 'ldap_salted_sha1'],
94 +                           deprecated=['cubicwebmd5crypt', 'des_crypt'])
95 +verify_and_update = _CRYPTO_CTX.verify_and_update
96 
97  def crypt_password(passwd, salt=None):
98      """return the encrypted password using the given salt or a generated one
99      """
100      if salt is None: