Mercurial > hg > ltpdarepo
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 «{{ username }}» 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