Files
odoo_source/addons/base_crypt/crypt.py
T
Xavier Morel 835cb5bcfa [FIX] concurrency issue when installing base_crypt from web client
Basically (and apparently), as base_crypt performs its conversion of
passwords to encrypted passwords a new request arrives and tries to
check its login, blowing up during the update.

* For the conversion, lock the whole table before updating the
  passwords, as we need to touch every record anyway we can just put a
  big exclusive lock on res_users

* Ideally we'd be able to just lock the relevant row of res_users in
  check/_login, but for some reason I don't quite get this does not
  work and still yields

    > TransactionRollbackError: could not serialize access due to concurrent update

  except on the SELECT FOR UPDATE query instead of the UPDATE one. The
  docs don't seem to help, and replacing the FOR UPDATE row-level lock
  by a table lock works even though they appear pretty much at the
  same point. Me no get.

bzr revid: xmo@openerp.com-20120716064903-403pth90kupgfdv3
2012-07-16 08:49:03 +02:00

285 lines
9.0 KiB
Python

# Notice:
# ------
#
# Implements encrypting functions.
#
# Copyright (c) 2008, F S 3 Consulting Inc.
#
# Maintainer:
# Alec Joseph Rivera (agi<at>fs3.ph)
#
#
# Warning:
# -------
#
# This program as such is intended to be used by professional programmers
# who take the whole responsibility of assessing all potential consequences
# resulting from its eventual inadequacies and bugs. End users who are
# looking for a ready-to-use solution with commercial guarantees and
# support are strongly adviced to contract a Free Software Service Company.
#
# This program is Free Software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by the
# Free Software Foundation; either version 2 of the License, or (at your
# option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
# Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the:
#
# Free Software Foundation, Inc.
# 59 Temple Place - Suite 330
# Boston, MA 02111-1307
# USA.
from __future__ import with_statement
from contextlib import closing
from random import seed, sample
from string import ascii_letters, digits
from osv import fields,osv
import pooler
from tools.translate import _
from service import security
magic_md5 = '$1$'
def gen_salt( length=8, symbols=ascii_letters + digits ):
seed()
return ''.join( sample( symbols, length ) )
# The encrypt_md5 is based on Mark Johnson's md5crypt.py, which in turn is
# based on FreeBSD src/lib/libcrypt/crypt.c (1.2) by Poul-Henning Kamp.
# Mark's port can be found in ActiveState ASPN Python Cookbook. Kudos to
# Poul and Mark. -agi
#
# Original license:
#
# * "THE BEER-WARE LICENSE" (Revision 42):
# *
# * <phk@login.dknet.dk> wrote this file. As long as you retain this
# * notice you can do whatever you want with this stuff. If we meet some
# * day, and you think this stuff is worth it, you can buy me a beer in
# * return.
# *
# * Poul-Henning Kamp
#TODO: py>=2.6: from hashlib import md5
import hashlib
def encrypt_md5( raw_pw, salt, magic=magic_md5 ):
raw_pw = raw_pw.encode('utf-8')
salt = salt.encode('utf-8')
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] )
i = len( raw_pw )
while i:
if i & 1:
hash.update('\x00')
else:
hash.update( raw_pw[0] )
i >>= 1
saltedmd5 = hash.digest()
for i in range( 1000 ):
hash = hashlib.md5()
if i & 1:
hash.update( raw_pw )
else:
hash.update( saltedmd5 )
if i % 3:
hash.update( salt )
if i % 7:
hash.update( raw_pw )
if i & 1:
hash.update( saltedmd5 )
else:
hash.update( raw_pw )
saltedmd5 = hash.digest()
itoa64 = './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'
rearranged = ''
for a, b, c in ((0, 6, 12), (1, 7, 13), (2, 8, 14), (3, 9, 15), (4, 10, 5)):
v = ord( saltedmd5[a] ) << 16 | ord( saltedmd5[b] ) << 8 | ord( saltedmd5[c] )
for i in range(4):
rearranged += itoa64[v & 0x3f]
v >>= 6
v = ord( saltedmd5[11] )
for i in range( 2 ):
rearranged += itoa64[v & 0x3f]
v >>= 6
return magic + salt + '$' + rearranged
class users(osv.osv):
_name="res.users"
_inherit="res.users"
# agi - 022108
# Add handlers for 'input_pw' field.
def init(self, cr):
with closing(pooler.get_db(cr.dbname).cursor()) as cr:
cr.execute('LOCK res_users')
cr.execute("SELECT id, password FROM res_users "
"WHERE active=true AND password NOT LIKE '$%' ")
cr.executemany("UPDATE res_users SET password=%s WHERE id=%s",
((encrypt_md5(password, gen_salt()), id)
for id, password in cr.fetchall()))
cr.commit()
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, stored_pw in stored_pws:
res[id] = stored_pw
return res
_columns = {
# 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',
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
# 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() AT TIME ZONE 'UTC'
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:
cr.execute('LOCK res_users')
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)
users()
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: