Skip to content

Commit

Permalink
Merge branch 'feature/password-reset-100039106' into develop
Browse files Browse the repository at this point in the history
Conflicts:
	librarian/static/.webassets-manifest
	librarian/static/css/content-b88598bc.css
	librarian/static/css/content-ba96052a.css
	librarian/static/css/content-c1a5d1e6.css
	librarian/static/css/main-6110eba4.css
	librarian/static/css/main-76e81060.css
	librarian/static/css/main-9e6a1495.css
	librarian/static/css/wizard-8b51edad.css
	librarian/static/css/wizard-91e60a22.css
	librarian/static/css/wizard-acc0f628.css
  • Loading branch information
Branko Vukelic committed Jul 29, 2015
2 parents 5dca3c1 + 191491e commit b0d6e77
Show file tree
Hide file tree
Showing 23 changed files with 8,174 additions and 17,302 deletions.
25 changes: 25 additions & 0 deletions librarian/forms/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,28 @@ def validate(self):
password2 = self.processed_data['password2']
if password1 != password2:
raise form.ValidationError('registration_error', {})


class PasswordResetForm(form.Form):
messages = {
'password_match': _("The entered passwords do not match."),
'invalid_token': _('Password reset token does not match any user'),
}
# Translators, used as label in create user form
reset_token = form.StringField(_("Password reset token"),
validators=[form.Required()],
placeholder='123456')
# Translators, used as label in password reset form
password1 = form.PasswordField(_("Password"),
validators=[form.Required()],
placeholder=_('password'))
# Translators, used as label in password reset form
password2 = form.PasswordField(_("Confirm Password"),
validators=[form.Required()],
placeholder=_('confirm password'))

def validate(self):
password1 = self.processed_data['password1']
password2 = self.processed_data['password2']
if password1 != password2:
raise form.ValidationError('password_match', {})
53 changes: 47 additions & 6 deletions librarian/lib/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,24 @@
file that comes with the source code, or http://www.gnu.org/licenses/gpl.txt.
"""

import datetime
import functools
import json
import sqlite3
import urllib
import random
import sqlite3
import hashlib
import urlparse
import datetime
import functools

import pbkdf2
from bottle import request, abort, redirect, hook

from .options import Options, DateTimeDecoder, DateTimeEncoder


TOKEN_CHARS = '0123456789'


class UserAlreadyExists(Exception):
pass

Expand Down Expand Up @@ -116,30 +121,56 @@ def encrypt_password(password):
return pbkdf2.crypt(password)


def set_password(username, clear_text):
""" Set password using provided clear-text password """
password = encrypt_password(clear_text)
db = request.db.sessions
query = db.Update('users', password=':password',
where='username = :username')
db.query(query, password=password, username=username)


def generate_reset_token(length=6):
# This token is not particularly secure, because the emphasis was on
# convenience rather than security. It is reasonably easy to crack the
# token.
return ''.join([random.choice(TOKEN_CHARS) for i in range(length)])


def is_valid_password(password, encrypted_password):
return encrypted_password == pbkdf2.crypt(password, encrypted_password)


def create_user(username, password, is_superuser=False, db=None,
overwrite=False):
overwrite=False, reset_token=None):
if not username or not password:
raise InvalidUserCredentials()

if not reset_token:
reset_token = generate_reset_token()

encrypted = encrypt_password(password)

sha1 = hashlib.sha1()
sha1.update(reset_token.encode('utf8'))
hashed_token = sha1.hexdigest()

user_data = {'username': username,
'password': encrypted,
'created': datetime.datetime.utcnow(),
'is_superuser': is_superuser}
'is_superuser': is_superuser,
'reset_token': hashed_token}

db = db or request.db.sessions
sql_cmd = db.Replace if overwrite else db.Insert
query = sql_cmd('users', cols=('username',
'password',
'created',
'is_superuser'))
'is_superuser',
'reset_token'))
try:
db.execute(query, user_data)
return reset_token
except sqlite3.IntegrityError:
raise UserAlreadyExists()

Expand All @@ -151,6 +182,16 @@ def get_user(username):
return db.result


def get_user_by_reset_token(token):
sha1 = hashlib.sha1()
sha1.update(token.encode('utf8'))
hashed_token = sha1.hexdigest()
db = request.db.sessions
query = db.Select(sets='users', where='reset_token = ?')
db.query(query, hashed_token)
return db.result


def login_user(username, password):
user = get_user(username)
if user and is_valid_password(password, user.password):
Expand Down
7 changes: 7 additions & 0 deletions librarian/migrations/sessions/05_add_reset_token_column.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
SQL = """
alter table users add column reset_token text;
"""


def up(db, conf):
db.executescript(SQL)
55 changes: 52 additions & 3 deletions librarian/routes/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,15 @@
"""

from bottle import request
from bottle_utils.i18n import i18n_path
from bottle_utils.html import set_qparam
from bottle_utils.form import ValidationError
from bottle_utils.csrf import csrf_protect, csrf_token, csrf_tag
from bottle_utils.i18n import i18n_path, i18n_url, lazy_gettext as _

from ..forms.auth import LoginForm
from ..forms.auth import LoginForm, PasswordResetForm
from ..lib.auth import get_user_by_reset_token, set_password
from ..utils.template import view, template
from ..utils.http import http_redirect
from ..utils.template import view
from ..utils.template_helpers import template_helper


Expand Down Expand Up @@ -50,9 +53,55 @@ def logout():
http_redirect(i18n_path(next_path))


@view('reset_password')
@csrf_token
def show_reset_form():
next_path = request.params.get('next', '/')
return dict(next_path=next_path, form=PasswordResetForm())


@view('reset_password')
@csrf_token
def reset():
next_path = request.params.get('next', '/')
form = PasswordResetForm(request.params)
if request.user.is_authenticated:
# Set arbitrary non-empty value to prevent form error. We don't really
# care about this field otherwise.
form.reset_token.bind_value('not needed')
if not form.is_valid():
return dict(next_path=next_path, form=form)
if request.user.is_authenticated:
username = request.user.username
else:
user = get_user_by_reset_token(form.processed_data['reset_token'])
if not user:
form._error = ValidationError('invalid_token', {'value': ''})
return dict(next_path=next_path, form=form)
username = user.username
set_password(username, form.processed_data['password1'])
if request.user.is_authenticated:
request.user.logout()
login_url = i18n_url('auth:login_form') + set_qparam(
next=next_path).to_qs()
return template('feedback.tpl',
# Translators, used as page title on feedback page
page_title=_('New password was set'),
# Translators, used as link label on feedback page in "You
# will be taken to log-in page..."
redirect_target=_('log-in page'),
# Translators, shown after password has been changed
message=_("Password for username '%(username)s' has been "
"set.") % {'username': username},
status='success',
redirect_url=login_url)


def routes(app):
return (
('auth:login_form', show_login_form, 'GET', '/login/', {}),
('auth:login', login, 'POST', '/login/', {}),
('auth:logout', logout, 'GET', '/logout/', {}),
('auth:reset_form', show_reset_form, 'GET', '/reset-password/', {}),
('auth:reset', reset, 'POST', '/reset-password/', {}),
)
9 changes: 6 additions & 3 deletions librarian/routes/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,21 +74,24 @@ def has_no_superuser():
@setup_wizard.register_step('superuser', template='setup/step_superuser.tpl',
method='GET', index=3, test=has_no_superuser)
def setup_superuser_form():
return dict(form=RegistrationForm())
return dict(form=RegistrationForm(),
reset_token=auth.generate_reset_token())


@setup_wizard.register_step('superuser', template='setup/step_superuser.tpl',
method='POST', index=3, test=has_no_superuser)
def setup_superuser():
form = RegistrationForm(request.forms)
reset_token = request.params.get('reset_token')
if not form.is_valid():
return dict(successful=False, form=form)
return dict(successful=False, form=form, reset_token=reset_token)

auth.create_user(form.processed_data['username'],
form.processed_data['password1'],
is_superuser=True,
db=request.db.sessions,
overwrite=True)
overwrite=True,
reset_token=reset_token)
return dict(successful=True)


Expand Down
22 changes: 11 additions & 11 deletions librarian/static/.webassets-manifest
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
{
"css/content-%(version)s.css": "ba96052a",
"css/dashboard-%(version)s.css": "dc20454c",
"css/main-%(version)s.css": "6110eba4",
"css/wizard-%(version)s.css": "acc0f628",
"js/content-%(version)s.js": "d5d568de",
"js/dashboard-%(version)s.js": "6dab1ac5",
"js/files-%(version)s.js": "d969cdea",
"js/reader-%(version)s.js": "06a44831",
"js/setup-%(version)s.js": "10ea5fdc",
"js/ui-%(version)s.js": "75b7b478"
{
"css/content-%(version)s.css": "c1a5d1e6",
"css/dashboard-%(version)s.css": "dc20454c",
"css/main-%(version)s.css": "f6f95a12",
"css/wizard-%(version)s.css": "91e60a22",
"js/content-%(version)s.js": "d5d568de",
"js/dashboard-%(version)s.js": "6dab1ac5",
"js/files-%(version)s.js": "d969cdea",
"js/reader-%(version)s.js": "42723026",
"js/setup-%(version)s.js": "10ea5fdc",
"js/ui-%(version)s.js": "7502180a"
}
Loading

0 comments on commit b0d6e77

Please sign in to comment.