Compare commits

...
45 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
Matt Wright 0f46f35981 Bump version number to 1.6.7 2013-07-11 14:58:46 -04:00
Matt Wright c0d9eecf10 Set release date. 2013-07-11 14:58:36 -04:00
Matt Wright 04bb2c4041 Update CHANGES 2013-07-11 14:54:22 -04:00
Matt Wright 8eeb832d2e Conditionally logout the current user when confirming an email address to prevent unnecessary code/signals to be fired. Fixes #133 2013-07-11 14:50:21 -04:00
Matt Wright 8f760aadbd Merge pull request #134 from rxl/email_confirmation_fix
Email confirmation fix
2013-07-11 11:21:05 -07:00
rxl 03d27cd600 add logout_user() to the beginning of confirm_email() 2013-07-11 14:14:50 -04:00
rxl d30a27b3bb remove '@anonymous_user_required' from confirm_email() 2013-07-11 14:14:28 -04:00
Matt Wright 78903fa2e5 Make password length message configurable. 2013-07-02 10:36:22 -04:00
Matt Wright 514de64303 Bump version number to 1.6.6 2013-06-28 17:24:50 -04:00
Matt Wright bad63265f8 Update CHANGES 2013-06-28 17:24:40 -04:00
Matt Wright 66a9dcd2e6 Merge branch 'develop' of github.com:mattupstate/flask-security into develop 2013-06-28 17:23:36 -04:00
Matt Wright df1647f1f9 Fix install_requires in setup.py 2013-06-28 17:23:16 -04:00
Matt Wright 89ecded480 Merge pull request #128 from vkotovv/develop
Fix issue tracker link
2013-06-26 08:04:10 -07:00
Vadim Kotov 2b35b37a66 Update sidebarintro.html
Fixed link to the issue tracker
2013-06-26 18:30:44 +04:00
Matt Wright 4d8a813004 Bump version number to 1.6.5 2013-06-20 16:01:36 -04:00
Matt Wright 06312ef50f Fix typo with _external parameter in confirmable.py. Fixes #126 2013-06-20 16:01:22 -04:00
Matt Wright c3ad5b2fa6 Take --use-mirrors out of pip calls in .travis.yml 2013-06-18 15:53:32 -04:00
Matt Wright 637fc913cd Take Flask-Mail out of .travis.yml 2013-06-18 15:32:32 -04:00
Matt Wright e00522f331 Add SECURITY_CHANGEABLE to docs. Fixes #115 2013-06-18 15:05:32 -04:00
23 changed files with 259 additions and 117 deletions
+2 -2
View File
@@ -6,9 +6,9 @@ python:
- "pypy" - "pypy"
install: install:
- pip install . --quiet --use-mirrors - pip install . --quiet
- "if [[ $TRAVIS_PYTHON_VERSION == '2.6' ]]; then pip install importlib --quiet --use-mirrors; fi" - "if [[ $TRAVIS_PYTHON_VERSION == '2.6' ]]; then pip install importlib --quiet --use-mirrors; fi"
- pip install nose simplejson Flask-SQLAlchemy Flask-MongoEngine Flask-Peewee Flask-Mail py-bcrypt MySQL-python --quiet --use-mirrors - pip install nose simplejson Flask-SQLAlchemy Flask-MongoEngine Flask-Peewee py-bcrypt MySQL-python --quiet
before_script: before_script:
- mysql -e 'create database flask_security_test;' - mysql -e 'create database flask_security_test;'
+44
View File
@@ -3,6 +3,50 @@ 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
-------------
Released July 11th 2013
- Made password length form error message configurable
- Fixed email confirmation bug that prevented logged in users from confirming their email
Version 1.6.6
-------------
Released June 28th 2013
- Fixed dependency versions
Version 1.6.5
-------------
Released June 20th 2013
- Fixed bug in `flask.ext.security.confirmable.generate_confirmation_link`
Version 1.6.4 Version 1.6.4
------------- -------------
+2 -2
View File
@@ -9,9 +9,9 @@
<ul> <ul>
<li><a href="http://pypi.python.org/pypi/Flask-Security">Flask-Security @ PyPI</a></li> <li><a href="http://pypi.python.org/pypi/Flask-Security">Flask-Security @ PyPI</a></li>
<li><a href="http://github.com/mattupstate/flask-security">Flask-Security @ github</a></li> <li><a href="http://github.com/mattupstate/flask-security">Flask-Security @ github</a></li>
<li><a href="http://github.com/jfinkels/flask-security/issues">Issue Tracker</a></li> <li><a href="http://github.com/mattupstate/flask-security/issues">Issue Tracker</a></li>
</ul> </ul>
<ul> <ul>
<li><a href="http://pypi.python.org/pypi/Flask-Social">Flask-Social</a></li> <li><a href="http://pypi.python.org/pypi/Flask-Social">Flask-Social</a></li>
<li><a href="http://github.com/mattupstate/flask-social">Flask-Social @ github</a></li> <li><a href="http://github.com/mattupstate/flask-social">Flask-Social @ github</a></li>
</ul> </ul>
+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.4' version = '1.6.9'
# The full version, including alpha/beta/rc tags. # The full version, including alpha/beta/rc tags.
release = version release = version
+37 -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
@@ -156,6 +164,10 @@ Feature Flags
sent an email with a login link. This feature is sent an email with a login link. This feature is
experimental and should be used with caution. Defaults experimental and should be used with caution. Defaults
to ``False``. to ``False``.
``SECURITY_CHANGEABLE`` Specifies if Flask-Security should enable the
change password endpoint. The URL for this endpoint is
specified by the ``SECURITY_CHANGE_URL`` configuration
option. Defaults to ``False``.
========================= ====================================================== ========================= ======================================================
Email Email
@@ -163,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.4' __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
+2 -2
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,\
@@ -27,7 +27,7 @@ _datastore = LocalProxy(lambda: _security.datastore)
def generate_confirmation_link(user): def generate_confirmation_link(user):
token = generate_confirmation_token(user) token = generate_confirmation_token(user)
return url_for_security('confirm_email', token=token, _extenal=True), token return url_for_security('confirm_email', token=token, _external=True), token
def send_confirmation_instructions(user): def send_confirmation_instructions(user):
+6 -5
View File
@@ -110,6 +110,7 @@ _default_messages = {
'EMAIL_NOT_PROVIDED': ('Email not provided', 'error'), 'EMAIL_NOT_PROVIDED': ('Email not provided', 'error'),
'INVALID_EMAIL_ADDRESS': ('Invalid email address', 'error'), 'INVALID_EMAIL_ADDRESS': ('Invalid email address', 'error'),
'PASSWORD_NOT_PROVIDED': ('Password not provided', 'error'), 'PASSWORD_NOT_PROVIDED': ('Password not provided', 'error'),
'PASSWORD_INVALID_LENGTH': ('Password must be at least 6 characters', 'error'),
'USER_DOES_NOT_EXIST': ('Specified user does not exist', 'error'), 'USER_DOES_NOT_EXIST': ('Specified user does not exist', 'error'),
'INVALID_PASSWORD': ('Invalid password', 'error'), 'INVALID_PASSWORD': ('Invalid password', 'error'),
'PASSWORDLESS_LOGIN_SUCCESSFUL': ('You have successfuly logged in.', 'success'), 'PASSWORDLESS_LOGIN_SUCCESSFUL': ('You have successfuly logged in.', 'success'),
@@ -243,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):
@@ -345,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)
+27 -26
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,25 +51,26 @@ 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
email_required = Required(message='EMAIL_NOT_PROVIDED') email_required = Required(message='EMAIL_NOT_PROVIDED')
email_validator = Email(message='INVALID_EMAIL_ADDRESS') email_validator = Email(message='INVALID_EMAIL_ADDRESS')
password_required = Required(message='PASSWORD_NOT_PROVIDED') password_required = Required(message='PASSWORD_NOT_PROVIDED')
password_length = Length(min=6, max=128, message='PASSWORD_INVALID_LENGTH')
def get_form_field_label(key): def get_form_field_label(key):
@@ -95,35 +97,33 @@ 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(
validators=[password_required, get_form_field_label('password'),
Length(min=6, max=128)]) validators=[password_required, password_length])
class PasswordConfirmFormMixin(): class PasswordConfirmFormMixin():
@@ -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,11 +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(
validators=[password_required, get_form_field_label('new_password'),
Length(min=6, max=128)]) 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])
+4 -2
View File
@@ -200,7 +200,6 @@ def send_confirmation():
**_ctx('send_confirmation')) **_ctx('send_confirmation'))
@anonymous_user_required
def confirm_email(token): def confirm_email(token):
"""View function which handles a email confirmation request.""" """View function which handles a email confirmation request."""
@@ -217,8 +216,11 @@ def confirm_email(token):
return redirect(get_url(_security.confirm_error_view) or return redirect(get_url(_security.confirm_error_view) or
url_for('send_confirmation')) url_for('send_confirmation'))
if user != current_user:
logout_user()
login_user(user)
confirm_user(user) confirm_user(user)
login_user(user)
after_this_request(_commit) after_this_request(_commit)
do_flash(*get_message('EMAIL_CONFIRMED')) do_flash(*get_message('EMAIL_CONFIRMED'))
+5 -5
View File
@@ -20,7 +20,7 @@ from setuptools import setup
setup( setup(
name='Flask-Security', name='Flask-Security',
version='1.6.4', 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',
@@ -35,10 +35,10 @@ setup(
platforms='any', platforms='any',
install_requires=[ install_requires=[
'Flask>=0.9', 'Flask>=0.9',
'Flask-Login==0.2.3', 'Flask-Login>=0.2.3',
'Flask-Mail==0.7.3', 'Flask-Mail>=0.7.3',
'Flask-Principal==0.3.3', 'Flask-Principal>=0.3.3',
'Flask-WTF==0.8', 'Flask-WTF>=0.8',
'itsdangerous>=0.17', 'itsdangerous>=0.17',
'passlib>=1.6.1', 'passlib>=1.6.1',
], ],
+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)
+41 -27
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']
@@ -336,6 +334,24 @@ class LoginWithoutImmediateConfirmTests(SecurityTest):
r = self._post('/register', data=data, follow_redirects=True) r = self._post('/register', data=data, follow_redirects=True)
self.assertIn(e, r.data) self.assertIn(e, r.data)
def test_confirm_email_of_user_different_than_current_user(self):
e1 = 'dude@lp.com'
e2 = 'lady@lp.com'
with capture_registrations() as registrations:
self.register(e1)
self.register(e2)
token1 = registrations[0]['confirm_token']
token2 = registrations[1]['confirm_token']
self.client.get('/confirm/' + token1, follow_redirects=True)
self.client.get('/logout')
self.authenticate(email=e1)
r = self.client.get('/confirm/' + token2, follow_redirects=True)
msg = self.app.config['SECURITY_MSG_EMAIL_CONFIRMED'][0]
self.assertIn(msg, r.data)
self.assertIn('Hello %s' % e2, r.data)
class RecoverableTests(SecurityTest): class RecoverableTests(SecurityTest):
@@ -347,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)
@@ -362,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={
@@ -459,16 +472,18 @@ class ChangePasswordTest(SecurityTest):
'new_password_confirm': 'a' 'new_password_confirm': 'a'
}, follow_redirects=True) }, follow_redirects=True)
self.assertNotIn('You successfully changed your password', r.data) self.assertNotIn('You successfully changed your password', r.data)
self.assertIn('Field must be between', 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)
@@ -486,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)
@@ -774,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)