hooks.py 5.19 KB
Newer Older
Charles Hebert's avatar
0.1.0  
Charles Hebert committed
1
2
3
""" this module contains server side hooks for cleaning forgotpwd table
"""

4
5
6
7
8
9
import random
import string
from datetime import datetime, timedelta

from logilab.common.decorators import monkeypatch
from yams import ValidationError
Sylvain Thénault's avatar
Sylvain Thénault committed
10

11
from cubicweb.selectors import is_instance
12
from cubicweb.crypto import encrypt
Sylvain Thénault's avatar
Sylvain Thénault committed
13
from cubicweb.server import hook
14
from cubicweb.server.repository import Repository
15
16
17
from cubicweb.sobjects.notification import NotificationView

_ = unicode
Charles Hebert's avatar
0.1.0  
Charles Hebert committed
18

Sylvain Thénault's avatar
Sylvain Thénault committed
19
20
21
class ServerStartupHook(hook.Hook):
    """on startup, register a task to delete old revocation key"""
    __regid__ = 'fpwd_startup'
Charles Hebert's avatar
0.1.0  
Charles Hebert committed
22
    events = ('server_startup',)
Sylvain Thénault's avatar
Sylvain Thénault committed
23
24
25
26
27

    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):
Charles Hebert's avatar
0.1.0  
Charles Hebert committed
28
            session = repo.internal_session()
Sylvain Thénault's avatar
Sylvain Thénault committed
29
30
31
32
33
34
            try:
                session.execute('DELETE Fpasswd F WHERE F revocation_date < %(date)s',
                                {'date': now()})
                session.commit()
            finally:
                session.close()
35
        # run looping task often enough to purge pwd-reset requests
Sylvain Thénault's avatar
Sylvain Thénault committed
36
37
        limit = self.repo.vreg.config['revocation-limit'] * 60
        self.repo.looping_task(limit, cleaning_revocation_key, self.repo)
38
39
40


class PasswordResetNotification(NotificationView):
Sylvain Thénault's avatar
Sylvain Thénault committed
41
    __regid__ = 'notif_after_add_entity'
42
    __select__ = is_instance('Fpasswd')
43
44
45
46
47
48
49
50
51
52
53
54
55
56

    content = _('''There was recently a request to change the password on your account.
If you requested this password change, please set a new password by following
the link below:

%(resetlink)s

If you do not want to change your password, you may ignore this message. The
link expires in %(limit)s minutes.

See you soon on %(base_url)s !
''')

    def subject(self):
Sylvain Thénault's avatar
Sylvain Thénault committed
57
        return self._cw._(u'Request to change your password')
58
59

    def recipients(self):
60
        fpasswd = self.cw_rset.get_entity(self.cw_row or 0, self.cw_col or 0)
61
        user = fpasswd.reverse_has_fpasswd[0]
62
        return [(user.cw_adapt_to('IEmailable').get_email(), user.property_value('ui.language'))]
63
64
65

    def context(self, **kwargs):
        return {
Sylvain Thénault's avatar
Sylvain Thénault committed
66
            'resetlink': self._cw.get_shared_data('resetlink', pop=True),
67
68
            # NOTE: it would probably be better to display the expiration date
            #       (with correct timezone)
Sylvain Thénault's avatar
Sylvain Thénault committed
69
70
            'limit': self._cw.vreg.config['revocation-limit'],
            'base_url': self._cw.base_url(),
71
            }
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128


@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 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',
                                            {'e': data['use_email']})
        if existing_requests:
            raise ValidationError(None, {None: session._('You have already asked for a new password.')})
        rset = session.execute('INSERT Fpasswd X: X revocation_id %(a)s, X revocation_date %(b)s, '
                               'U has_fpasswd X WHERE U primary_email E, E address %(e)s',
                               {'a':revocation_id, 'b':revocation_date, 'e': data['use_email']})
        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['forgotpwd-cypher-seed'])
        url = session.build_url('forgottenpasswordrequest', key=key)
        session.set_shared_data('resetlink', url)
        # mail is sent on commit
        session.commit()
    finally:
        session.close()


@monkeypatch(Repository)
def forgotpwd_change_passwd(self, data):
    session = self.internal_session()
    try:
        rset = session.execute('Any F, U WHERE U is CWUser, U primary_email E, '
                               'E address %(email)s, EXISTS(U has_fpasswd F, '
                               'F revocation_id %(revid)s)',
                               {'email': data['use_email'],
                                'revid': data['revocation_id']})
        if rset:
            forgotpwd = rset.get_entity(0,0)
            revocation_date = forgotpwd.revocation_date
            user = rset.get_entity(0,1)
            if revocation_date > datetime.now():
                session.execute('SET U upassword %(newpasswd)s WHERE U is CWUser, U eid %(usereid)s',
                                {'newpasswd':data['upassword'].encode('UTF-8'), 'usereid':user.eid})
                session.execute('DELETE Fpasswd F WHERE F eid %(feid)s',
                                {'feid':forgotpwd.eid})
                session.commit()
                msg = session._(u'Your password has been changed.')
            else:
                msg = session._(u'That link has either expired or is not valid.')
        else:
            msg = session._(u'You already changed your password. This link has expired.')
        return msg
    finally:
        session.close()