# HG changeset patch # User Daniele Nicolodi # Date 1313943446 -7200 # Node ID 7d03f602cade090586c56371453575777f1a287c # Parent 6a52c9c3d5ff1ea7347af91a4c5fc76c8efae7db 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. diff -r 6a52c9c3d5ff -r 7d03f602cade src/ltpdarepo/templates/mail/activate.txt --- /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 }} diff -r 6a52c9c3d5ff -r 7d03f602cade src/ltpdarepo/templates/users/activate.html --- /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 %} +

Activate user «{{ username }}» account

+

Please choose a safe password

+{{ forms.render(form) }} +{% endblock %} diff -r 6a52c9c3d5ff -r 7d03f602cade src/ltpdarepo/user.py --- 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() diff -r 6a52c9c3d5ff -r 7d03f602cade src/ltpdarepo/views/profile.py --- 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('

Token expired.

') + except BadSignature: + raise BadRequest('

Invalid token.

') + if value != username: + raise BadRequest('

Invalid token.

') + + # 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('

User already activated.

') + + +@app.route('//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('//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) diff -r 6a52c9c3d5ff -r 7d03f602cade src/ltpdarepo/views/users.py --- 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: %s' % (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