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

backport default to stable since 3.6 is the stable release

--HG--
branch : stable
......@@ -4,7 +4,7 @@
modname = 'forgotpwd'
distname = 'cubicweb-forgotpwd'
numversion = (0, 1, 0)
numversion = (0, 2, 0)
version = '.'.join(str(num) for num in numversion)
license = 'LCL'
......@@ -14,8 +14,46 @@ 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.
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.
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
......@@ -43,8 +81,11 @@ for dname in ('entities', 'views', 'sobjects', 'hooks', 'schema', 'data', 'i18n'
# Note: here, you'll need to add subdirectories if you want
# them to be included in the debian package
__depends_cubes__ = {'registration': '>= 0.2.1'}
__depends__ = {'cubicweb': '>= 3.5.0'}
__depends_cubes__ = {}
__depends__ = {'cubicweb': '>= 3.6.1',
'python-crypto': None,
'PIL': None,
}
__use__ = tuple(__depends_cubes__)
__recommend__ = ()
/* template specific CSS */
div#loginContent a {
color:black;
}
color: black;
text-align: center;
}
div#loginContent a:hover {
color:gray;
}
\ No newline at end of file
color: gray;
}
\ No newline at end of file
cubicweb-forgotpwd (0.2.0-1) unstable; urgency=low
* new upstream release
-- Sylvain Thénault <sylvain.thenault@logilab.fr> Thu, 25 Feb 2010 09:57:57 +0100
cubicweb-forgotpwd (0.1.0-2) unstable; urgency=low
* fix typo in debian/control dependencies
......
......@@ -4,15 +4,13 @@ Priority: optional
Maintainer: LOGILAB S.A. (Paris, FRANCE) <contact@logilab.fr>
Build-Depends: debhelper (>= 5.0.37.1), python (>=2.4), python-dev (>=2.4)
Standards-Version: 3.8.0
Homepage: http://www.cubicweb.org/project/cubicweb-forgotpwd
Package: cubicweb-forgotpwd
Architecture: all
Depends: cubicweb-common (>= 3.5.11), cubicweb-registration (>= 0.2.1)
Depends: cubicweb-common (>= 3.6.1), python-crypto, python-imaging
Description: password recovery cube
CubicWeb is a semantic web application framework.
This CubicWeb component provides password recovery functionnality.
.
password recovery cube
.
This package will install all the components you need to run the
cubicweb-forgotpwd application (cube :)..
CubicWeb is a semantic web application framework, see http://www.cubicweb.org
""" 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(),
}
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR Free Software Foundation, Inc.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"PO-Revision-Date: 2010-02-04 10:38+0100\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Project-Id-Version: cubicweb 3.5.11\n"
"PO-Revision-Date: 2010-02-04 19:13+0100\n"
"Last-Translator: Logilab Team <contact@logilab.fr>\n"
"Language-Team: en <contact@logilab.fr>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=CHARSET\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
#~ msgid ""
#~ msgstr ""
#~ "MIME-Version: 1.0\n"
#~ "Content-Type: text/plain; charset=UTF-8\n"
#~ "Content-Transfer-Encoding: 8bit\n"
#~ "Generated-By: pygettext.py 1.5\n"
#~ "Plural-Forms: nplurals=2; plural=(n > 1);\n"
"Generated-By: cubicweb-devtools\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
msgid ""
"An email has been sent, follow instructions in there to change your password."
......
msgid ""
msgstr ""
"Project-Id-Version: cubicweb 3.5.11\n"
"PO-Revision-Date: 2010-02-04 19:13+0100\n"
"Last-Translator: Logilab Team <contact@logilab.fr>\n"
"Language-Team: es <contact@logilab.fr>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: pygettext.py 1.5\n"
"Generated-By: cubicweb-devtools\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
msgid ""
......
msgid ""
msgstr ""
"Project-Id-Version: cubicweb 3.5.11\n"
"PO-Revision-Date: 2010-02-04 10:38+0100\n"
"PO-Revision-Date: 2010-02-04 19:13+0100\n"
"Last-Translator: Logilab Team <contact@logilab.fr>\n"
"Language-Team: fr <contact@logilab.fr>\n"
"MIME-Version: 1.0\n"
......@@ -68,7 +68,7 @@ msgstr ""
"ce message.\n"
"Ce lien expirera dans %(limit)s minutes.\n"
"\n"
"À bientôt sur %(base_url)s.\\n"
"À bientôt sur %(base_url)s.\n"
msgid "This Fpasswd"
msgstr ""
......
# 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': ('managers',),
'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'
# -*- coding: utf-8 -*-
from os.path import dirname, abspath, join
HERE = abspath(dirname(__file__))
options = (
('revocation-limit',
{'type' : 'int',
......@@ -8,4 +5,10 @@ options = (
'help': 'Forgot password link life time validity',
'group': 'forgotpwd', 'inputlevel': 2,
}),
('forgotpwd-cypher-seed',
{'type' : 'string',
'default': u"this is my dummy forgotpwd cypher seed",
'help': 'seed used to cypher validation key sent in forgot password email link',
'group': 'forgotpwd', 'inputlevel': 2,
}),
)
......@@ -12,55 +12,54 @@ from datetime import datetime, timedelta
from yams import ValidationError
from cubicweb.web import (Redirect, controller, form, formwidgets as wdg,
formfields as ff)
from cubicweb.web.views import forms, urlrewrite, basetemplates
from cubicweb.view import StartupView
from cubicweb.server.repository import Repository
from cubes.registration.views import CaptchaWidget, encrypt, decrypt
from logilab.mtconverter import xml_escape
from cubicweb.view import StartupView
from cubicweb.crypto import encrypt, decrypt
from cubicweb.web import (Redirect, controller, form, captcha,
formwidgets as wdg, formfields as ff)
from cubicweb.web.views import forms, urlrewrite, basetemplates
_ = unicode
# Login form
# ----------
class LogFormTemplateForgotPassword(basetemplates.LogFormTemplate):
class LogFormView(basetemplates.LogFormView):
def login_form(self, id):
super(LogFormTemplateForgotPassword, self).login_form(id)
self.req.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 ?')))
super(LogFormView, self).login_form(id)
self._cw.add_css('cubes.forgotpwd.css')
self.w(u'<span class="forgotpwdLink"><a href="%s">%s</a></span>' % (
xml_escape(self._cw.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,
captcha = ff.StringField(widget=captcha.CaptchaWidget(), required=True,
label=_('captcha'),
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._cw.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 +70,74 @@ 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._cw.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._cw.vreg.config['forgotpwd-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.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:'))
form = self._cw.vreg['forms'].select('forgottenpasswordrequest', self._cw)
form.add_hidden('use_email', key['use_email'])
form.add_hidden('revocation_id', key['revocation_id'])
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 +145,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)
cw = self._cw
fieldsform = cw.vreg['forms'].select('forgottenpasswordrequest', cw)
data = {}
errors = {}
for field in fieldsform._fields_:
value = self.req.form.get(field.name, u'').strip()
if not value:
if field.required:
errors[field.name] = self.req._('required attribute')
for field in fieldsform.fields:
value = cw.form.get(field.name, u'').strip()
if not value and field.required:
errors[field.name] = 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'] = cw.form.get('use_email', '').strip()
data['revocation_id'] = cw.form.get('revocation_id', '').strip()
if data['upassword'] != cw.form.get('upassword-confirm'):
errors['upassword'] = _('passwords are different')
if errors:
raise ValidationError(None, errors)
......@@ -163,7 +164,7 @@ class ForgottenPasswordRequestConfirm(controller.Controller):
class PasswordResetView(StartupView):
id = 'pwdreset'
__regid__ = 'pwdreset'
def call(self):
self.wview('index', self.rset)
......@@ -174,13 +175,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',
......@@ -193,7 +195,7 @@ def forgotpwd_send_email(self, data):
if not rset:
raise ValidationError(None, {None: session._(u'An error occured, this email address is unknown.')})
data['revocation_id'] = revocation_id
key = encrypt(data, session.vreg.config['cypher-seed'])
key = encrypt(data, session.vreg.config['forgotpwd-cypher-seed'])
url = session.build_url('forgottenpasswordrequest', key=key)
session.set_shared_data('resetlink', url)
# mail is sent on commit
......@@ -226,14 +228,6 @@ def forgotpwd_change_passwd(self, data):
finally:
session.close()
# Registration
# ------------
def registration_callback(vreg):
vreg.register_all(globals().values(), __name__, (LogFormTemplateForgotPassword,))
vreg.register_and_replace(LogFormTemplateForgotPassword,
basetemplates.LogFormTemplate)
# URL rewriting
# -------------
......@@ -244,3 +238,10 @@ class RegistrationSimpleReqRewriter(urlrewrite.SimpleReqRewriter):
('/pwdsent', dict(vid='pwdsent')),
('/pwdreset', dict(vid='pwdreset')),
]
# Registration
# ------------
def registration_callback(vreg):
vreg.register_all(globals().values(), __name__, (LogFormView,))
vreg.register_and_replace(LogFormView, basetemplates.LogFormView)
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