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