[MERGE] merge with latest stable
[odoo/odoo.git] / addons / base_crypt / crypt.py
index 7d2ab0e..a616b7e 100644 (file)
 # USA.
 
 from random import seed, sample
-from string import letters, digits
+from string import ascii_letters, digits
 from osv import fields,osv
 import pooler
-import tools
+from tools.translate import _
 from service import security
 
 magic_md5 = '$1$'
 
-def gen_salt( length=8, symbols=letters + digits ):
+def gen_salt( length=8, symbols=ascii_letters + digits ):
     seed()
     return ''.join( sample( symbols, length ) )
 
@@ -65,11 +65,16 @@ def gen_salt( length=8, symbols=letters + digits ):
 # *
 # * Poul-Henning Kamp
 
-import md5
+
+#TODO: py>=2.6: from hashlib import md5
+import hashlib
 
 def encrypt_md5( raw_pw, salt, magic=magic_md5 ):
-    hash = md5.new( raw_pw + magic + salt )
-    stretch = md5.new( raw_pw + salt + raw_pw).digest()
+    hash = hashlib.md5()
+    hash.update( raw_pw + magic + salt )
+    st = hashlib.md5()
+    st.update( raw_pw + salt + raw_pw)
+    stretch = st.digest()
 
     for i in range( 0, len( raw_pw ) ):
         hash.update( stretch[i % 16] )
@@ -86,7 +91,7 @@ def encrypt_md5( raw_pw, salt, magic=magic_md5 ):
     saltedmd5 = hash.digest()
 
     for i in range( 1000 ):
-        hash = md5.new()
+        hash = hashlib.md5()
 
         if i & 1:
             hash.update( raw_pw )
@@ -122,106 +127,168 @@ def encrypt_md5( raw_pw, salt, magic=magic_md5 ):
 
     return magic + salt + '$' + rearranged
 
-_salt_cache = {}
-
-def login(db, login, password):
-    cr = pooler.get_db(db).cursor()
-    cr.execute( 'select password from res_users where login=%s', (login.encode( 'utf-8' ),) )
-    stored_pw = cr.fetchone()
-
-    if stored_pw:
-        stored_pw = stored_pw[0]
-    else:
-        # Return early if no one has a login name like that.
-        return False
-
-    # Calculate a new password ('updated_pw') from 'stored_pw' if the
-    # latter isn't encrypted yet. Use that to update the database entry.
-    # Also update the 'stored_pw' to reflect the change.
-
-    if stored_pw[0:3] != magic_md5:
-        updated_pw = encrypt_md5( stored_pw, gen_salt() )
-        cr.execute( 'update res_users set password=%s where login=%s', (updated_pw.encode( 'utf-8' ), login.encode( 'utf-8' ),) )
-        cr.commit()
-
-        cr.execute( 'select password from res_users where login=%s', (login.encode( 'utf-8' ),) )
-        stored_pw = cr.fetchone()[0]
-
-    # Calculate an encrypted password from the user-provided
-    # password ('encrypted_pw').
-
-    salt = _salt_cache[password] = stored_pw[3:11]
-    encrypted_pw = encrypt_md5( password, salt )
-
-    # Retrieve a user id from the database, factoring in an encrypted
-    # password.
-
-    cr.execute('select id from res_users where login=%s and password=%s and active', (login.encode('utf-8'), encrypted_pw.encode('utf-8')))
-    res = cr.fetchone()
-    cr.close()
-
-    if res:
-        return res[0]
-    else:
-        return False
-
-#def check_super(passwd):
-#    salt = _salt_cache[passwd]
-#    if encrypt_md5( passwd, salt ) == tools.config['admin_passwd']:
-#         return True
-#    else:
-#         raise Exception('AccessDenied')
-
-def check(db, uid, passwd):
-    if security._uid_cache.has_key( uid ) and (security._uid_cache[uid]==passwd):
-        return True
-    cr = pooler.get_db(db).cursor()
-    salt = _salt_cache[passwd]
-    cr.execute(' select count(*) from res_users where id=%d and password=%s', (int(uid), encrypt_md5( passwd, salt )) )
-    res = cr.fetchone()[0]
-    cr.close()
-    if not bool(res):
-        raise Exception('AccessDenied')
-    if res:
-        security._uid_cache[uid] = passwd
-    return bool(res)
-
-
-def access(db, uid, passwd, sec_level, ids):
-    cr = pooler.get_db(db).cursor()
-    salt = _salt_cache[passwd]
-    cr.execute('select id from res_users where id=%s and password=%s', (uid, encrypt_md5( passwd, salt )) )
-    res = cr.fetchone()
-    cr.close()
-    if not res:
-        raise Exception('Bad username or password')
-    return res[0]
-
-# check if module is installed or not
-security.login=login
-#security.check_super=check_super
-security.access=access
-security.check=check
-
 class users(osv.osv):
     _name="res.users"
     _inherit="res.users"
     # agi - 022108
     # Add handlers for 'input_pw' field.
 
-    def set_pw( self, cr, uid, id, name, value, args, context ):
-        self.write( cr, uid, id, { 'password' : encrypt_md5( value, gen_salt() ) } )
+    def set_pw(self, cr, uid, id, name, value, args, context):
+        if not value:
+            raise osv.except_osv(_('Error'), _("Please specify the password !"))
+
+        obj = pooler.get_pool(cr.dbname).get('res.users')
+        if not hasattr(obj, "_salt_cache"):
+            obj._salt_cache = {}
+
+        salt = obj._salt_cache[id] = gen_salt()
+        encrypted = encrypt_md5(value, salt)
+        cr.execute('update res_users set password=%s where id=%s',
+            (encrypted.encode('utf-8'), int(id)))
+        cr.commit()
         del value
 
     def get_pw( self, cr, uid, ids, name, args, context ):
+        cr.execute('select id, password from res_users where id in %s', (tuple(map(int, ids)),))
+        stored_pws = cr.fetchall()
         res = {}
-        for id in ids:
-            res[id] = ''
-        return res
 
-    # Continuing to original code.
+        for id, stored_pw in stored_pws:
+            res[id] = stored_pw
+
+        return res
 
     _columns = {
-        'input_pw': fields.function( get_pw, fnct_inv=set_pw, type='char', method=True, size=20, string='Password', invisible=True),
-            }
-users()
\ No newline at end of file
+        # The column size could be smaller as it is meant to store a hash, but
+        # an existing column cannot be downsized; thus we use the original
+        # column size.
+        'password': fields.function(get_pw, fnct_inv=set_pw, type='char',
+            method=True, size=64, string='Password', invisible=True,
+            store=True),
+    }
+
+    def login(self, db, login, password):
+        if not password:
+            return False
+        if db is False:
+            raise RuntimeError("Cannot authenticate to False db!")
+        cr = None
+        try:
+            cr = pooler.get_db(db).cursor()
+            return self._login(cr, db, login, password)
+        except Exception:
+            import logging
+            logging.getLogger('netsvc').exception('Could not authenticate')
+            return Exception('Access Denied')
+        finally:
+            if cr is not None:
+                cr.close()
+
+    def _login(self, cr, db, login, password):
+        cr.execute( 'SELECT password, id FROM res_users WHERE login=%s AND active',
+            (login.encode('utf-8'),))
+
+        if cr.rowcount:
+            stored_pw, id = cr.fetchone()
+        else:
+            # Return early if no one has a login name like that.
+            return False
+    
+        stored_pw = self.maybe_encrypt(cr, stored_pw, id)
+        
+        if not stored_pw:
+            # means couldn't encrypt or user is not active!
+            return False
+
+        # Calculate an encrypted password from the user-provided
+        # password.
+        obj = pooler.get_pool(db).get('res.users')
+        if not hasattr(obj, "_salt_cache"):
+            obj._salt_cache = {}
+        salt = obj._salt_cache[id] = stored_pw[len(magic_md5):11]
+        encrypted_pw = encrypt_md5(password, salt)
+    
+        # Check if the encrypted password matches against the one in the db.
+        cr.execute('UPDATE res_users SET date=now() ' \
+                'WHERE id=%s AND password=%s AND active RETURNING id', 
+            (int(id), encrypted_pw.encode('utf-8')))
+        res = cr.fetchone()
+        cr.commit()
+    
+        if res:
+            return res[0]
+        else:
+            return False
+
+    def check(self, db, uid, passwd):
+        if not passwd:
+            # empty passwords disallowed for obvious security reasons
+            raise security.ExceptionNoTb('AccessDenied')
+
+        # Get a chance to hash all passwords in db before using the uid_cache.
+        obj = pooler.get_pool(db).get('res.users')
+        if not hasattr(obj, "_salt_cache"):
+            obj._salt_cache = {}
+            self._uid_cache.get(db, {}).clear()
+
+        cached_pass = self._uid_cache.get(db, {}).get(uid)
+        if (cached_pass is not None) and cached_pass == passwd:
+            return True
+
+        cr = pooler.get_db(db).cursor()
+        try:
+            if uid not in self._salt_cache.get(db, {}):
+                # If we don't have cache, we have to repeat the procedure
+                # through the login function.
+                cr.execute( 'SELECT login FROM res_users WHERE id=%s', (uid,) )
+                stored_login = cr.fetchone()
+                if stored_login:
+                    stored_login = stored_login[0]
+        
+                res = self._login(cr, db, stored_login, passwd)
+                if not res:
+                    raise security.ExceptionNoTb('AccessDenied')
+            else:
+                salt = self._salt_cache[db][uid]
+                cr.execute('SELECT COUNT(*) FROM res_users WHERE id=%s AND password=%s AND active', 
+                    (int(uid), encrypt_md5(passwd, salt)))
+                res = cr.fetchone()[0]
+        finally:
+            cr.close()
+
+        if not bool(res):
+            raise security.ExceptionNoTb('AccessDenied')
+
+        if res:
+            if self._uid_cache.has_key(db):
+                ulist = self._uid_cache[db]
+                ulist[uid] = passwd
+            else:
+                self._uid_cache[db] = {uid: passwd}
+        return bool(res)
+    
+    def maybe_encrypt(self, cr, pw, id):
+        """ Return the password 'pw', making sure it is encrypted.
+        
+        If the password 'pw' is not encrypted, then encrypt all active passwords
+        in the db. Returns the (possibly newly) encrypted password for 'id'.
+        """
+
+        if not pw.startswith(magic_md5):
+            cr.execute("SELECT id, password FROM res_users " \
+                "WHERE active=true AND password NOT LIKE '$%'")
+            # Note that we skip all passwords like $.., in anticipation for
+            # more than md5 magic prefixes.
+            res = cr.fetchall()
+            for i, p in res:
+                encrypted = encrypt_md5(p, gen_salt())
+                cr.execute('UPDATE res_users SET password=%s where id=%s',
+                        (encrypted, i))
+                if i == id:
+                    encrypted_res = encrypted
+            cr.commit()
+            return encrypted_res
+        return pw
+
+users()
+# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: