changeset 88:7d03f602cade

Implement user activation. New users are now created in an inactive state assigning to them an invalid password. A cryptographically signed token required for user account activation is sent to the user email and shown to the administrator. The activation procedure assigns an user chosen password to the account.
author Daniele Nicolodi <daniele@grinta.net>
date Sun, 21 Aug 2011 18:17:26 +0200
parents 6a52c9c3d5ff
children 9e2ae3d086ce
files src/ltpdarepo/templates/mail/activate.txt src/ltpdarepo/templates/users/activate.html src/ltpdarepo/user.py src/ltpdarepo/views/profile.py src/ltpdarepo/views/users.py
diffstat 5 files changed, 101 insertions(+), 34 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/ltpdarepo/templates/mail/activate.txt	Sun Aug 21 18:17:26 2011 +0200
@@ -0,0 +1,8 @@
+Dear {{ user.name or user.username }},
+
+to activate your account visit
+
+{{ url }}
+
+Regards,
+The LTPDA Repository Admin at {{ request.host }}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/ltpdarepo/templates/users/activate.html	Sun Aug 21 18:17:26 2011 +0200
@@ -0,0 +1,8 @@
+{% import 'forms.html' as forms %}
+{% extends "layout.html" %}
+{% block title %}Activate account{% endblock %}
+{% block body %}
+<h2>Activate user &#x00AB;{{ username }}&#x00BB; account</h2>
+<p class="discrete">Please choose a safe password</p>
+{{ forms.render(form) }}
+{% endblock %}
--- a/src/ltpdarepo/user.py	Sun Aug 21 18:17:26 2011 +0200
+++ b/src/ltpdarepo/user.py	Sun Aug 21 18:17:26 2011 +0200
@@ -9,12 +9,7 @@
 
 from ltpdarepo.form import Form
 
-
-def _generate_password():
-    import random
-    import string
-    chars = string.letters + string.digits
-    return "".join([random.choice(chars) for i in range(8)])
+INVALIDPASSWORD = '!xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
 
 
 class IUser(Form):
@@ -52,11 +47,12 @@
 
 
 class User(object):
-    __slots__ = ('username', 'password', 'name', 'surname', 'email', 'telephone', 'institution', 'admin')
+    __slots__ = ('username', 'name', 'surname', 'email',
+                 'telephone', 'institution', 'admin')
 
-    def __init__(self, username='', password='', name='', surname='', email='', telephone='', institution='', admin=False):
+    def __init__(self, username='', name='', surname='', email='',
+                 telephone='', institution='', admin=False):
         self.username = username
-        self.password = password
         self.name = name
         self.surname = surname
         self.email = email
@@ -87,14 +83,14 @@
         return User(**user)
 
     def create(self):
-        if not self.password:
-            self.password = _generate_password()
+        # use an invalid password to prevent user login
+        password = INVALIDPASSWORD
 
         curs = g.db.cursor()
 
         for host in ('localhost', '%'):
-            curs.execute("""CREATE USER %s@%s IDENTIFIED BY %s""",
-                         (self.username, host, self.password))
+            curs.execute("""CREATE USER %s@%s IDENTIFIED BY PASSWORD %s""",
+                         (self.username, host, password))
 
         curs.execute("""INSERT INTO users (username, given_name, family_name,
                                            email, telephone, institution, is_admin)
@@ -126,18 +122,13 @@
 
         g.db.commit()
 
-    def passwd(self, password=None):
-        if password is not None:
-            self.password = password
-        if not self.password:
-            self.password = _generate_password()
-
+    def passwd(self, password):
         curs = g.db.cursor()
 
         curs.execute("""SELECT Host FROM mysql.user WHERE User=%s""", self.username)
         hosts = [row[0] for row in curs.fetchall()]
         for host in hosts:
             curs.execute("""SET PASSWORD FOR %s@%s = PASSWORD(%s)""",
-                         (self.username, host, self.password))
+                         (self.username, host, password))
 
         g.db.commit()
--- a/src/ltpdarepo/views/profile.py	Sun Aug 21 18:17:26 2011 +0200
+++ b/src/ltpdarepo/views/profile.py	Sun Aug 21 18:17:26 2011 +0200
@@ -1,11 +1,9 @@
-from flask import Blueprint, abort, flash, render_template, request, redirect, url_for
+from flask import Blueprint, g, abort, flash, render_template, request, redirect, url_for, session
+from werkzeug.exceptions import BadRequest
 
+from ltpdarepo.sign import Signer, BadSignature, SignatureExpired
 from ltpdarepo.security import require, permission
-from ltpdarepo.form import Form
-from wtforms.fields import PasswordField
-from wtforms.validators import ValidationError
-
-from ltpdarepo.user import User, IUser, IPassword
+from ltpdarepo.user import User, IUser, IPassword, INVALIDPASSWORD
 
 app = Blueprint('user', __name__)
 
@@ -39,6 +37,48 @@
         return render_template('users/edit.html', username=username, form=form)
 
 
+def _validate_request(request, username):
+    token = request.values.get('token', '')
+    s = Signer()
+    try:
+        value = s.loads(token, maxage=3600*24)
+    except SignatureExpired:
+        raise BadRequest('<p>Token expired.</p>')
+    except BadSignature:
+        raise BadRequest('<p>Invalid token.</p>')
+    if value != username:
+        raise BadRequest('<p>Invalid token.</p>')
+
+    # check that the user is not active
+    curs = g.db.cursor()
+    curs.execute("""SELECT Password FROM mysql.user WHERE User=%s""", username)
+    if curs.fetchone()[0] != INVALIDPASSWORD:
+        raise BadRequest('<p>User already activated.</p>')
+
+
+@app.route('/<username>/activate', methods=['GET', 'POST'])
+def activate(username):
+    user = User.load(username)
+    if user is None:
+        # not found
+        abort(404)
+
+    # validate token
+    _validate_request(request, username)
+
+    form = IPassword()
+    if request.method == 'POST' and form.validate():
+        # set password
+        user.passwd(form.password.data)
+        flash('User account activated.')
+        # login if not already logged in
+        if 'username' not in session:
+            session['username'] = username
+        return redirect(url_for('index'))
+
+    return render_template('users/activate.html', username=username, form=form)
+
+
 @app.route('/<username>/password', methods=('GET', 'POST'))
 @require('user')
 def password(username):
@@ -51,7 +91,7 @@
         if request.method == 'POST' and form.validate():
             # set password
             user.passwd(form.password.data)
-            flash('Password changed.')
+            flash('Password set.')
             return redirect(url_for('user.view', username=username))
         return render_template('users/password.html', form=form)
 
--- a/src/ltpdarepo/views/users.py	Sun Aug 21 18:17:26 2011 +0200
+++ b/src/ltpdarepo/views/users.py	Sun Aug 21 18:17:26 2011 +0200
@@ -1,10 +1,14 @@
+from email.mime.text import MIMEText
+
 from flask import Blueprint, abort, flash, g, render_template, request, redirect, url_for
 
+from MySQLdb.cursors import DictCursor
+
 from ltpdarepo.security import require
 from ltpdarepo.user import User, IUser
 from ltpdarepo.form import Form
-
-from MySQLdb.cursors import DictCursor
+from ltpdarepo.sign import Signer
+from ltpdarepo.mail import Mailer
 
 app = Blueprint('manage.users', __name__)
 
@@ -13,9 +17,8 @@
 @require('admin')
 def index():
     curs = g.db.cursor(DictCursor)
-    curs.execute("""SELECT username,
-                           CONCAT(given_name, ' ', family_name) AS name,
-                           email
+    curs.execute("""SELECT username, email,
+                    CONCAT(given_name, ' ', family_name) AS name
                     FROM users""")
     users = curs.fetchall()
     return render_template('users/index.html', users=users)
@@ -29,7 +32,7 @@
         # not found
         abort(404)
     form = IUser(obj=user)
-    
+
     privs = {}
     curs = g.db.cursor()
     curs.execute('''SELECT DISTINCT Db, Select_priv, Insert_priv,
@@ -67,7 +70,23 @@
         user = User()
         form.update(user)
         user.create()
-        flash('User "%s" created.' % form.data['username'])
+
+        # generate activation token
+        token = Signer().dumps(user.username)
+
+        # activation url
+        url = url_for('user.activate', username=user.username, token=token, _external=True)
+
+        # send activation token
+        mailer = Mailer()
+        message = MIMEText(render_template('mail/activate.txt', user=user, url=url))
+        message['Subject'] = 'LTPDA Repository account activation'
+        message['From'] = mailer.admin_email_addr
+        message['To'] = user.emailaddr
+        mailer.send(message)
+
+        flash('User created. Activation token: <a href="%s"><tt>%s</tt></a>' % (url, token))
+
         return redirect(url_for('manage.users.index'))
     return render_template('users/create.html', form=form)
 
@@ -88,4 +107,5 @@
         return redirect(url_for('manage.users.index'))
     return render_template('users/drop.html', form=form, user=user)
 
+
 module = app