[IMP]MRP :BOM view are improved
[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=ascii_letters + digits ):
51     seed()
52     return ''.join( sample( symbols, length ) )
53
54 # The encrypt_md5 is based on Mark Johnson's md5crypt.py, which in turn is
55 # based on  FreeBSD src/lib/libcrypt/crypt.c (1.2)  by  Poul-Henning Kamp.
56 # Mark's port can be found in  ActiveState ASPN Python Cookbook.  Kudos to
57 # Poul and Mark. -agi
58 #
59 # Original license:
60 #
61 # * "THE BEER-WARE LICENSE" (Revision 42):
62 # *
63 # * <phk@login.dknet.dk>  wrote  this file.  As  long as  you retain  this
64 # * notice  you can do  whatever you want with this stuff. If we meet some
65 # * day,  and you think this stuff is worth it,  you can buy me  a beer in
66 # * return.
67 # *
68 # * Poul-Henning Kamp
69
70
71 #TODO: py>=2.6: from hashlib import md5
72 import hashlib
73
74 def encrypt_md5( raw_pw, salt, magic=magic_md5 ):
75     raw_pw = raw_pw.encode('utf-8')
76     salt = salt.encode('utf-8')
77     hash = hashlib.md5()
78     hash.update( raw_pw + magic + salt )
79     st = hashlib.md5()
80     st.update( raw_pw + salt + raw_pw)
81     stretch = st.digest()
82
83     for i in range( 0, len( raw_pw ) ):
84         hash.update( stretch[i % 16] )
85
86     i = len( raw_pw )
87
88     while i:
89         if i & 1:
90             hash.update('\x00')
91         else:
92             hash.update( raw_pw[0] )
93         i >>= 1
94
95     saltedmd5 = hash.digest()
96
97     for i in range( 1000 ):
98         hash = hashlib.md5()
99
100         if i & 1:
101             hash.update( raw_pw )
102         else:
103             hash.update( saltedmd5 )
104
105         if i % 3:
106             hash.update( salt )
107         if i % 7:
108             hash.update( raw_pw )
109         if i & 1:
110             hash.update( saltedmd5 )
111         else:
112             hash.update( raw_pw )
113
114         saltedmd5 = hash.digest()
115
116     itoa64 = './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'
117
118     rearranged = ''
119     for a, b, c in ((0, 6, 12), (1, 7, 13), (2, 8, 14), (3, 9, 15), (4, 10, 5)):
120         v = ord( saltedmd5[a] ) << 16 | ord( saltedmd5[b] ) << 8 | ord( saltedmd5[c] )
121
122         for i in range(4):
123             rearranged += itoa64[v & 0x3f]
124             v >>= 6
125
126     v = ord( saltedmd5[11] )
127
128     for i in range( 2 ):
129         rearranged += itoa64[v & 0x3f]
130         v >>= 6
131
132     return magic + salt + '$' + rearranged
133
134 class users(osv.osv):
135     _name="res.users"
136     _inherit="res.users"
137     # agi - 022108
138     # Add handlers for 'input_pw' field.
139
140     def set_pw(self, cr, uid, id, name, value, args, context):
141         if not value:
142             raise osv.except_osv(_('Error'), _("Please specify the password !"))
143
144         obj = pooler.get_pool(cr.dbname).get('res.users')
145         if not hasattr(obj, "_salt_cache"):
146             obj._salt_cache = {}
147
148         salt = obj._salt_cache[id] = gen_salt()
149         encrypted = encrypt_md5(value, salt)
150         cr.execute('update res_users set password=%s where id=%s',
151             (encrypted.encode('utf-8'), int(id)))
152         cr.commit()
153         del value
154
155     def get_pw( self, cr, uid, ids, name, args, context ):
156         cr.execute('select id, password from res_users where id in %s', (tuple(map(int, ids)),))
157         stored_pws = cr.fetchall()
158         res = {}
159
160         for id, stored_pw in stored_pws:
161             res[id] = stored_pw
162
163         return res
164
165     _columns = {
166         # The column size could be smaller as it is meant to store a hash, but
167         # an existing column cannot be downsized; thus we use the original
168         # column size.
169         'password': fields.function(get_pw, fnct_inv=set_pw, type='char',
170             size=64, string='Password', invisible=True,
171             store=True),
172     }
173
174     def login(self, db, login, password):
175         if not password:
176             return False
177         if db is False:
178             raise RuntimeError("Cannot authenticate to False db!")
179         cr = None
180         try:
181             cr = pooler.get_db(db).cursor()
182             return self._login(cr, db, login, password)
183         except Exception:
184             _logger.exception('Could not authenticate')
185             return Exception('Access Denied')
186         finally:
187             if cr is not None:
188                 cr.close()
189
190     def _login(self, cr, db, login, password):
191         cr.execute( 'SELECT password, id FROM res_users WHERE login=%s AND active',
192             (login.encode('utf-8'),))
193
194         if cr.rowcount:
195             stored_pw, id = cr.fetchone()
196         else:
197             # Return early if no one has a login name like that.
198             return False
199     
200         stored_pw = self.maybe_encrypt(cr, stored_pw, id)
201         
202         if not stored_pw:
203             # means couldn't encrypt or user is not active!
204             return False
205
206         # Calculate an encrypted password from the user-provided
207         # password.
208         obj = pooler.get_pool(db).get('res.users')
209         if not hasattr(obj, "_salt_cache"):
210             obj._salt_cache = {}
211         salt = obj._salt_cache[id] = stored_pw[len(magic_md5):11]
212         encrypted_pw = encrypt_md5(password, salt)
213     
214         # Check if the encrypted password matches against the one in the db.
215         cr.execute("""UPDATE res_users
216                         SET date=now() AT TIME ZONE 'UTC'
217                         WHERE id=%s AND password=%s AND active
218                         RETURNING id""", 
219                    (int(id), encrypted_pw.encode('utf-8')))
220         res = cr.fetchone()
221         cr.commit()
222     
223         if res:
224             return res[0]
225         else:
226             return False
227
228     def check(self, db, uid, passwd):
229         if not passwd:
230             # empty passwords disallowed for obvious security reasons
231             raise security.ExceptionNoTb('AccessDenied')
232
233         # Get a chance to hash all passwords in db before using the uid_cache.
234         obj = pooler.get_pool(db).get('res.users')
235         if not hasattr(obj, "_salt_cache"):
236             obj._salt_cache = {}
237             self._uid_cache.get(db, {}).clear()
238
239         cached_pass = self._uid_cache.get(db, {}).get(uid)
240         if (cached_pass is not None) and cached_pass == passwd:
241             return True
242
243         cr = pooler.get_db(db).cursor()
244         try:
245             if uid not in self._salt_cache.get(db, {}):
246                 # If we don't have cache, we have to repeat the procedure
247                 # through the login function.
248                 cr.execute( 'SELECT login FROM res_users WHERE id=%s', (uid,) )
249                 stored_login = cr.fetchone()
250                 if stored_login:
251                     stored_login = stored_login[0]
252         
253                 res = self._login(cr, db, stored_login, passwd)
254                 if not res:
255                     raise security.ExceptionNoTb('AccessDenied')
256             else:
257                 salt = self._salt_cache[db][uid]
258                 cr.execute('SELECT COUNT(*) FROM res_users WHERE id=%s AND password=%s AND active', 
259                     (int(uid), encrypt_md5(passwd, salt)))
260                 res = cr.fetchone()[0]
261         finally:
262             cr.close()
263
264         if not bool(res):
265             raise security.ExceptionNoTb('AccessDenied')
266
267         if res:
268             if self._uid_cache.has_key(db):
269                 ulist = self._uid_cache[db]
270                 ulist[uid] = passwd
271             else:
272                 self._uid_cache[db] = {uid: passwd}
273         return bool(res)
274     
275     def maybe_encrypt(self, cr, pw, id):
276         """ Return the password 'pw', making sure it is encrypted.
277         
278         If the password 'pw' is not encrypted, then encrypt all active passwords
279         in the db. Returns the (possibly newly) encrypted password for 'id'.
280         """
281
282         if not pw.startswith(magic_md5):
283             cr.execute("SELECT id, password FROM res_users " \
284                 "WHERE active=true AND password NOT LIKE '$%'")
285             # Note that we skip all passwords like $.., in anticipation for
286             # more than md5 magic prefixes.
287             res = cr.fetchall()
288             for i, p in res:
289                 encrypted = encrypt_md5(p, gen_salt())
290                 cr.execute('UPDATE res_users SET password=%s where id=%s',
291                         (encrypted, i))
292                 if i == id:
293                     encrypted_res = encrypted
294             cr.commit()
295             return encrypted_res
296         return pw
297
298 users()
299 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: