reset_password cleanups
bzr revid: al@openerp.com-20120725225828-ai1uvon05e48p69t
This commit is contained in:
@@ -0,0 +1,115 @@
|
||||
import base64
|
||||
import hashlib
|
||||
import simplejson
|
||||
import time
|
||||
import urlparse
|
||||
|
||||
from openerp.tools import config
|
||||
from openerp.osv import osv, fields
|
||||
|
||||
TWENTY_FOUR_HOURS = 24 * 60 * 60
|
||||
|
||||
def message_sign(data, secret):
|
||||
src = simplejson.dumps([data,secret], indent=None, separators=(',', ':'), sort_keys=True)
|
||||
sign = hashlib.sha1(src).hexdigest()
|
||||
msg = simplejson.dumps([data,sign], indent=None, separators=(',', ':'), sort_keys=True)
|
||||
# pad message to avoid '='
|
||||
pad = (3-len(msg)%3)%3
|
||||
msg = msg + " " * pad
|
||||
msg = base64.urlsafe_b64encode(msg)
|
||||
return msg, sign
|
||||
|
||||
def message_check(msg, secret):
|
||||
msg = base64.urlsafe_b64decode(msg)
|
||||
l = simplejson.loads(msg)
|
||||
msg_data = l[0]
|
||||
msg_sign = l[1]
|
||||
tmp, sign = message_sign(msg_data, secret)
|
||||
if msg_sign == sign:
|
||||
return msg_data
|
||||
|
||||
class res_users(osv.osv):
|
||||
_inherit = 'res.users'
|
||||
|
||||
_sql_constraints = [
|
||||
('email_uniq', 'UNIQUE (user_email)', 'You can not have two users with the same email!')
|
||||
]
|
||||
|
||||
def _auth_reset_password_secret(self, cr, uid, ids, context=None)
|
||||
uuid = self.pool.get('ir.config_parameter').get_param(cr, uid, 'database.uuid')
|
||||
res = {
|
||||
'dbname': cr.dbname,
|
||||
'uuid': uuid,
|
||||
'admin_passwd': config['admin_passwd']
|
||||
}
|
||||
return res
|
||||
|
||||
def _auth_reset_password_host(self, cr, uid, ids, context=None):
|
||||
return self.pool.get('ir.config_parameter').get_param(cr, uid, 'web.base.url', '')
|
||||
|
||||
def _auth_reset_password_link(self, cr, uid, ids, context=None):
|
||||
assert len(ids) == 1
|
||||
user = self.browse(cr, uid, ids[0], context=context)
|
||||
host = self._auth_reset_password_host(cr, uid, ids, context)
|
||||
secret = self._auth_reset_password_secret(cr, uid, ids, context)
|
||||
msg_src = {
|
||||
'time' : time.time(),
|
||||
'uid' : ids[0],
|
||||
}
|
||||
msg, sign = message_sign(msg_src, secret)
|
||||
link = urlparse.urljoin(host, '/web/webclient/home#action_id=reset_password&token=%s' % msg)
|
||||
return link
|
||||
|
||||
def _auth_reset_password_check_token(self, cr, uid, token, context=None):
|
||||
secret = self._auth_reset_password_secret(cr, uid, ids, context)
|
||||
data = message_check(token, secret)
|
||||
if data and (time.time() - data['time'] < TWENTY_FOUR_HOURS):
|
||||
return data
|
||||
|
||||
def send_reset_password_request(self, cr, uid, email, context=None):
|
||||
ids = self.pool.get('res.users').search(cr, 1, [('user_email', '=', email)], context=context)
|
||||
if ids:
|
||||
model, template_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'auth_reset_password', 'reset_password_email')
|
||||
msg_id = self.pool.get('email.template').send_mail(cr, uid, template_id, ids[0], context=ctx)
|
||||
return True
|
||||
|
||||
class auth_reset_password(osv.TransientModel):
|
||||
_name = 'auth.reset_password'
|
||||
_rec_name = 'password'
|
||||
_columns = {
|
||||
'password': fields.char('Password', size=64),
|
||||
'password_confirmation': fields.char('Confirm Password', size=64),
|
||||
'token': fields.char('Token', size=128),
|
||||
'state': fields.selection([(x, x) for x in 'draft done missmatch error'.split()], required=True),
|
||||
}
|
||||
_defaults = {
|
||||
'state': 'draft',
|
||||
}
|
||||
|
||||
|
||||
def create(self, cr, uid, values, context=None):
|
||||
# NOTE here, invalid values raises exceptions to avoid storing
|
||||
# sensitive data into the database (which then are available to anyone)
|
||||
|
||||
if values['password'] != values['password_confirmation']:
|
||||
raise osv.except_osv('Error', 'Passwords missmatch')
|
||||
|
||||
Users = self.pool.get('res.users')
|
||||
data = Users._auth_reset_password_check_token(self, cr, uid, values['token']):
|
||||
if data:
|
||||
Users.write(cr, 1, data['uid'], {'password': pw}, context=context)
|
||||
else:
|
||||
raise osv.except_osv('Error', 'Invalid token')
|
||||
|
||||
# Dont store password
|
||||
values = {'state': 'done'}
|
||||
return super(auth_reset_password, self).create(cr, uid, values, context)
|
||||
|
||||
def change(self, cr, uid, ids, context=None):
|
||||
return True
|
||||
|
||||
def onchange_pw(self, cr, uid, ids, password, password_confirmation, context=None):
|
||||
if password != password_confirmation:
|
||||
return {'value': {'state': 'missmatch'}}
|
||||
return {'value': {'state': 'draft'}}
|
||||
|
||||
+27
-6
@@ -1,25 +1,45 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<openerp>
|
||||
<data>
|
||||
|
||||
<!-- Email template for reset password -->
|
||||
<record id="reset_password_email" model="email.template">
|
||||
<field name="name">Reset Password</field>
|
||||
<field name="model_id" ref="base.model_res_users"/>
|
||||
<field name="email_from"><![CDATA[${object.company_id.name} <${object.company_id.email}>]]></field>
|
||||
<field name="email_to">(set by reset_password module)</field>
|
||||
<field name="subject">Password reset</field>
|
||||
<field name="body_text"><![CDATA[
|
||||
A password reset was requested the OpenERP account linked to this email on ${object._auth_reset_password_host()}
|
||||
|
||||
You may change your password following this link:
|
||||
|
||||
${object._auth_reset_password_link()}
|
||||
|
||||
If you don't have asked for password reset, you can safely ignore this email.
|
||||
|
||||
]]></field>
|
||||
</record>
|
||||
|
||||
<!-- TODO get own css -->
|
||||
<record id="reset_password_wizard_form_view" model="ir.ui.view">
|
||||
<field name="name">reset_password.wizard.form</field>
|
||||
<field name="model">reset_password.wizard</field>
|
||||
<field name="name">auth.reset_password.form</field>
|
||||
<field name="model">auth.reset_password</field>
|
||||
<field name="type">form</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Reset Password" version="7.0">
|
||||
<field name="state" invisible="1"/>
|
||||
<field name="token" on_change="onchange_token(token)" invisible="1"/>
|
||||
<group colspan="4" states="draft,missmatch">
|
||||
<field name="pw" required='1' on_change="onchange_pw(pw,cpw)"/>
|
||||
<field name="cpw" required='1' on_change="onchange_pw(pw,cpw)"/>
|
||||
<group colspan="4" states="draft,missmatch">
|
||||
<field name="password" required='1' on_change="onchange_pw(password,password_confirmation)"/>
|
||||
<field name="password_confirmation" required='1' on_change="onchange_pw(password,password_confirmation)"/>
|
||||
<group colspan="4" states="missmatch">
|
||||
<div>Passwords missmatch</div>
|
||||
</group>
|
||||
<group colspan="2" col="1">
|
||||
<button string="Change Password" name="change" icon="gtk-dialog-authentication" attrs="{'readonly': [('state', '=', 'missmatch')]}"/>
|
||||
</group>
|
||||
</group>
|
||||
</group>
|
||||
<group colspan="4" states="error" col="1">
|
||||
<div>Invalid or expired token</div>
|
||||
<button special="cancel" string="Close"/>
|
||||
@@ -41,5 +61,6 @@
|
||||
<field name="target">new</field>
|
||||
</record>
|
||||
|
||||
|
||||
</data>
|
||||
</openerp>
|
||||
@@ -1,55 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<openerp>
|
||||
<data>
|
||||
|
||||
<record model="email.template" id="email_no_user">
|
||||
<field name="name">Reset Password No User</field>
|
||||
<field name="model_id" ref="base.model_res_company"/>
|
||||
<field name="email_from"><![CDATA[${object.name} <${object.email}>]]></field>
|
||||
<field name="email_to">(set by reset_password module)</field>
|
||||
<field name="subject">Password reset attempt</field>
|
||||
<field name="body_text"><![CDATA[
|
||||
You (or someone else) enter this email address when asking for password reset for an OpenERP account on ${ctx['url']}.
|
||||
However this email is not associated to any account.
|
||||
|
||||
If you have an OpenERP account at this url, please verify your email on your preferences.
|
||||
|
||||
If you don't have an OpenERP account, you can ignore this email.
|
||||
|
||||
For more information about OpenERP, visit http://www.openerp.com
|
||||
|
||||
Kind Regards.
|
||||
]]></field>
|
||||
</record>
|
||||
|
||||
<record model="email.template" id="email_reset_link">
|
||||
<field name="name">Reset Password</field>
|
||||
<field name="model_id" ref="base.model_res_users"/>
|
||||
<field name="email_from"><![CDATA[${object.company_id.name} <${object.company_id.email}>]]></field>
|
||||
<field name="email_to">(set by reset_password module)</field>
|
||||
<field name="subject">Password reset</field>
|
||||
<field name="body_text"><![CDATA[
|
||||
You (or someone else) enter this email address when asking for password reset for an OpenERP account on ${ctx['url']}.
|
||||
|
||||
If you don't have asked for password reset, you can ignore this email.
|
||||
|
||||
To continue the password reset process, use the following link: ${object._rp_get_link()}
|
||||
|
||||
Kind Regards.
|
||||
]]></field>
|
||||
</record>
|
||||
<record model="email.template" id="email_password_changed">
|
||||
<field name="name">Password Changed</field>
|
||||
<field name="model_id" ref="base.model_res_users"/>
|
||||
<field name="email_from"><![CDATA[${object.company_id.name} <${object.company_id.email}>]]></field>
|
||||
<field name="email_to">(set by reset_password module)</field>
|
||||
<field name="subject">Password changed</field>
|
||||
<field name="body_text"><![CDATA[
|
||||
Your password for ${ctx['url']} has been changed.
|
||||
|
||||
Kind Regards.
|
||||
]]></field>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</openerp>
|
||||
@@ -1,118 +0,0 @@
|
||||
import urlparse
|
||||
import itsdangerous
|
||||
from openerp.tools import config
|
||||
from openerp.osv import osv, fields
|
||||
|
||||
TWENTY_FOUR_HOURS = 24 * 60 * 60
|
||||
|
||||
def serializer(dbname):
|
||||
key = '%s.%s' % (dbname, config['admin_passwd'])
|
||||
return itsdangerous.URLSafeTimedSerializer(key)
|
||||
|
||||
def generate_token(dbname, user):
|
||||
s = serializer(dbname)
|
||||
return s.dumps((user.id, user.user_email))
|
||||
|
||||
def valid_token(dbname, token, max_age=TWENTY_FOUR_HOURS):
|
||||
try:
|
||||
unsign_token(dbname, token, max_age)
|
||||
return True
|
||||
except itsdangerous.BadSignature:
|
||||
return False
|
||||
|
||||
def unsign_token(dbname, token, max_age=TWENTY_FOUR_HOURS):
|
||||
# TODO avoid replay by comparing timestamp with last connection date of user ? (need a query)
|
||||
s = serializer(dbname)
|
||||
return s.loads(token, max_age)
|
||||
|
||||
class res_users(osv.osv):
|
||||
_inherit = 'res.users'
|
||||
|
||||
_sql_constraints = [
|
||||
('email_uniq', 'UNIQUE (user_email)', 'You can not have two users with the same email!')
|
||||
]
|
||||
|
||||
def _rp_send_email(self, cr, uid, email, tpl_name, res_id, context=None):
|
||||
model, tpl_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'reset_password', tpl_name)
|
||||
assert model == 'email.template'
|
||||
|
||||
host = self.pool.get('ir.config_parameter').get_param(cr, uid, 'web.base.url', '')
|
||||
ctx = dict(context or {}, url=host)
|
||||
|
||||
msg_id = self.pool.get(model).send_mail(cr, uid, tpl_id, res_id, force_send=False, context=ctx)
|
||||
MailMessage = self.pool.get('mail.message')
|
||||
MailMessage.write(cr, uid, [msg_id], {'email_to': email}, context=context)
|
||||
MailMessage.send(cr, uid, [msg_id], context=context)
|
||||
|
||||
def _rp_get_link(self, cr, uid, ids, context=None):
|
||||
assert len(ids) == 1
|
||||
user = self.browse(cr, uid, ids[0], context=context)
|
||||
host = self.pool.get('ir.config_parameter').get_param(cr, uid, 'web.base.url', '')
|
||||
token = generate_token(cr.dbname, user)
|
||||
link = urlparse.urljoin(host, '/reset_password?db=%s&token=%s' % (cr.dbname, token))
|
||||
return link
|
||||
|
||||
def send_reset_password_request(self, cr, uid, email, context=None):
|
||||
uid = 1
|
||||
ids = self.search(cr, uid, [('user_email', '=', email)], context=context)
|
||||
assert len(ids) <= 1
|
||||
if not ids:
|
||||
_m, company_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'base', 'main_company')
|
||||
self._rp_send_email(cr, uid, email, 'email_no_user', company_id, context=context)
|
||||
else:
|
||||
self._rp_send_email(cr, uid, email, 'email_reset_link', ids[0], context=context)
|
||||
return True
|
||||
|
||||
res_users()
|
||||
|
||||
|
||||
class reset_pw_wizard(osv.TransientModel):
|
||||
_name = 'reset_password.wizard'
|
||||
_rec_name = 'pw'
|
||||
_columns = {
|
||||
'pw': fields.char('Password', size=64),
|
||||
'cpw': fields.char('Confirm Password', size=64),
|
||||
'token': fields.char('Token', size=128),
|
||||
'state': fields.selection([(x, x) for x in 'draft done missmatch error'.split()], required=True),
|
||||
}
|
||||
_defaults = {
|
||||
'state': 'draft',
|
||||
}
|
||||
|
||||
def create(self, cr, uid, values, context=None):
|
||||
# NOTE here, invalid values raises exceptions to avoid storing
|
||||
# sensitive data into the database (which then are available to anyone)
|
||||
|
||||
token = values.get('token')
|
||||
pw = values.get('pw')
|
||||
cpw = values.get('cpw')
|
||||
|
||||
if pw != cpw:
|
||||
raise osv.except_osv('Error', 'Passwords missmatch')
|
||||
|
||||
Users = self.pool.get('res.users')
|
||||
|
||||
try:
|
||||
user_id, user_email = unsign_token(cr.dbname, token)
|
||||
except Exception:
|
||||
raise osv.except_osv('Error', 'Invalid token')
|
||||
|
||||
Users.write(cr, 1, user_id, {'password': pw}, context=context)
|
||||
Users._rp_send_email(cr, 1, user_email, 'email_password_changed', user_id, context=context)
|
||||
|
||||
values = {'state': 'done'}
|
||||
|
||||
return super(reset_pw_wizard, self).create(cr, uid, values, context)
|
||||
|
||||
def change(self, cr, uid, ids, context=None):
|
||||
return True
|
||||
|
||||
def onchange_token(self, cr, uid, ids, token, context=None):
|
||||
if not valid_token(cr.dbname, token):
|
||||
return {'value': {'state': 'error'}}
|
||||
return {}
|
||||
|
||||
def onchange_pw(self, cr, uid, ids, pw, cpw, context=None):
|
||||
if pw != cpw:
|
||||
return {'value': {'state': 'missmatch'}}
|
||||
return {'value': {'state': 'draft'}}
|
||||
@@ -21,7 +21,6 @@ openerp.reset_password = function(instance) {
|
||||
this.do_warn(_t("Login"), _t("No database selected !"));
|
||||
return false;
|
||||
}
|
||||
|
||||
var $m = $e.find('form input[name=is_reset_pw]');
|
||||
if ($m.is(':checked')) {
|
||||
var email = $e.find('form input[name=email]').val()
|
||||
@@ -30,7 +29,6 @@ openerp.reset_password = function(instance) {
|
||||
return this._super(ev);
|
||||
}
|
||||
},
|
||||
|
||||
do_reset_password: function(db, email) {
|
||||
var self = this;
|
||||
instance.connection.session_authenticate(db, 'anonymous', 'anonymous', true).pipe(function () {
|
||||
|
||||
Reference in New Issue
Block a user