Compare commits

...
26 Commits
Author SHA1 Message Date
Matt Wright 07bb00f75c Bump version number to 1.6.9 2013-08-20 12:35:29 -04:00
Matt Wright 9a10be5c90 Merge branch 'develop' 2013-08-20 12:35:12 -04:00
Matt Wright 829bb135d7 Update CHANGES 2013-08-20 12:35:08 -04:00
Matt Wright ab64406b6c Merge pull request #152 from AleshGood/develop
Fix imports from Flask-WTF
2013-08-20 09:31:32 -07:00
Alexey Poryadin 6f68f1d540 Fix imports from Flask-WTF 2013-08-15 12:46:09 +04:00
Matt Wright 510c7bfa2a Merge pull request #151 from lupiter/fix-peewee-delete-role
Commit deleting role from user (peewee).
2013-08-12 06:58:47 -07:00
Matt Wright 67d8134e3a Merge pull request #150 from vmalloc/fix-147
#147: only get by email_or_id if it is numeric
2013-08-12 06:58:35 -07:00
Wise, Catherine (ICT Centre, Acton) 7bef96c261 Commit deleting role from user (peewee). 2013-08-12 15:07:13 +10:00
Rotem YaariandRotem Yaari 7d6b42e960 #147: only get by email_or_id if it is numeric 2013-08-11 20:15:06 +03:00
Matt Wright e773232e5a Merge branch 'develop' 2013-08-01 16:28:18 -04:00
Matt Wright f401eac496 Merge branch 'develop' of github.com:mattupstate/flask-security into develop 2013-08-01 16:28:11 -04:00
Matt Wright f6b532103d Bump version number to 1.6.8 2013-08-01 16:27:32 -04:00
Matt Wright 27a4bbac6a Update CHANGES 2013-08-01 16:27:25 -04:00
Matt Wright 0cf7d563dc Update changes 2013-08-01 16:24:46 -04:00
Matt Wright 7c906de427 When logging in user lookups on email should be case insensitive 2013-08-01 16:22:28 -04:00
Matt Wright 9c03884441 Merge pull request #140 from joeyespo/ignore-validation-errors
Ignore validation errors in find_user
2013-07-25 14:28:01 -07:00
Joe Esposito 4ed4579738 Ignore validation errors in find_user 2013-07-24 16:08:19 -04:00
Matt Wright 156ccaecc1 Attempting a fix to get tests to pass with travis. Very odd... 2013-07-24 11:35:04 -04:00
Matt Wright a5482922f3 Merge pull request #139 from codebutler/develop
Use token_callback for checking tokens.
2013-07-24 07:01:29 -07:00
Eric Butler d3cfddfcac Use token_callback for checking tokens.
Fixes error if user is not found.
2013-07-23 15:37:28 -07:00
Matt Wright 312a527498 PEP8 and general clean up on tests 2013-07-22 12:42:20 -04:00
Matt Wright d87765fc3b PEP8 polish 2013-07-22 12:37:44 -04:00
Matt Wright d05e699a15 Clean up 2013-07-22 12:19:29 -04:00
Matt Wright e7855488df Docs polish 2013-07-22 11:44:19 -04:00
Matt Wright 01bdc0d356 A little more documentation. Fixes #131 2013-07-22 11:35:45 -04:00
Matt Wright 5e2a016bcd Update docs. Fixes #137 2013-07-22 11:26:11 -04:00
20 changed files with 195 additions and 101 deletions
+20
View File
@@ -3,6 +3,26 @@ Flask-Security Changelog
Here you can see the full list of changes between each Flask-Security release. Here you can see the full list of changes between each Flask-Security release.
Version 1.6.9
-------------
Released August 20th 2013
- Fix bug in SQLAlchemy datastore's `get_user` function
- Fix bug in PeeWee datastore's `remove_role_from_user` function
- Fixed import error caused by new Flask-WTF release
Version 1.6.8
-------------
Released August 1st 2013
- Fixed bug with case sensitivity of email address during login
- Code cleanup regarding token_callback
- Ignore validation errors in find_user function for MongoEngineUserDatastore
Version 1.6.7 Version 1.6.7
------------- -------------
+1 -1
View File
@@ -49,7 +49,7 @@ copyright = u'2012, Matt Wright'
# built documents. # built documents.
# #
# The short X.Y version. # The short X.Y version.
version = '1.6.7' version = '1.6.9'
# The full version, including alpha/beta/rc tags. # The full version, including alpha/beta/rc tags.
release = version release = version
+33 -19
View File
@@ -56,6 +56,8 @@ URLs and Views
``/register``. ``/register``.
``SECURITY_RESET_URL`` Specifies the password reset URL. Defaults to ``SECURITY_RESET_URL`` Specifies the password reset URL. Defaults to
``/reset``. ``/reset``.
``SECURITY_CHANGE_URL`` Specifies the password change URL. Defaults to
``/change``.
``SECURITY_CONFIRM_URL`` Specifies the email confirmation URL. Defaults ``SECURITY_CONFIRM_URL`` Specifies the email confirmation URL. Defaults
to ``/confirm``. to ``/confirm``.
``SECURITY_POST_LOGIN_VIEW`` Specifies the default view to redirect to after ``SECURITY_POST_LOGIN_VIEW`` Specifies the default view to redirect to after
@@ -86,8 +88,14 @@ URLs and Views
successfully resets their password. This value successfully resets their password. This value
can be set to a URL or an endpoint name. If this can be set to a URL or an endpoint name. If this
value is ``None`` the user is redirected to the value is ``None`` the user is redirected to the
value of ``SECURITY_POST_LOGIN_VIEW``. Defaults to value of ``SECURITY_POST_LOGIN_VIEW``. Defaults
``None``. to ``None``.
``SECURITY_POST_CHANGE_VIEW`` Specifies the view to redirect to after a user
successfully changes their password. This value
can be set to a URL or an endpoint name. If this
value is ``None`` the user is redirected to the
value of ``SECURITY_POST_LOGIN_VIEW``. Defaults
to ``None``.
``SECURITY_UNAUTHORIZED_VIEW`` Specifies the view to redirect to if a user ``SECURITY_UNAUTHORIZED_VIEW`` Specifies the view to redirect to if a user
attempts to access a URL/endpoint that they do attempts to access a URL/endpoint that they do
not have permission to access. If this value is not have permission to access. If this value is
@@ -167,23 +175,29 @@ Email
.. tabularcolumns:: |p{6.5cm}|p{8.5cm}| .. tabularcolumns:: |p{6.5cm}|p{8.5cm}|
=========================================== ==================================== ================================================= ==============================
``SECURITY_EMAIL_SUBJECT_REGISTER`` Sets the subject for the ``SECURITY_EMAIL_SUBJECT_REGISTER`` Sets the subject for the
confirmation email. Defaults to confirmation email. Defaults
``Welcome`` to ``Welcome``
``SECURITY_EMAIL_SUBJECT_PASSWORDLESS`` Sets the subject for the ``SECURITY_EMAIL_SUBJECT_PASSWORDLESS`` Sets the subject for the
passwordless feature. Defaults to passwordless feature. Defaults
``Login instructions`` to ``Login instructions``
``SECURITY_EMAIL_SUBJECT_PASSWORD_NOTICE`` Sets subject for the password ``SECURITY_EMAIL_SUBJECT_PASSWORD_NOTICE`` Sets subject for the password
notice. Defaults to notice. Defaults to ``Your
``Your password has been reset`` password has been reset``
``SECURITY_EMAIL_SUBJECT_PASSWORD_RESET`` Sets the subject for the password ``SECURITY_EMAIL_SUBJECT_PASSWORD_RESET`` Sets the subject for the
reset. Defaults to password reset email. Defaults
``Password reset instructions`` to ``Password reset
``SECURITY_EMAIL_SUBJECT_CONFIRM`` Sets the subject for the email instructions``
confirmation message. Defaults to ``SECURITY_EMAIL_SUBJECT_PASSWORD_CHANGE_NOTICE`` Sets the subject for the
``Please confirm your email`` password change notice.
=========================================== ==================================== Defaults to ``Your password
has been changed``
``SECURITY_EMAIL_SUBJECT_CONFIRM`` Sets the subject for the email
confirmation message. Defaults
to ``Please confirm your
email``
================================================= ==============================
Miscellaneous Miscellaneous
------------- -------------
+16 -1
View File
@@ -34,7 +34,8 @@ Password encryption is enabled with `passlib`_. Passwords are stored in plain
text by default but you can easily configure the encryption algorithm. You text by default but you can easily configure the encryption algorithm. You
should **always use an encryption algorithm** in your production environment. should **always use an encryption algorithm** in your production environment.
You may also specify to use HMAC with a configured salt value in addition to the You may also specify to use HMAC with a configured salt value in addition to the
algorithm chosen. Bear in mind passlib does not assume which algorithm you will choose and may require additional libraries to be installed. algorithm chosen. Bear in mind passlib does not assume which algorithm you will
choose and may require additional libraries to be installed.
Basic HTTP Authentication Basic HTTP Authentication
@@ -104,6 +105,20 @@ statistics. They include:
* Total login count * Total login count
JSON/Ajax Support
-----------------
Flask-Security supports JSON/Ajax requests where appropriate. Just remember that
all endpoints require a CSRF token just like HTML views. More specifically
JSON is supported for the following operations:
* Login requests
* Registration requests
* Change password requests
* Confirmation requests
* Forgot password requests
* Passwordless login requests
.. _Flask-Login: http://packages.python.org/Flask-Login/ .. _Flask-Login: http://packages.python.org/Flask-Login/
.. _alternative token: http://packages.python.org/Flask-Login/#alternative-tokens .. _alternative token: http://packages.python.org/Flask-Login/#alternative-tokens
+1
View File
@@ -13,6 +13,7 @@ Flask application. They include:
7. Token based password recovery / resetting (optional) 7. Token based password recovery / resetting (optional)
8. User registration (optional) 8. User registration (optional)
9. Login tracking (optional) 9. Login tracking (optional)
10. JSON/Ajax Support
Many of these features are made possible by integrating various Flask extensions Many of these features are made possible by integrating various Flask extensions
and libraries. They include: and libraries. They include:
+1 -1
View File
@@ -10,7 +10,7 @@
:license: MIT, see LICENSE for more details. :license: MIT, see LICENSE for more details.
""" """
__version__ = '1.6.7' __version__ = '1.6.9'
from .core import Security, RoleMixin, UserMixin, AnonymousUser, current_user from .core import Security, RoleMixin, UserMixin, AnonymousUser, current_user
from .datastore import SQLAlchemyUserDatastore, MongoEngineUserDatastore, PeeweeUserDatastore from .datastore import SQLAlchemyUserDatastore, MongoEngineUserDatastore, PeeweeUserDatastore
+1 -1
View File
@@ -10,7 +10,7 @@
:license: MIT, see LICENSE for more details. :license: MIT, see LICENSE for more details.
""" """
from flask import current_app as app, request from flask import current_app as app
from werkzeug.local import LocalProxy from werkzeug.local import LocalProxy
from .signals import password_changed from .signals import password_changed
+1 -1
View File
@@ -11,7 +11,7 @@
from datetime import datetime from datetime import datetime
from flask import current_app as app, request from flask import current_app as app
from werkzeug.local import LocalProxy from werkzeug.local import LocalProxy
from .utils import send_mail, md5, url_for_security, get_token_status,\ from .utils import send_mail, md5, url_for_security, get_token_status,\
+5 -5
View File
@@ -244,7 +244,7 @@ def _context_processor():
class RoleMixin(object): class RoleMixin(object):
"""Mixin for `Role` model definitions""" """Mixin for `Role` model definitions"""
def __eq__(self, other): def __eq__(self, other):
return (self.name == other or \ return (self.name == other or
self.name == getattr(other, 'name', None)) self.name == getattr(other, 'name', None))
def __ne__(self, other): def __ne__(self, other):
@@ -346,10 +346,10 @@ class Security(object):
self._state = self.init_app(app, datastore, **kwargs) self._state = self.init_app(app, datastore, **kwargs)
def init_app(self, app, datastore=None, register_blueprint=True, def init_app(self, app, datastore=None, register_blueprint=True,
login_form=None, confirm_register_form=None, login_form=None, confirm_register_form=None,
register_form=None, forgot_password_form=None, register_form=None, forgot_password_form=None,
reset_password_form=None, change_password_form=None, reset_password_form=None, change_password_form=None,
send_confirmation_form=None, passwordless_login_form=None): send_confirmation_form=None, passwordless_login_form=None):
"""Initializes the Flask-Security extension for the specified """Initializes the Flask-Security extension for the specified
application and datastore implentation. application and datastore implentation.
+46 -2
View File
@@ -9,6 +9,7 @@
:license: MIT, see LICENSE for more details. :license: MIT, see LICENSE for more details.
""" """
class Datastore(object): class Datastore(object):
def __init__(self, db): def __init__(self, db):
self.db = db self.db = db
@@ -81,6 +82,10 @@ class UserDatastore(object):
kwargs['roles'] = roles kwargs['roles'] = roles
return kwargs return kwargs
def get_user(self, id_or_email):
"""Returns a user matching the specified ID or email address"""
raise NotImplementedError
def find_user(self, *args, **kwargs): def find_user(self, *args, **kwargs):
"""Returns a user matching the provided parameters.""" """Returns a user matching the provided parameters."""
raise NotImplementedError raise NotImplementedError
@@ -174,6 +179,22 @@ class SQLAlchemyUserDatastore(SQLAlchemyDatastore, UserDatastore):
SQLAlchemyDatastore.__init__(self, db) SQLAlchemyDatastore.__init__(self, db)
UserDatastore.__init__(self, user_model, role_model) UserDatastore.__init__(self, user_model, role_model)
def get_user(self, id_or_email):
returned = None
if self._is_numeric(id_or_email):
returned = self.user_model.query.get(id_or_email)
if not returned:
returned = self.user_model.query.filter(
self.user_model.email.ilike(id_or_email)).first()
return returned
def _is_numeric(self, value):
try:
int(value)
except ValueError:
return False
return True
def find_user(self, **kwargs): def find_user(self, **kwargs):
return self.user_model.query.filter_by(**kwargs).first() return self.user_model.query.filter_by(**kwargs).first()
@@ -189,15 +210,26 @@ class MongoEngineUserDatastore(MongoEngineDatastore, UserDatastore):
MongoEngineDatastore.__init__(self, db) MongoEngineDatastore.__init__(self, db)
UserDatastore.__init__(self, user_model, role_model) UserDatastore.__init__(self, user_model, role_model)
def get_user(self, id_or_email):
from mongoengine import ValidationError
try:
return self.user_model.objects(id=id_or_email).first()
except ValidationError:
return self.user_model.objects(email__iexact=id_or_email).first()
def find_user(self, **kwargs): def find_user(self, **kwargs):
try: try:
from mongoengine.queryset import Q, QCombination from mongoengine.queryset import Q, QCombination
except ImportError: except ImportError:
from mongoengine.queryset.visitor import Q, QCombination from mongoengine.queryset.visitor import Q, QCombination
from mongoengine.errors import ValidationError
queries = map(lambda i: Q(**{i[0]: i[1]}), kwargs.items()) queries = map(lambda i: Q(**{i[0]: i[1]}), kwargs.items())
query = QCombination(QCombination.AND, queries) query = QCombination(QCombination.AND, queries)
return self.user_model.objects(query).first() try:
return self.user_model.objects(query).first()
except ValidationError:
return None
def find_role(self, role): def find_role(self, role):
return self.role_model.objects(name=role).first() return self.role_model.objects(name=role).first()
@@ -216,6 +248,17 @@ class PeeweeUserDatastore(PeeweeDatastore, UserDatastore):
UserDatastore.__init__(self, user_model, role_model) UserDatastore.__init__(self, user_model, role_model)
self.UserRole = role_link self.UserRole = role_link
def get_user(self, id_or_email):
try:
return self.user_model.get(self.user_model.id == id_or_email)
except ValueError:
pass
try:
return self.user_model.get(self.user_model.email ** id_or_email)
except self.user_model.DoesNotExist:
pass
return None
def find_user(self, **kwargs): def find_user(self, **kwargs):
try: try:
return self.user_model.filter(**kwargs).get() return self.user_model.filter(**kwargs).get()
@@ -259,8 +302,9 @@ class PeeweeUserDatastore(PeeweeDatastore, UserDatastore):
result = self.UserRole.select() \ result = self.UserRole.select() \
.where(self.UserRole.user == user, self.UserRole.role == role) .where(self.UserRole.user == user, self.UserRole.role == role)
if result.count(): if result.count():
self.UserRole.delete().where( query = self.UserRole.delete().where(
self.UserRole.user == user, self.UserRole.role == role) self.UserRole.user == user, self.UserRole.role == role)
query.execute()
return True return True
else: else:
return False return False
+4 -8
View File
@@ -53,21 +53,17 @@ def _check_token():
token = request.args.get(args_key, header_token) token = request.args.get(args_key, header_token)
if request.json: if request.json:
token = request.json.get(args_key, token) token = request.json.get(args_key, token)
serializer = _security.remember_token_serializer
try: user = _security.login_manager.token_callback(token)
data = serializer.loads(token)
except:
return False
user = _security.datastore.find_user(id=data[0]) if user and user.is_authenticated():
if utils.md5(user.password) == data[1]:
app = current_app._get_current_object() app = current_app._get_current_object()
_request_ctx_stack.top.user = user _request_ctx_stack.top.user = user
identity_changed.send(app, identity=Identity(user.id)) identity_changed.send(app, identity=Identity(user.id))
return True return True
return False
def _check_http_auth(): def _check_http_auth():
auth = request.authorization or BasicAuth(username=None, password=None) auth = request.authorization or BasicAuth(username=None, password=None)
+24 -22
View File
@@ -15,7 +15,8 @@ import urlparse
import flask_wtf as wtf import flask_wtf as wtf
from flask import request, current_app from flask import request, current_app
from flask_wtf import Form as BaseForm, TextField, PasswordField, \ from flask_wtf import Form as BaseForm
from wtforms import TextField, PasswordField, validators, \
SubmitField, HiddenField, BooleanField, ValidationError, Field SubmitField, HiddenField, BooleanField, ValidationError, Field
from flask_login import current_user from flask_login import current_user
from werkzeug.local import LocalProxy from werkzeug.local import LocalProxy
@@ -50,19 +51,19 @@ class ValidatorMixin(object):
return super(ValidatorMixin, self).__call__(form, field) return super(ValidatorMixin, self).__call__(form, field)
class EqualTo(ValidatorMixin, wtf.EqualTo): class EqualTo(ValidatorMixin, validators.EqualTo):
pass pass
class Required(ValidatorMixin, wtf.Required): class Required(ValidatorMixin, validators.Required):
pass pass
class Email(ValidatorMixin, wtf.Email): class Email(ValidatorMixin, validators.Email):
pass pass
class Length(ValidatorMixin, wtf.Length): class Length(ValidatorMixin, validators.Length):
pass pass
@@ -96,33 +97,32 @@ class Form(BaseForm):
class EmailFormMixin(): class EmailFormMixin():
email = TextField(get_form_field_label('email'), email = TextField(
validators=[email_required, get_form_field_label('email'),
email_validator]) validators=[email_required, email_validator])
class UserEmailFormMixin(): class UserEmailFormMixin():
user = None user = None
email = TextField(get_form_field_label('email'), email = TextField(
validators=[email_required, get_form_field_label('email'),
email_validator, validators=[email_required, email_validator, valid_user_email])
valid_user_email])
class UniqueEmailFormMixin(): class UniqueEmailFormMixin():
email = TextField(get_form_field_label('email'), email = TextField(
validators=[email_required, get_form_field_label('email'),
email_validator, validators=[email_required, email_validator, unique_user_email])
unique_user_email])
class PasswordFormMixin(): class PasswordFormMixin():
password = PasswordField(get_form_field_label('password'), password = PasswordField(
validators=[password_required]) get_form_field_label('password'), validators=[password_required])
class NewPasswordFormMixin(): class NewPasswordFormMixin():
password = PasswordField(get_form_field_label('password'), password = PasswordField(
get_form_field_label('password'),
validators=[password_required, password_length]) validators=[password_required, password_length])
@@ -220,7 +220,7 @@ class LoginForm(Form, NextFormMixin):
self.password.errors.append(get_message('PASSWORD_NOT_PROVIDED')[0]) self.password.errors.append(get_message('PASSWORD_NOT_PROVIDED')[0])
return False return False
self.user = _datastore.find_user(email=self.email.data) self.user = _datastore.get_user(self.email.data)
if self.user is None: if self.user is None:
self.email.errors.append(get_message('USER_DOES_NOT_EXIST')[0]) self.email.errors.append(get_message('USER_DOES_NOT_EXIST')[0])
@@ -255,10 +255,12 @@ class ResetPasswordForm(Form, NewPasswordFormMixin, PasswordConfirmFormMixin):
class ChangePasswordForm(Form, PasswordFormMixin): class ChangePasswordForm(Form, PasswordFormMixin):
"""The default change password form""" """The default change password form"""
new_password = PasswordField(get_form_field_label('new_password'), new_password = PasswordField(
get_form_field_label('new_password'),
validators=[password_required, password_length]) validators=[password_required, password_length])
new_password_confirm = PasswordField(get_form_field_label('retype_password'), new_password_confirm = PasswordField(
get_form_field_label('retype_password'),
validators=[EqualTo('new_password', message='RETYPE_PASSWORD_MISMATCH')]) validators=[EqualTo('new_password', message='RETYPE_PASSWORD_MISMATCH')])
submit = SubmitField(get_form_field_label('change_password')) submit = SubmitField(get_form_field_label('change_password'))
+1 -1
View File
@@ -9,7 +9,7 @@
:license: MIT, see LICENSE for more details. :license: MIT, see LICENSE for more details.
""" """
from flask import request, current_app as app from flask import current_app as app
from werkzeug.local import LocalProxy from werkzeug.local import LocalProxy
from .signals import login_instructions_sent from .signals import login_instructions_sent
+2 -1
View File
@@ -9,7 +9,7 @@
:license: MIT, see LICENSE for more details. :license: MIT, see LICENSE for more details.
""" """
from flask import current_app as app, request from flask import current_app as app
from werkzeug.local import LocalProxy from werkzeug.local import LocalProxy
from .signals import password_reset, reset_password_instructions_sent from .signals import password_reset, reset_password_instructions_sent
@@ -67,6 +67,7 @@ def reset_password_token_status(token):
""" """
return get_token_status(token, 'reset', 'RESET_PASSWORD') return get_token_status(token, 'reset', 'RESET_PASSWORD')
def update_password(user, password): def update_password(user, password):
"""Update the specified user's password """Update the specified user's password
+1 -1
View File
@@ -38,6 +38,6 @@ def register_user(**kwargs):
if config_value('SEND_REGISTER_EMAIL'): if config_value('SEND_REGISTER_EMAIL'):
send_mail(config_value('EMAIL_SUBJECT_REGISTER'), user.email, 'welcome', send_mail(config_value('EMAIL_SUBJECT_REGISTER'), user.email, 'welcome',
user=user, confirmation_link=confirmation_link) user=user, confirmation_link=confirmation_link)
return user return user
+3 -4
View File
@@ -78,8 +78,9 @@ def get_hmac(password):
return password return password
if _security.password_salt is None: if _security.password_salt is None:
raise RuntimeError('The configuration value `SECURITY_PASSWORD_SALT` ' raise RuntimeError(
'must not be None when the value of `SECURITY_PASSWORD_HASH` is ' 'The configuration value `SECURITY_PASSWORD_SALT` must '
'not be None when the value of `SECURITY_PASSWORD_HASH` is '
'set to "%s"' % _security.password_hash) 'set to "%s"' % _security.password_hash)
h = hmac.new(_security.password_salt, password.encode('utf-8'), hashlib.sha512) h = hmac.new(_security.password_salt, password.encode('utf-8'), hashlib.sha512)
@@ -386,5 +387,3 @@ def capture_signals():
confirm_instructions_sent, login_instructions_sent, confirm_instructions_sent, login_instructions_sent,
password_reset, password_changed, password_reset, password_changed,
reset_password_instructions_sent]) reset_password_instructions_sent])
+1 -1
View File
@@ -20,7 +20,7 @@ from setuptools import setup
setup( setup(
name='Flask-Security', name='Flask-Security',
version='1.6.7', version='1.6.9',
url='https://github.com/mattupstate/flask-security', url='https://github.com/mattupstate/flask-security',
license='MIT', license='MIT',
author='Matt Wright', author='Matt Wright',
+7 -6
View File
@@ -30,7 +30,8 @@ class SecurityTest(TestCase):
session['csrf'] = 'csrf_token' session['csrf'] = 'csrf_token'
csrf_hmac = hmac.new(self.app.config['SECRET_KEY'], csrf_hmac = hmac.new(self.app.config['SECRET_KEY'],
'csrf_token'.encode('utf8'), digestmod=sha1) 'csrf_token'.encode('utf8'),
digestmod=sha1)
self.csrf_token = '##' + csrf_hmac.hexdigest() self.csrf_token = '##' + csrf_hmac.hexdigest()
def _create_app(self, auth_config, **kwargs): def _create_app(self, auth_config, **kwargs):
@@ -38,17 +39,17 @@ class SecurityTest(TestCase):
def _get(self, route, content_type=None, follow_redirects=None, headers=None): def _get(self, route, content_type=None, follow_redirects=None, headers=None):
return self.client.get(route, follow_redirects=follow_redirects, return self.client.get(route, follow_redirects=follow_redirects,
content_type=content_type or 'text/html', content_type=content_type or 'text/html',
headers=headers) headers=headers)
def _post(self, route, data=None, content_type=None, follow_redirects=True, headers=None): def _post(self, route, data=None, content_type=None, follow_redirects=True, headers=None):
if isinstance(data, dict): if isinstance(data, dict):
data['csrf_token'] = self.csrf_token data['csrf_token'] = self.csrf_token
content_type = content_type or 'application/x-www-form-urlencoded'
return self.client.post(route, data=data, return self.client.post(route, data=data,
follow_redirects=follow_redirects, follow_redirects=follow_redirects,
content_type=content_type or 'application/x-www-form-urlencoded', content_type=content_type, headers=headers)
headers=headers)
def register(self, email, password='password'): def register(self, email, password='password'):
data = dict(email=email, password=password, csrf_token=self.csrf_token) data = dict(email=email, password=password, csrf_token=self.csrf_token)
+22 -26
View File
@@ -122,7 +122,6 @@ class DefaultTemplatePathTests(SecurityTest):
'SECURITY_LOGIN_USER_TEMPLATE': 'custom_security/login_user.html', 'SECURITY_LOGIN_USER_TEMPLATE': 'custom_security/login_user.html',
} }
def test_login_user_template(self): def test_login_user_template(self):
r = self._get('/login') r = self._get('/login')
@@ -155,9 +154,8 @@ class RecoverableTemplatePathTests(SecurityTest):
def test_reset_password_template(self): def test_reset_password_template(self):
with capture_reset_password_requests() as requests: with capture_reset_password_requests() as requests:
r = self._post('/reset', r = self._post('/reset', data=dict(email='joe@lp.com'),
data=dict(email='joe@lp.com'), follow_redirects=True)
follow_redirects=True)
t = requests[0]['token'] t = requests[0]['token']
@@ -365,9 +363,8 @@ class RecoverableTests(SecurityTest):
def test_reset_view(self): def test_reset_view(self):
with capture_reset_password_requests() as requests: with capture_reset_password_requests() as requests:
r = self._post('/reset', r = self._post('/reset', data=dict(email='joe@lp.com'),
data=dict(email='joe@lp.com'), follow_redirects=True)
follow_redirects=True)
t = requests[0]['token'] t = requests[0]['token']
r = self._get('/reset/' + t) r = self._get('/reset/' + t)
self.assertIn('<h1>Reset password</h1>', r.data) self.assertIn('<h1>Reset password</h1>', r.data)
@@ -380,20 +377,18 @@ class RecoverableTests(SecurityTest):
def test_forgot_password_json(self): def test_forgot_password_json(self):
r = self._post('/reset', data='{"email": "matt@lp.com"}', r = self._post('/reset', data='{"email": "matt@lp.com"}',
content_type="application/json") content_type="application/json")
self.assertEquals(r.status_code, 200) self.assertEquals(r.status_code, 200)
def test_forgot_password_invalid_email(self): def test_forgot_password_invalid_email(self):
r = self._post('/reset', r = self._post('/reset', data=dict(email='larry@lp.com'),
data=dict(email='larry@lp.com'), follow_redirects=True)
follow_redirects=True)
self.assertIn("Specified user does not exist", r.data) self.assertIn("Specified user does not exist", r.data)
def test_reset_password_with_valid_token(self): def test_reset_password_with_valid_token(self):
with capture_reset_password_requests() as requests: with capture_reset_password_requests() as requests:
r = self._post('/reset', r = self._post('/reset', data=dict(email='joe@lp.com'),
data=dict(email='joe@lp.com'), follow_redirects=True)
follow_redirects=True)
t = requests[0]['token'] t = requests[0]['token']
r = self._post('/reset/' + t, data={ r = self._post('/reset/' + t, data={
@@ -480,13 +475,15 @@ class ChangePasswordTest(SecurityTest):
self.assertIn('Password must be at least 6 characters', r.data) self.assertIn('Password must be at least 6 characters', r.data)
def test_change_password_success(self): def test_change_password_success(self):
data = {
'password': 'password',
'new_password': 'newpassword',
'new_password_confirm': 'newpassword'
}
self.authenticate() self.authenticate()
with self.app.extensions['mail'].record_messages() as outbox: with self.app.extensions['mail'].record_messages() as outbox:
r = self._post('/change', data={ r = self._post('/change', data=data, follow_redirects=True)
'password': 'password',
'new_password': 'newpassword',
'new_password_confirm': 'newpassword'
}, follow_redirects=True)
self.assertIn('You successfully changed your password', r.data) self.assertIn('You successfully changed your password', r.data)
self.assertIn('Home Page', r.data) self.assertIn('Home Page', r.data)
@@ -504,12 +501,13 @@ class ChangePasswordPostViewTest(SecurityTest):
} }
def test_change_password_success(self): def test_change_password_success(self):
data = {
'password': 'password',
'new_password': 'newpassword',
'new_password_confirm': 'newpassword'
}
self.authenticate() self.authenticate()
r = self._post('/change', data={ r = self._post('/change', data=data, follow_redirects=True)
'password': 'password',
'new_password': 'newpassword',
'new_password_confirm': 'newpassword'
}, follow_redirects=True)
self.assertIn('Profile Page', r.data) self.assertIn('Profile Page', r.data)
@@ -792,8 +790,6 @@ class ConfirmableExtendFormsTest(SecurityTest):
r = self._get('/register', follow_redirects=True) r = self._get('/register', follow_redirects=True)
self.assertIn("My Confirm Register Email Address Field", r.data) self.assertIn("My Confirm Register Email Address Field", r.data)
def test_send_confirmation(self): def test_send_confirmation(self):
r = self._get('/confirm', follow_redirects=True) r = self._get('/confirm', follow_redirects=True)
self.assertIn("My Send Confirmation Email Address Field", r.data) self.assertIn("My Send Confirmation Email Address Field", r.data)
+5
View File
@@ -33,6 +33,10 @@ class DefaultSecurityTests(SecurityTest):
r = self.authenticate() r = self.authenticate()
self.assertIn('Hello matt@lp.com', r.data) self.assertIn('Hello matt@lp.com', r.data)
def test_authenticate_case_insensitive_email(self):
r = self.authenticate(email='MATT@lp.com')
self.assertIn('Hello matt@lp.com', r.data)
def test_unprovided_username(self): def test_unprovided_username(self):
r = self.authenticate("") r = self.authenticate("")
self.assertIn(self.get_message('EMAIL_NOT_PROVIDED'), r.data) self.assertIn(self.get_message('EMAIL_NOT_PROVIDED'), r.data)
@@ -59,6 +63,7 @@ class DefaultSecurityTests(SecurityTest):
self.assertIsHomePage(r.data) self.assertIsHomePage(r.data)
def test_unauthorized_access(self): def test_unauthorized_access(self):
self.logout()
r = self._get('/profile', follow_redirects=True) r = self._get('/profile', follow_redirects=True)
self.assertIn('<li class="info">Please log in to access this page.</li>', r.data) self.assertIn('<li class="info">Please log in to access this page.</li>', r.data)