[IMP] sale,purchase,invoice: make `Send by mail` action more robust to template/view...
[odoo/odoo.git] / addons / base_crypt / crypt.py
1 # Notice:
2 # ------
3 #
4 # Implements encrypting functions.
5 #
6 # Copyright (c) 2008, F S 3 Consulting Inc.
7 #
8 # Maintainer:
9 # Alec Joseph Rivera (agi<at>fs3.ph)
10 #
11 #
12 # Warning:
13 # -------
14 #
15 # This program as  such is intended to be used by  professional programmers
16 # who take the whole responsibility of assessing all potential consequences
17 # resulting  from its eventual  inadequacies and  bugs.  End users  who are
18 # looking  for a  ready-to-use  solution  with  commercial  guarantees  and
19 # support are strongly adviced to contract a Free Software Service Company.
20 #
21 # This program  is Free Software; you can  redistribute it and/or modify it
22 # under  the terms of the  GNU General  Public License  as published by the
23 # Free Software  Foundation;  either version 2 of the  License, or (at your
24 # option) any later version.
25 #
26 # This  program is  distributed in  the hope that  it will  be useful,  but
27 # WITHOUT   ANY   WARRANTY;   without   even   the   implied   warranty  of
28 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General
29 # Public License for more details.
30 #
31 # You should  have received a copy of the GNU General  Public License along
32 # with this program; if not, write to the:
33 #
34 # Free Software Foundation, Inc.
35 # 59 Temple Place - Suite 330
36 # Boston, MA  02111-1307
37 # USA.
38
39 from random import seed, sample
40 from string import ascii_letters, digits
41 from osv import fields,osv
42 import pooler
43 from tools.translate import _
44 from service import security
45 import logging
46
47 magic_md5 = '$1$'
48 _logger = logging.getLogger(__name__)
49
50 def gen_salt( length=8, symbols=None):
51     if symbols is None:
52         symbols = ascii_letters + digits
53     seed()
54     return ''.join( sample( symbols, length ) )
55
56 # The encrypt_md5 is based on Mark Johnson's md5crypt.py, which in turn is
57 # based on  FreeBSD src/lib/libcrypt/crypt.c (1.2)  by  Poul-Henning Kamp.
58 # Mark's port can be found in  ActiveState ASPN Python Cookbook.  Kudos to
59 # Poul and Mark. -agi
60 #
61 # Original license:
62 #
63 # * "THE BEER-WARE LICENSE" (Revision 42):
64 # *
65 # * <phk@login.dknet.dk>  wrote  this file.  As  long as  you retain  this
66 # * notice  you can do  whatever you want with this stuff. If we meet some
67 # * day,  and you think this stuff is worth it,  you can buy me  a beer in
68 # * return.
69 # *
70 # * Poul-Henning Kamp
71
72
73 #TODO: py>=2.6: from hashlib import md5
74 import hashlib
75
76 def encrypt_md5( raw_pw, salt, magic=magic_md5 ):
77     raw_pw = raw_pw.encode('utf-8')
78     salt = salt.encode('utf-8')
79     hash = hashlib.md5()
80     hash.update( raw_pw + magic + salt )
81     st = hashlib.md5()
82     st.update( raw_pw + salt + raw_pw)
83     stretch = st.digest()
84
85     for i in range( 0, len( raw_pw ) ):
86         hash.update( stretch[i % 16] )
87
88     i = len( raw_pw )
89
90     while i:
91         if i & 1:
92             hash.update('\x00')
93         else:
94             hash.update( raw_pw[0] )
95         i >>= 1
96
97     saltedmd5 = hash.digest()
98
99     for i in range( 1000 ):
100         hash = hashlib.md5()
101
102         if i & 1:
103             hash.update( raw_pw )
104         else:
105             hash.update( saltedmd5 )
106
107         if i % 3:
108             hash.update( salt )
109         if i % 7:
110             hash.update( raw_pw )
111         if i & 1:
112             hash.update( saltedmd5 )
113         else:
114             hash.update( raw_pw )
115
116         saltedmd5 = hash.digest()
117
118     itoa64 = './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'
119
120     rearranged = ''
121     for a, b, c in ((0, 6, 12), (1, 7, 13), (2, 8, 14), (3, 9, 15), (4, 10, 5)):
122         v = ord( saltedmd5[a] ) << 16 | ord( saltedmd5[b] ) << 8 | ord( saltedmd5[c] )
123
124         for i in range(4):
125             rearranged += itoa64[v & 0x3f]
126             v >>= 6
127
128     v = ord( saltedmd5[11] )
129
130     for i in range( 2 ):
131         rearranged += itoa64[v & 0x3f]
132         v >>= 6
133
134     return magic + salt + '$' + rearranged
135
136 class users(osv.osv):
137     _name="res.users"
138     _inherit="res.users"
139     # agi - 022108
140     # Add handlers for 'input_pw' field.
141
142     def set_pw(self, cr, uid, id, name, value, args, context):
143         if not value:
144             raise osv.except_osv(_('Error!'), _("You have to specify a password."))
145
146         obj = pooler.get_pool(cr.dbname).get('res.users')
147         if not hasattr(obj, "_salt_cache"):
148             obj._salt_cache = {}
149
150         salt = obj._salt_cache[id] = gen_salt()
151         encrypted = encrypt_md5(value, salt)
152         cr.execute('update res_users set password=%s where id=%s',
153             (encrypted.encode('utf-8'), int(id)))
154         cr.commit()
155         del value
156
157     def get_pw( self, cr, uid, ids, name, args, context ):
158         cr.execute('select id, password from res_users where id in %s', (tuple(map(int, ids)),))
159         stored_pws = cr.fetchall()
160         res = {}
161
162         for id, stored_pw in stored_pws:
163             res[id] = stored_pw
164
165         return res
166
167     _columns = {
168         # The column size could be smaller as it is meant to store a hash, but
169         # an existing column cannot be downsized; thus we use the original
170         # column size.
171         'password': fields.function(get_pw, fnct_inv=set_pw, type='char',
172             size=64, string='Password', invisible=True,
173             store=True),
174     }
175
176     def login(self, db, login, password):
177         if not password:
178             return False
179         if db is False:
180             raise RuntimeError("Cannot authenticate to False db!")
181         cr = None
182         try:
183             cr = pooler.get_db(db).cursor()
184             return self._login(cr, db, login, password)
185         except Exception:
186             _logger.exception('Cannot authenticate.')
187             return Exception('Access denied.')
188         finally:
189             if cr is not None:
190                 cr.close()
191
192     def _login(self, cr, db, login, password):
193         cr.execute( 'SELECT password, id FROM res_users WHERE login=%s AND active',
194             (login.encode('utf-8'),))
195
196         if cr.rowcount:
197             stored_pw, id = cr.fetchone()
198         else:
199             # Return early if no one has a login name like that.
200             return False
201
202         stored_pw = self.maybe_encrypt(cr, stored_pw, id)
203
204         if not stored_pw:
205             # means couldn't encrypt or user is not active!
206             return False
207
208         # Calculate an encrypted password from the user-provided
209         # password.
210         obj = pooler.get_pool(db).get('res.users')
211         if not hasattr(obj, "_salt_cache"):
212             obj._salt_cache = {}
213         salt = obj._salt_cache[id] = stored_pw[len(magic_md5):11]
214         encrypted_pw = encrypt_md5(password, salt)
215
216         # Check if the encrypted password matches against the one in the db.
217         cr.execute("""UPDATE res_users
218                         SET login_date=now() AT TIME ZONE 'UTC'
219                         WHERE id=%s AND password=%s AND active
220                         RETURNING id""",
221                    (int(id), encrypted_pw.encode('utf-8')))
222         res = cr.fetchone()
223         cr.commit()
224
225         if res:
226             return res[0]
227         else:
228             return False
229
230     def check(self, db, uid, passwd):
231         if not passwd:
232             # empty passwords disallowed for obvious security reasons
233             raise security.ExceptionNoTb('AccessDenied')
234
235         # Get a chance to hash all passwords in db before using the uid_cache.
236         obj = pooler.get_pool(db).get('res.users')
237         if not hasattr(obj, "_salt_cache"):
238             obj._salt_cache = {}
239             self._uid_cache.get(db, {}).clear()
240
241         cached_pass = self._uid_cache.get(db, {}).get(uid)
242         if (cached_pass is not None) and cached_pass == passwd:
243             return True
244
245         cr = pooler.get_db(db).cursor()
246         try:
247             if uid not in self._salt_cache.get(db, {}):
248                 # If we don't have cache, we have to repeat the procedure
249                 # through the login function.
250                 cr.execute( 'SELECT login FROM res_users WHERE id=%s', (uid,) )
251                 stored_login = cr.fetchone()
252                 if stored_login:
253                     stored_login = stored_login[0]
254
255                 res = self._login(cr, db, stored_login, passwd)
256                 if not res:
257                     raise security.ExceptionNoTb('AccessDenied')
258             else:
259                 salt = self._salt_cache[db][uid]
260                 cr.execute('SELECT COUNT(*) FROM res_users WHERE id=%s AND password=%s AND active',
261                     (int(uid), encrypt_md5(passwd, salt)))
262                 res = cr.fetchone()[0]
263         finally:
264             cr.close()
265
266         if not bool(res):
267             raise security.ExceptionNoTb('AccessDenied')
268
269         if res:
270             if self._uid_cache.has_key(db):
271                 ulist = self._uid_cache[db]
272                 ulist[uid] = passwd
273             else:
274                 self._uid_cache[db] = {uid: passwd}
275         return bool(res)
276
277     def maybe_encrypt(self, cr, pw, id):
278         """ Return the password 'pw', making sure it is encrypted.
279
280         If the password 'pw' is not encrypted, then encrypt all active passwords
281         in the db. Returns the (possibly newly) encrypted password for 'id'.
282         """
283
284         if not pw.startswith(magic_md5):
285             cr.execute("SELECT id, password FROM res_users " \
286                 "WHERE active=true AND password NOT LIKE '$%'")
287             # Note that we skip all passwords like $.., in anticipation for
288             # more than md5 magic prefixes.
289             res = cr.fetchall()
290             for i, p in res:
291                 encrypted = encrypt_md5(p, gen_salt())
292                 cr.execute('UPDATE res_users SET password=%s where id=%s',
293                         (encrypted, i))
294                 if i == id:
295                     encrypted_res = encrypted
296             cr.commit()
297             return encrypted_res
298         return pw
299
300 users()
301
302 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: