client+server: finish password reminders

This commit is contained in:
rr-
2016-04-06 17:56:34 +02:00
parent 1fb2f53914
commit 92dd958866
14 changed files with 246 additions and 56 deletions

View File

@ -1,4 +1,4 @@
''' Falcon-compatible API facades. '''
from szurubooru.api.password_reminder_api import PasswordReminderApi
from szurubooru.api.password_reset_api import PasswordResetApi
from szurubooru.api.user_api import UserListApi, UserDetailApi

View File

@ -8,18 +8,18 @@ MAIL_BODY = \
'If you wish to proceed, click this link: {url}\n' \
'Otherwise, please ignore this email.'
class PasswordReminderApi(BaseApi):
class PasswordResetApi(BaseApi):
def get(self, context, user_name):
''' Send a mail with secure token to the correlated user. '''
user = users.get_by_name(context.session, user_name)
user = users.get_by_name_or_email(context.session, user_name)
if not user:
raise errors.NotFoundError('User %r not found.' % user_name)
if not user.email:
raise errors.ValidationError(
'User %r hasn\'t supplied email. Cannot reset password.' % user_name)
token = auth.generate_authentication_token(user)
url = '%s/password-reset/%s' % (
config.config['basic']['base_url'].rstrip('/'), token)
url = '%s/password-reset/%s:%s' % (
config.config['basic']['base_url'].rstrip('/'), user.name, token)
mailer.send_mail(
'noreply@%s' % config.config['basic']['name'],
user.email,
@ -29,7 +29,7 @@ class PasswordReminderApi(BaseApi):
def post(self, context, user_name):
''' Verify token from mail, generate a new password and return it. '''
user = users.get_by_name(context.session, user_name)
user = users.get_by_name_or_email(context.session, user_name)
if not user:
raise errors.NotFoundError('User %r not found.' % user_name)
good_token = auth.generate_authentication_token(user)

View File

@ -1,4 +1,3 @@
import sqlalchemy
from szurubooru import errors, search
from szurubooru.util import auth, users
from szurubooru.api.base_api import BaseApi

View File

@ -64,7 +64,7 @@ def create_app():
user_list_api = api.UserListApi()
user_detail_api = api.UserDetailApi()
password_reminder_api = api.PasswordReminderApi()
password_reset_api = api.PasswordResetApi()
app.add_error_handler(errors.AuthError, _on_auth_error)
app.add_error_handler(errors.IntegrityError, _on_integrity_error)
@ -74,6 +74,6 @@ def create_app():
app.add_route('/users/', user_list_api)
app.add_route('/user/{user_name}', user_detail_api)
app.add_route('/password_reminder/{user_name}', password_reminder_api)
app.add_route('/password-reset/{user_name}', password_reset_api)
return app

View File

@ -0,0 +1,77 @@
from datetime import datetime
from unittest import mock
from szurubooru import api, db, errors, config
from szurubooru.util import auth, misc, mailer
from szurubooru.tests.database_test_case import DatabaseTestCase
from szurubooru.tests.api import util
class TestPasswordReset(DatabaseTestCase):
def setUp(self):
super().setUp()
config_mock = {
'basic': {
'secret': 'x',
'base_url': 'http://example.com/',
'name': 'Test instance',
},
}
self.old_config = config.config
config.config = config_mock
self.context = misc.dotdict()
self.context.session = self.session
self.context.request = {}
self.context.user = db.User()
self.api = api.PasswordResetApi()
def test_reset_non_existing(self):
self.assertRaises(errors.NotFoundError, self.api.get, self.context, 'u1')
def test_reset_without_email(self):
user = util.mock_user('u1', 'regular_user')
user.email = None
self.session.add(user)
self.assertRaises(errors.ValidationError, self.api.get, self.context, 'u1')
def test_reset_sending_email(self):
user = util.mock_user('u1', 'regular_user')
user.email = 'user@example.com'
self.session.add(user)
for getter in ['u1', 'user@example.com']:
with mock.MagicMock() as mock_method:
mailer.send_mail = mock_method
self.assertEqual({}, self.api.get(self.context, getter))
mock_method.assert_called_once_with(
'noreply@Test instance',
'user@example.com',
'Password reset for Test instance',
'You (or someone else) requested to reset your password ' +
'on Test instance.\nIf you wish to proceed, click this l' +
'ink: http://example.com/password-reset/u1:4ac0be176fb36' +
'4f13ee6b634c43220e2\nOtherwise, please ignore this email.')
def test_confirmation_non_existing(self):
self.assertRaises(errors.NotFoundError, self.api.post, self.context, 'u1')
def test_confirmation_no_token(self):
user = util.mock_user('u1', 'regular_user')
user.email = 'user@example.com'
self.session.add(user)
self.context.request = {}
self.assertRaises(errors.ValidationError, self.api.post, self.context, 'u1')
def test_confirmation_bad_token(self):
user = util.mock_user('u1', 'regular_user')
user.email = 'user@example.com'
self.session.add(user)
self.context.request = {'token': 'bad'}
self.assertRaises(errors.ValidationError, self.api.post, self.context, 'u1')
def test_confirmation_good_token(self):
user = util.mock_user('u1', 'regular_user')
user.email = 'user@example.com'
old_hash = user.password_hash
self.session.add(user)
self.context.request = {'token': '4ac0be176fb364f13ee6b634c43220e2'}
result = self.api.post(self.context, 'u1')
self.assertNotEqual(user.password_hash, old_hash)
self.assertTrue(auth.is_valid_password(user, result['password']))

View File

@ -2,30 +2,7 @@ from datetime import datetime
from szurubooru import api, db, errors, config
from szurubooru.util import auth, misc
from szurubooru.tests.database_test_case import DatabaseTestCase
def _create_user(name, rank='admin'):
user = db.User()
user.name = name
user.password = 'dummy'
user.password_salt = 'dummy'
user.password_hash = 'dummy'
user.email = 'dummy'
user.access_rank = rank
user.creation_time = datetime(1997, 1, 1)
user.avatar_style = db.User.AVATAR_GRAVATAR
return user
def _mock_params(context, params):
def get_param_as_string(key, default=None):
if key not in params:
return default
return params[key]
def get_param_as_int(key, default=None):
if key not in params:
return default
return int(params[key])
context.get_param_as_string = get_param_as_string
context.get_param_as_int = get_param_as_int
from szurubooru.tests.api import util
class TestRetrievingUsers(DatabaseTestCase):
def setUp(self):
@ -48,10 +25,10 @@ class TestRetrievingUsers(DatabaseTestCase):
self.context.user = db.User()
def test_retrieving_multiple(self):
user1 = _create_user('u1', 'mod')
user2 = _create_user('u2', 'mod')
user1 = util.mock_user('u1', 'mod')
user2 = util.mock_user('u2', 'mod')
self.session.add_all([user1, user2])
_mock_params(self.context, {'query': '', 'page': 1})
util.mock_params(self.context, {'query': '', 'page': 1})
self.context.user.access_rank = 'regular_user'
api_ = api.UserListApi()
result = api_.get(self.context)
@ -63,15 +40,15 @@ class TestRetrievingUsers(DatabaseTestCase):
def test_retrieving_multiple_without_privileges(self):
self.context.user.access_rank = 'anonymous'
_mock_params(self.context, {'query': '', 'page': 1})
util.mock_params(self.context, {'query': '', 'page': 1})
api_ = api.UserListApi()
self.assertRaises(errors.AuthError, api_.get, self.context)
def test_retrieving_single(self):
user = _create_user('u1', 'regular_user')
user = util.mock_user('u1', 'regular_user')
self.session.add(user)
self.context.user.access_rank = 'regular_user'
_mock_params(self.context, {'query': '', 'page': 1})
util.mock_params(self.context, {'query': '', 'page': 1})
api_ = api.UserDetailApi()
result = api_.get(self.context, 'u1')
self.assertEqual(result['user']['id'], user.user_id)
@ -83,13 +60,13 @@ class TestRetrievingUsers(DatabaseTestCase):
def test_retrieving_non_existing(self):
self.context.user.access_rank = 'regular_user'
_mock_params(self.context, {'query': '', 'page': 1})
util.mock_params(self.context, {'query': '', 'page': 1})
api_ = api.UserDetailApi()
self.assertRaises(errors.NotFoundError, api_.get, self.context, '-')
def test_retrieving_single_without_privileges(self):
self.context.user.access_rank = 'anonymous'
_mock_params(self.context, {'query': '', 'page': 1})
util.mock_params(self.context, {'query': '', 'page': 1})
api_ = api.UserDetailApi()
self.assertRaises(errors.AuthError, api_.get, self.context, '-')
@ -200,7 +177,7 @@ class TestUpdatingUser(DatabaseTestCase):
config.config = self.old_config
def test_update_changing_nothing(self):
admin_user = _create_user('u1', 'admin')
admin_user = util.mock_user('u1', 'admin')
self.session.add(admin_user)
self.context.user = admin_user
self.api.put(self.context, 'u1')
@ -210,13 +187,13 @@ class TestUpdatingUser(DatabaseTestCase):
self.assertEqual(admin_user.access_rank, 'admin')
def test_updating_non_existing_user(self):
admin_user = _create_user('u1', 'admin')
admin_user = util.mock_user('u1', 'admin')
self.session.add(admin_user)
self.context.user = admin_user
self.assertRaises(errors.NotFoundError, self.api.put, self.context, 'u2')
def test_admin_updating_everything_for_themselves(self):
admin_user = _create_user('u1', 'admin')
admin_user = util.mock_user('u1', 'admin')
self.session.add(admin_user)
self.context.user = admin_user
self.context.request = {
@ -234,7 +211,7 @@ class TestUpdatingUser(DatabaseTestCase):
self.assertFalse(auth.is_valid_password(admin_user, 'invalid'))
def test_removing_email(self):
admin_user = _create_user('u1', 'admin')
admin_user = util.mock_user('u1', 'admin')
self.session.add(admin_user)
self.context.user = admin_user
self.context.request = {'email': ''}
@ -243,7 +220,7 @@ class TestUpdatingUser(DatabaseTestCase):
self.assertEqual(admin_user.email, None)
def test_invalid_inputs(self):
admin_user = _create_user('u1', 'admin')
admin_user = util.mock_user('u1', 'admin')
self.session.add(admin_user)
self.context.user = admin_user
self.context.request = {'name': '.'}
@ -260,8 +237,8 @@ class TestUpdatingUser(DatabaseTestCase):
errors.ValidationError, self.api.put, self.context, 'u1')
def test_user_trying_to_update_someone_else(self):
user1 = _create_user('u1', 'regular_user')
user2 = _create_user('u2', 'regular_user')
user1 = util.mock_user('u1', 'regular_user')
user2 = util.mock_user('u2', 'regular_user')
self.session.add_all([user1, user2])
self.context.user = user1
for request in [
@ -274,26 +251,28 @@ class TestUpdatingUser(DatabaseTestCase):
errors.AuthError, self.api.put, self.context, user2.name)
def test_user_trying_to_become_someone_else(self):
user1 = _create_user('me', 'regular_user')
user2 = _create_user('her', 'regular_user')
user1 = util.mock_user('me', 'regular_user')
user2 = util.mock_user('her', 'regular_user')
self.session.add_all([user1, user2])
self.context.user = user1
self.context.request = {'name': 'her'}
self.assertRaises(
errors.IntegrityError, self.api.put, self.context, 'me')
self.session.rollback()
def test_user_trying_to_become_someone_else_insensitive(self):
user1 = _create_user('me', 'regular_user')
user2 = _create_user('her', 'regular_user')
user1 = util.mock_user('me', 'regular_user')
user2 = util.mock_user('her', 'regular_user')
self.session.add_all([user1, user2])
self.context.user = user1
self.context.request = {'name': 'HER'}
self.assertRaises(
errors.IntegrityError, self.api.put, self.context, 'me')
self.session.rollback()
def test_mods_trying_to_become_admin(self):
user1 = _create_user('u1', 'mod')
user2 = _create_user('u2', 'mod')
user1 = util.mock_user('u1', 'mod')
user2 = util.mock_user('u2', 'mod')
self.session.add_all([user1, user2])
self.context.user = user1
self.context.request = {'accessRank': 'admin'}

View File

@ -0,0 +1,26 @@
from datetime import datetime
from szurubooru import db
def mock_user(name, rank='admin'):
user = db.User()
user.name = name
user.password = 'dummy'
user.password_salt = 'dummy'
user.password_hash = 'dummy'
user.email = 'dummy'
user.access_rank = rank
user.creation_time = datetime(1997, 1, 1)
user.avatar_style = db.User.AVATAR_GRAVATAR
return user
def mock_params(context, params):
def get_param_as_string(key, default=None):
if key not in params:
return default
return params[key]
def get_param_as_int(key, default=None):
if key not in params:
return default
return int(params[key])
context.get_param_as_string = get_param_as_string
context.get_param_as_int = get_param_as_int

View File

@ -53,7 +53,7 @@ def verify_privilege(user, privilege_name):
def generate_authentication_token(user):
''' Generate nonguessable challenge (e.g. links in password reminder). '''
digest = hashlib.sha256()
digest = hashlib.md5()
digest.update(config.config['basic']['secret'].encode('utf8'))
digest.update(user.password_salt.encode('utf8'))
return digest.hexdigest()

View File

@ -68,3 +68,11 @@ def get_by_name(session, name):
return session.query(db.User) \
.filter(func.lower(db.User.name) == func.lower(name)) \
.first()
def get_by_name_or_email(session, name_or_email):
''' Retrieve an user by its name or email. '''
return session.query(db.User) \
.filter(
(func.lower(db.User.name) == func.lower(name_or_email))
| (func.lower(db.User.email) == func.lower(name_or_email))) \
.first()