Commit 60b0439a authored by Sylvain Thénault's avatar Sylvain Thénault
Browse files

3.6 api updates

parent cdc15a307b2b
......@@ -14,8 +14,50 @@ http://www.logilab.fr -- mailto:contact@logilab.fr'''
author = 'LOGILAB S.A. (Paris, FRANCE)'
author_email = 'contact@logilab.fr'
short_desc = 'password recovery cube'
long_desc = '''password recovery cube'''
short_desc = 'password recovery component for the CubicWeb framework'
long_desc = """\
Summary
-------
The `forgotpwd` cube cube provides an easy way to generate a new
password for an user.
Depends
-------
You must use the cube `registration`.
Usage
-----
This cube creates a new entity called `Fpasswd`. This is an internal
entity: managers and users can't read/delete or modify this kink of
entity.
The workflow of password recovery is defined below :
1. ask for a new password, the user must have a valid primary email
associated to his account.
.. image:: cube_forgotpwd_1.png
2. An email has been sent. This email contains a generated url associated to an
user. This link is valid during a short period. This time limit can be
configured in the all-in-one.conf file:
.. sourcecode:: ini
[FORGOTPWD]
revocation-limit=30 # minutes
3. If the link is valid, the user can change his password in a new form.
.. image:: cube_forgotpwd_1.png
There is an automatic task that delete periodically all old Fpasswd which are
stored in the database. This task is started at the launching of the
application.
"""
web = 'http://www.cubicweb.org/project/%s' % distname
......
""" this module contains server side hooks for cleaning forgotpwd table
"""
from datetime import datetime
from cubicweb.selectors import implements
from cubicweb.server import hooksmanager
from cubicweb.server import hook
from cubicweb.sobjects.notification import NotificationView
_ = unicode
class ServerStartupHook(hooksmanager.Hook):
""" Delete old revocation key
"""
class ServerStartupHook(hook.Hook):
"""on startup, register a task to delete old revocation key"""
__regid__ = 'fpwd_startup'
events = ('server_startup',)
def call(self, repo):
from datetime import datetime
def cleaning_revocation_key(repo):
def __call__(self):
# XXX use named args and inner functions to avoid referencing globals
# which may cause reloading pb
def cleaning_revocation_key(repo, now=datetime.now):
session = repo.internal_session()
session.execute('DELETE Fpasswd F WHERE F revocation_date < %(date)s', {'date': datetime.now()})
session.commit()
session.close()
# revocation keu must be deleted
cleaning_revocation_key(repo)
try:
session.execute('DELETE Fpasswd F WHERE F revocation_date < %(date)s',
{'date': now()})
session.commit()
finally:
session.close()
# run looping task often enough to purge pwd-reset requests
limit = self.vreg.config['revocation-limit']
repo.looping_task(limit * 60, cleaning_revocation_key, repo)
limit = self.repo.vreg.config['revocation-limit'] * 60
self.repo.looping_task(limit, cleaning_revocation_key, self.repo)
class PasswordResetNotification(NotificationView):
id = 'notif_after_add_entity'
__regid__ = 'notif_after_add_entity'
__select__ = implements('Fpasswd')
content = _('''There was recently a request to change the password on your account.
......@@ -42,18 +47,18 @@ See you soon on %(base_url)s !
''')
def subject(self):
return self.req._(u'Request to change your password')
return self._cw._(u'Request to change your password')
def recipients(self):
fpasswd = self.rset.get_entity(0, 0)
fpasswd = self._cw.rset.get_entity(self.cw_row or 0, self.cw_col or 0)
user = fpasswd.reverse_has_fpasswd[0]
return [(user.get_email(), user.property_value('ui.language'))]
def context(self, **kwargs):
return {
'resetlink': self.req.get_shared_data('resetlink', pop=True),
'resetlink': self._cw.get_shared_data('resetlink', pop=True),
# NOTE: it would probably be better to display the expiration date
# (with correct timezone)
'limit': self.vreg.config['revocation-limit'],
'base_url': self.req.base_url(),
'limit': self._cw.vreg.config['revocation-limit'],
'base_url': self._cw.base_url(),
}
# cube's specific schema
"""
"""forgot password schema
:organization: Logilab
:copyright: 2001-2010 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2.
:copyright: 2009-2010 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2.
:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
:license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses
"""
from cubicweb.schemas.base import CWUser
from yams.buildobjs import Datetime, String, EntityType, RelationDefinition
from yams.buildobjs import EntityType, Datetime, String, RelationDefinition
class Fpasswd(EntityType):
permissions = {'read': ('managers',),
'add': (),
'delete': (),
'update': (),
}
# Fpasswd handled by internal hooks, simply let managers removing manually
# if desired
__permissions__ = {'read': ('managers',),
'add': (),
'delete': ('mamangers',),
'update': (),
}
revocation_id = String(required=True, unique=True)
revocation_date = Datetime(required=True)
class has_fpasswd(RelationDefinition):
permissions = {'read': ('managers',),
'add': (),
'delete': (),
}
name = 'has_fpasswd'
__permissions__ = {'read': ('managers',),
'add': (),
'delete': ('managers',),
}
subject = 'CWUser'
object = 'Fpasswd'
......@@ -12,6 +12,8 @@ from datetime import datetime, timedelta
from yams import ValidationError
from logilab.mtconverter import xml_escape
from cubicweb.web import (Redirect, controller, form, formwidgets as wdg,
formfields as ff)
from cubicweb.web.views import forms, urlrewrite, basetemplates
......@@ -20,8 +22,6 @@ from cubicweb.server.repository import Repository
from cubes.registration.views import CaptchaWidget, encrypt, decrypt
from logilab.mtconverter import xml_escape
_ = unicode
# Login form
......@@ -31,20 +31,20 @@ class LogFormTemplateForgotPassword(basetemplates.LogFormTemplate):
def login_form(self, id):
super(LogFormTemplateForgotPassword, self).login_form(id)
self.req.add_css('cubes.forgotpwd.css')
self._cw.add_css('cubes.forgotpwd.css')
self.w(u'<p style="text-align:center"><a href="%s">%s</a></p>' % (
self.build_url('forgottenpassword'), self.req._('Forgot your password ?')))
self.build_url('forgottenpassword'), self._cw._('Forgot your password ?')))
# First form, send an email
# -------------------------
class ForgottenPasswordForm(forms.FieldsForm):
id = 'forgottenpassword'
__regid__ = 'forgottenpassword'
form_buttons = [wdg.SubmitButton()]
@property
def action(self):
return self.req.build_url(u'forgottenpassword_sendmail')
return self._cw.build_url(u'forgottenpassword_sendmail')
use_email = ff.StringField(widget=wdg.TextInput(), required=True, label=_(u'your email address'))
captcha = ff.StringField(widget=CaptchaWidget(), required=True,
......@@ -52,15 +52,15 @@ class ForgottenPasswordForm(forms.FieldsForm):
help=_('please copy the letters from the image'))
class ForgottenPasswordFormView(form.FormViewMixIn, StartupView):
id = 'forgottenpassword'
__regid__ = 'forgottenpassword'
def call(self):
form = self.vreg['forms'].select('forgottenpassword', self.req)
self.w(u'<p>%s</p>' % self.req._(u'Forgot your password ?'))
form = self.vreg['forms'].select('forgottenpassword', self._cw)
self.w(u'<p>%s</p>' % self._cw._(u'Forgot your password ?'))
self.w(form.render())
class ForgottenPasswordSendMailController(controller.Controller):
id = 'forgottenpassword_sendmail'
__regid__ = 'forgottenpassword_sendmail'
def publish(self, rset=None):
data = self.checked_data()
......@@ -71,72 +71,72 @@ class ForgottenPasswordSendMailController(controller.Controller):
except Exception, exc:
msg = str(exc)
else:
msg = self.req._(u'An email has been sent, follow instructions in there to change your password.')
msg = self._cw._(u'An email has been sent, follow instructions in there to change your password.')
raise Redirect(self.build_url('pwdsent', __message=msg))
def checked_data(self):
'''only basic data check here (required attributes and password
confirmation check)
'''
fieldsform = self.vreg['forms'].select('forgottenpassword', self.req)
fieldsform = self.vreg['forms'].select('forgottenpassword', self._cw)
data = {}
errors = {}
for field in fieldsform._fields_:
value = self.req.form.get(field.name, u'').strip()
value = self._cw.form.get(field.name, u'').strip()
if not value:
if field.required:
errors[field.name] = self.req._('required attribute')
errors[field.name] = self._cw._('required attribute')
data[field.name] = value
captcha = self.req.get_session_data('captcha', None, pop=True)
captcha = self._cw.get_session_data('captcha', None, pop=True)
if captcha is None:
errors[None] = self.req._('unable to check captcha, please try again')
errors[None] = self._cw._('unable to check captcha, please try again')
elif data['captcha'].lower() != captcha.lower():
errors['captcha'] = self.req._('incorrect captcha value')
errors['captcha'] = self._cw._('incorrect captcha value')
if errors:
raise ValidationError(None, errors)
return data
class PasswordSentView(StartupView):
id = 'pwdsent'
__regid__ = 'pwdsent'
def call(self):
self.wview('index', self.rset)
self.wview('index', self.cw_rset)
# Second form, ask for a new password
# -----------------------------------
class ForgottenPasswordRequestForm(forms.FieldsForm):
id = 'forgottenpasswordrequest'
__regid__ = 'forgottenpasswordrequest'
form_buttons = [wdg.SubmitButton()]
@property
def action(self):
return self.req.build_url(u'forgottenpassword-requestconfirm')
return self._cw.build_url(u'forgottenpassword-requestconfirm')
upassword = ff.StringField(widget=wdg.PasswordInput(), required=True)
class ForgottenPasswordRequestView(form.FormViewMixIn, StartupView):
id = 'forgottenpasswordrequest'
__regid__ = 'forgottenpasswordrequest'
def check_key(self):
try:
return decrypt(self.req.form['key'], self.vreg.config['cypher-seed'])
return decrypt(self._cw.form['key'], self.vreg.config['cypher-seed'])
except:
msg = self.req._(u'Invalid link. Please try again.')
raise Redirect(self.req.build_url(u'forgottenpassword', __message=msg))
msg = self._cw._(u'Invalid link. Please try again.')
raise Redirect(self._cw.build_url(u'forgottenpassword', __message=msg))
def call(self):
key = self.check_key()
form = self.vreg['forms'].select('forgottenpasswordrequest', self.req)
form = self.vreg['forms'].select('forgottenpasswordrequest', self._cw)
form.form_add_hidden('use_email', key['use_email'])
form.form_add_hidden('revocation_id', key['revocation_id'])
self.w(u'<p>%s</p>' % self.req._(u'Update your password:'))
self.w(u'<p>%s</p>' % self._cw._(u'Update your password:'))
self.w(form.render())
class ForgottenPasswordRequestConfirm(controller.Controller):
id = 'forgottenpassword-requestconfirm'
__regid__ = 'forgottenpassword-requestconfirm'
def publish(self, rset=None):
data = self.checked_data()
......@@ -144,18 +144,18 @@ class ForgottenPasswordRequestConfirm(controller.Controller):
raise Redirect(self.build_url('pwdreset', __message=msg))
def checked_data(self):
fieldsform = self.vreg['forms'].select('forgottenpasswordrequest', self.req)
fieldsform = self.vreg['forms'].select('forgottenpasswordrequest', self._cw)
data = {}
errors = {}
for field in fieldsform._fields_:
value = self.req.form.get(field.name, u'').strip()
value = self._cw.form.get(field.name, u'').strip()
if not value:
if field.required:
errors[field.name] = self.req._('required attribute')
errors[field.name] = self._cw._('required attribute')
data[field.name] = value
data['use_email'] = self.req.form.get('use_email', '').strip()
data['revocation_id'] = self.req.form.get('revocation_id', '').strip()
if data['upassword'] != self.req.form.get('upassword-confirm'):
data['use_email'] = self._cw.form.get('use_email', '').strip()
data['revocation_id'] = self._cw.form.get('revocation_id', '').strip()
if data['upassword'] != self._cw.form.get('upassword-confirm'):
errors['upassword'] = _('passwords are different')
if errors:
raise ValidationError(None, errors)
......@@ -163,7 +163,7 @@ class ForgottenPasswordRequestConfirm(controller.Controller):
class PasswordResetView(StartupView):
id = 'pwdreset'
__regid__ = 'pwdreset'
def call(self):
self.wview('index', self.rset)
......@@ -174,13 +174,14 @@ class PasswordResetView(StartupView):
from cubicweb.server.repository import Repository
from logilab.common.decorators import monkeypatch
from cubicweb.common.mail import format_mail
from cubicweb.mail import format_mail
@monkeypatch(Repository)
def forgotpwd_send_email(self, data):
session = self.internal_session()
revocation_limit = self.config['revocation-limit']
revocation_id = u''.join([random.choice(string.letters+string.digits) for x in range(10)])
revocation_id = u''.join([random.choice(string.letters+string.digits)
for x in xrange(10)])
revocation_date = datetime.now() + timedelta(minutes=revocation_limit)
try:
existing_requests = session.execute('Any F WHERE U primary_email E, E address %(e)s, U has_fpasswd F',
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment