[IMP] auth_reset_password: new implementation based on auth_signup

bzr revid: rco@openerp.com-20121001150723-hcr2gcqqt7jsytp6
This commit is contained in:
Raphael Collet 2012-10-01 17:07:23 +02:00
parent c2a1e7e15d
commit 07ba5a0ce2
9 changed files with 184 additions and 251 deletions

View File

@ -1 +1,23 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
# Copyright (C) 2012-today OpenERP SA (<http://www.openerp.com>)
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 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 Affero General Public License for more details
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>
#
##############################################################################
import controllers
import auth_reset_password

View File

@ -1,3 +1,24 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
# Copyright (C) 2012-today OpenERP SA (<http://www.openerp.com>)
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 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 Affero General Public License for more details
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>
#
##############################################################################
{
'name': 'Reset Password',
'description': """
@ -9,9 +30,8 @@ Allow users to reset their password from the login page.
'category': 'Authentication',
'website': 'http://www.openerp.com',
'installable': True,
'depends': ['auth_anonymous', 'email_template'],
'depends': ['auth_signup', 'email_template'],
'data': ['auth_reset_password.xml'],
'js': ['static/src/js/reset_password.js'],
'css': ['static/src/css/reset_password.css'],
'qweb': ['static/src/xml/reset_password.xml'],
}

View File

@ -1,130 +1,55 @@
import base64
import hashlib
import simplejson
import time
import urlparse
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
# Copyright (C) 2012-today OpenERP SA (<http://www.openerp.com>)
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 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 Affero General Public License for more details
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>
#
##############################################################################
from openerp.tools import config
from openerp.osv import osv, fields
from openerp import SUPERUSER_ID
from openerp.tools.misc import DEFAULT_SERVER_DATETIME_FORMAT
TWENTY_FOUR_HOURS = 24 * 60 * 60
from datetime import datetime, timedelta
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 now(**kwargs):
dt = datetime.now() + timedelta(**kwargs)
return dt.strftime(DEFAULT_SERVER_DATETIME_FORMAT)
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'
def _auth_reset_password_secret(self, cr, uid, 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 reset_password(self, cr, uid, login, context=None):
""" retrieve the user corresponding to login (login or email),
create a specific signup token, and send it to the user
"""
user_ids = self.search(cr, uid, [('login', '=', login)], context=context)
if not user_ids:
user_ids = self.search(cr, uid, [('email', '=', login)], context=context)
if len(user_ids) != 1:
raise Exception('Reset password: invalid username or email')
def _auth_reset_password_host(self, cr, uid, context=None):
return self.pool.get('ir.config_parameter').get_param(cr, uid, 'web.base.url', '')
# prepare reset password signup
user = self.browse(cr, uid, user_ids[0], context)
user.partner_id.signup_prepare(expiration=now(days=+1))
def _auth_reset_password_link(self, cr, uid, ids, context=None):
assert len(ids) == 1
host = self._auth_reset_password_host(cr, uid, context)
secret = self._auth_reset_password_secret(cr, uid, context)
msg_src = {
'time': time.time(),
'uid': ids[0],
}
msg, sign = message_sign(msg_src, secret)
link = urlparse.urljoin(host, '/login?db=%s&login=anonymous&key=anonymous#action=reset_password&token=%s' % (cr.dbname, msg))
return link
# send email to user with their signup url
user = self.browse(cr, uid, user.id, context)
template = self.pool.get('ir.model.data').get_object(cr, uid, 'auth_reset_password', 'reset_password_email')
assert template._name == 'email.template'
self.pool.get('email.template').send_mail(cr, uid, template.id, user.id, force_send=True, context=context)
def _auth_reset_password_check_token(self, cr, uid, token, context=None):
secret = self._auth_reset_password_secret(cr, uid, context)
data = message_check(token, secret)
if data and (time.time() - data['time'] < TWENTY_FOUR_HOURS):
return data
return None
def _auth_reset_password_send_email(self, cr, uid, email_to, tpl_name, res_id, context=None):
model, tpl_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'auth_reset_password', tpl_name)
assert model == 'email.template'
msg_id = self.pool.get(model).send_mail(cr, uid, tpl_id, res_id, force_send=False, context=context)
MailMessage = self.pool.get('mail.message')
MailMessage.write(cr, uid, [msg_id], {'email_to': email_to}, context=context)
MailMessage.send(cr, uid, [msg_id], context=context)
def send_reset_password_request(self, cr, uid, email, context=None):
# TODO reseting a password knowing only an email is not good enough (email can be shared between multiple logins).
ids = self.pool.get('res.users').search(cr, SUPERUSER_ID, [('user_email', '=', email)], context=context)
if ids:
self._auth_reset_password_send_email(cr, SUPERUSER_ID, email, 'reset_password_email', ids[0], context=context)
return True
#else:
# _m, company_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'base', 'main_company')
# self._auth_reset_password_send_email(cr, uid, email, 'email_no_user', company_id, context=context)
return False
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)
pw = values.get('password')
if not pw or pw != values.get('password_confirmation'):
raise osv.except_osv('Error', 'Passwords missmatch')
Users = self.pool.get('res.users')
data = Users._auth_reset_password_check_token(cr, uid, values.get('token', ''))
if data:
Users.write(cr, SUPERUSER_ID, 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'}}
def onchange_token(self, cr, uid, ids, token, context=None):
Users = self.pool.get('res.users')
if not Users._auth_reset_password_check_token(cr, uid, token, context=context):
return {'value': {'state': 'error'}}
return {}

View File

@ -7,56 +7,15 @@
<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" eval="False"><!--(set by reset_password module)--></field>
<field name="email_to">${object.email}</field>
<field name="subject">Password reset</field>
<field name="body_html"><![CDATA[
<p>A password reset was requested the OpenERP account linked to this email on ${object._auth_reset_password_host()}</p>
<p>A password reset was requested for the OpenERP account linked to this email.</p>
<p>You may change your password following this <a href="${object._auth_reset_password_link()}">link</a>,
or by copy-pasting the following URL in your browser: ${object._auth_reset_password_link()}</p>
<p>You may change your password following <a href="${object.signup_url}">this link</a>.</p>
<p>Note: If you did not ask for a password reset, you can safely ignore this email.</p>]]></field>
</record>
<!-- TODO get own css -->
<record id="reset_password_wizard_form_view" model="ir.ui.view">
<field name="name">auth.reset_password.form</field>
<field name="model">auth.reset_password</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="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 colspan="4" states="error" col="1">
<div>Invalid or expired token</div>
<button special="cancel" string="Close"/>
</group>
<group colspan="4" states="done" col="1">
<div>Password changed. We sent you an email confirming the password change.</div>
<button special="cancel" string="Close"/>
</group>
</form>
</field>
</record>
<record id="action_reset" model="ir.actions.act_window">
<field name="name">Reset Password</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">auth.reset_password</field>
<field name="view_type">form</field>
<field name="view_mode">form</field>
<field name="target">new</field>
</record>
</data>
</openerp>

View File

@ -0,0 +1,24 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
# Copyright (C) 2012-today OpenERP SA (<http://www.openerp.com>)
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 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 Affero General Public License for more details
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>
#
##############################################################################
import main
# vim:expandtab:tabstop=4:softtabstop=4:shiftwidth=4:

View File

@ -0,0 +1,52 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
# Copyright (C) 2012-today OpenERP SA (<http://www.openerp.com>)
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 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 Affero General Public License for more details
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>
#
##############################################################################
from openerp import SUPERUSER_ID
from openerp.modules.registry import RegistryManager
import openerp.addons.web.common.http as openerpweb
import werkzeug
import logging
_logger = logging.getLogger(__name__)
class Controller(openerpweb.Controller):
_cp_path = '/auth_reset_password'
@openerpweb.httprequest
def reset_password(self, req, dbname, login):
""" retrieve user, and perform reset password """
url = '/'
registry = RegistryManager.get(dbname)
with registry.cursor() as cr:
try:
res_users = registry.get('res.users')
res_users.reset_password(cr, SUPERUSER_ID, login)
cr.commit()
message = 'An email has been sent with credentials to reset your password'
except Exception as e:
# signup error
_logger.exception('error when resetting password')
message = e.message
url = "/#action=login&error_message=%s" % werkzeug.urls.url_quote(message)
return werkzeug.utils.redirect(url)
# vim:expandtab:tabstop=4:softtabstop=4:shiftwidth=4:

View File

@ -1,12 +0,0 @@
.openerp .oe_login .oe_login_pane {
height: 152px;
}
.openerp .oe_login .oe_login_pane ul.oe_login_switch a {
color: #eeeeee;
margin: 0 8px;
}
.openerp .oe_login .oe_login_pane ul.oe_login_switch a:hover {
text-decoration: underline;
}

View File

@ -1,72 +1,30 @@
openerp.auth_reset_password = function(instance) {
var _t = instance.web._t;
instance.web.Login.include({
start: function() {
var $e = this.$el;
$e.find('a.oe_login_switch').click(function() {
$e.find('ul.oe_login_switch').toggle();
var $m = $e.find('form input[name=is_reset_pw]');
$m.attr('checked', !$m.is(':checked'));
});
this.$('a.oe_reset_password').click(this.do_reset_password);
return this._super();
},
on_submit: function(ev) {
if(ev) {
do_reset_password: function(ev) {
if (ev) {
ev.preventDefault();
}
var $e = this.$el;
var db = $e.find("form [name=db]").val();
var db = this.$("form [name=db]").val();
var login = this.$("form input[name=login]").val();
if (!db) {
this.do_warn(_t("Login"), _t("No database selected !"));
this.do_warn("Login", "No database selected !");
return false;
} else if (!login) {
this.do_warn("Login", "Please enter a username or email address.")
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();
return this.do_reset_password(db, email);
} else {
return this._super(ev);
}
},
do_reset_password: function(db, email) {
var self = this;
instance.session.session_authenticate(db, 'anonymous', 'anonymous', true).pipe(function () {
var func = new instance.web.Model("res.users").get_func("send_reset_password_request");
return func(email).then(function(res) {
// show message
self.do_notify(_t('Reset Password'), _.str.sprintf(_t('We have sent an email to %s with further instructions'), email), true);
}, function(error, event) {
// no traceback please
event.preventDefault();
});
}).fail(function(error, event) {
// cannot log as anonymous or reset_password not installed
self.do_warn(_t('Reset Password'), _.str.sprintf(_t('Reset Password functionnality is not available for database %s'), db), true);
});
var params = {
dbname : db,
login: login,
};
var url = "/auth_reset_password/reset_password?" + $.param(params);
window.location = url;
}
});
instance.reset_password = {};
instance.reset_password.ResetPassword = instance.web.Widget.extend({
init: function(parent, params) {
this._super(parent);
this.token = (params && params.token) || false;
},
start: function() {
this.do_action({
name: 'Reset Password',
type: 'ir.actions.act_window',
context: {default_token: this.token},
res_model: 'auth.reset_password',
target: 'new',
views: [[false, 'form']]
});
}
});
instance.web.client_actions.add("reset_password", "instance.reset_password.ResetPassword");
};

View File

@ -1,26 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- vim:fdl=1:
-->
<!-- vim:fdl=1: -->
<templates id="template" xml:space="preserve">
<t t-extend="Login">
<t t-jquery="form ul:first">
// addClass does not work :(
this.attr('class', (this.attr('class') || '') + ' oe_login_switch');
</t>
<t t-jquery="form ul:first li:last" t-operation="after">
<li>
<a class="oe_login_switch" href="#">Forgot your password?</a>
</li>
</t>
<t t-jquery="form ul:first" t-operation="after">
<ul class="oe_login_switch" style="display:none;">
<li style="display:none;"><input type="checkbox" name="is_reset_pw"/></li>
<li>Email</li>
<li><input type="email" name="email"/></li>
<li><button name="submit">Reset Password</button></li>
<li><a class="oe_login_switch" href="#">&lt; Back</a></li>
</ul>
<li><a class="oe_reset_password" href="#">Reset password</a></li>
</t>
</t>
</templates>