mirror of
https://github.com/wassname/flask-security.git
synced 2026-08-07 11:22:21 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e773232e5a | ||
|
|
f401eac496 | ||
|
|
f6b532103d | ||
|
|
27a4bbac6a | ||
|
|
0cf7d563dc | ||
|
|
7c906de427 | ||
|
|
9c03884441 | ||
|
|
4ed4579738 | ||
|
|
156ccaecc1 | ||
|
|
a5482922f3 | ||
|
|
d3cfddfcac | ||
|
|
312a527498 | ||
|
|
d87765fc3b | ||
|
|
d05e699a15 | ||
|
|
e7855488df | ||
|
|
01bdc0d356 | ||
|
|
5e2a016bcd | ||
|
|
0f46f35981 | ||
|
|
c0d9eecf10 | ||
|
|
04bb2c4041 | ||
|
|
8eeb832d2e | ||
|
|
8f760aadbd | ||
|
|
03d27cd600 | ||
|
|
d30a27b3bb | ||
|
|
78903fa2e5 | ||
|
|
514de64303 | ||
|
|
bad63265f8 | ||
|
|
66a9dcd2e6 | ||
|
|
df1647f1f9 | ||
|
|
89ecded480 | ||
|
|
2b35b37a66 | ||
|
|
4d8a813004 | ||
|
|
06312ef50f | ||
|
|
c3ad5b2fa6 | ||
|
|
637fc913cd | ||
|
|
e00522f331 | ||
|
|
3d34d87a97 | ||
|
|
461ace9303 | ||
|
|
d19bb98abd | ||
|
|
c24af5ca6e | ||
|
|
26045fc4dc | ||
|
|
bf260d4b7e |
+2
-2
@@ -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;'
|
||||||
|
|||||||
@@ -3,6 +3,49 @@ 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.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
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Released June 18th 2013
|
||||||
|
|
||||||
|
- Added `SECURITY_DEFAULT_REMEMBER_ME` configuration value to unify behavior between endpoints
|
||||||
|
- Fixed Flask-Login dependency problem
|
||||||
|
- Added optional `next` parameter to registration endpoint, similar to that of login
|
||||||
|
|
||||||
|
|
||||||
Version 1.6.3
|
Version 1.6.3
|
||||||
-------------
|
-------------
|
||||||
@@ -117,10 +160,10 @@ Version 1.2.0
|
|||||||
|
|
||||||
Released March 12th 2012
|
Released March 12th 2012
|
||||||
|
|
||||||
- Added configuration option `SECURITY_FLASH_MESSAGES` which can be set to a
|
- Added configuration option `SECURITY_FLASH_MESSAGES` which can be set to a
|
||||||
boolean value to specify if Flask-Security should flash messages or not.
|
boolean value to specify if Flask-Security should flash messages or not.
|
||||||
|
|
||||||
Version 1.1.0
|
Version 1.1.0
|
||||||
-------------
|
-------------
|
||||||
|
|
||||||
Initial release
|
Initial release
|
||||||
|
|||||||
Vendored
+2
-2
@@ -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
@@ -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.3'
|
version = '1.6.8'
|
||||||
# The full version, including alpha/beta/rc tags.
|
# The full version, including alpha/beta/rc tags.
|
||||||
release = version
|
release = version
|
||||||
|
|
||||||
|
|||||||
+40
-19
@@ -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
|
||||||
-------------
|
-------------
|
||||||
@@ -220,4 +238,7 @@ Miscellaneous
|
|||||||
remember tokens. Remember tokens are
|
remember tokens. Remember tokens are
|
||||||
used instead of user ID's as it is more
|
used instead of user ID's as it is more
|
||||||
secure. Defaults to ``remember-salt``.
|
secure. Defaults to ``remember-salt``.
|
||||||
|
``SECURITY_DEFAULT_REMEMBER_ME`` Specifies the default "remember me"
|
||||||
|
value used when logging in a user.
|
||||||
|
Defaults to ``False``.
|
||||||
======================================= ========================================
|
======================================= ========================================
|
||||||
|
|||||||
+16
-1
@@ -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
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
:license: MIT, see LICENSE for more details.
|
:license: MIT, see LICENSE for more details.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__version__ = '1.6.3'
|
__version__ = '1.6.8'
|
||||||
|
|
||||||
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
|
||||||
|
|||||||
@@ -10,12 +10,11 @@
|
|||||||
: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
|
||||||
from .utils import send_mail, encrypt_password, url_for_security, \
|
from .utils import send_mail, encrypt_password, config_value
|
||||||
config_value
|
|
||||||
|
|
||||||
|
|
||||||
# Convenient references
|
# Convenient references
|
||||||
|
|||||||
@@ -11,11 +11,11 @@
|
|||||||
|
|
||||||
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,\
|
||||||
config_value
|
config_value
|
||||||
from .signals import user_confirmed, confirm_instructions_sent
|
from .signals import user_confirmed, confirm_instructions_sent
|
||||||
|
|
||||||
|
|
||||||
@@ -27,8 +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)
|
||||||
url = url_for_security('confirm_email', token=token)
|
return url_for_security('confirm_email', token=token, _external=True), token
|
||||||
return request.url_root[:-1] + url, token
|
|
||||||
|
|
||||||
|
|
||||||
def send_confirmation_instructions(user):
|
def send_confirmation_instructions(user):
|
||||||
|
|||||||
+14
-14
@@ -10,10 +10,10 @@
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
from flask import current_app
|
from flask import current_app
|
||||||
from flask.ext.login import AnonymousUser as AnonymousUserBase, \
|
from flask.ext.login import AnonymousUserMixin, UserMixin as BaseUserMixin, \
|
||||||
UserMixin as BaseUserMixin, LoginManager, current_user
|
LoginManager, current_user
|
||||||
from flask.ext.principal import Principal, RoleNeed, UserNeed, Identity, \
|
from flask.ext.principal import Principal, RoleNeed, UserNeed, Identity, \
|
||||||
identity_loaded
|
identity_loaded
|
||||||
from itsdangerous import URLSafeTimedSerializer
|
from itsdangerous import URLSafeTimedSerializer
|
||||||
from passlib.context import CryptContext
|
from passlib.context import CryptContext
|
||||||
from werkzeug.datastructures import ImmutableList
|
from werkzeug.datastructures import ImmutableList
|
||||||
@@ -22,8 +22,8 @@ from werkzeug.local import LocalProxy
|
|||||||
from .utils import config_value as cv, get_config, md5, url_for_security
|
from .utils import config_value as cv, get_config, md5, url_for_security
|
||||||
from .views import create_blueprint
|
from .views import create_blueprint
|
||||||
from .forms import LoginForm, ConfirmRegisterForm, RegisterForm, \
|
from .forms import LoginForm, ConfirmRegisterForm, RegisterForm, \
|
||||||
ForgotPasswordForm, ChangePasswordForm, ResetPasswordForm, \
|
ForgotPasswordForm, ChangePasswordForm, ResetPasswordForm, \
|
||||||
SendConfirmationForm, PasswordlessLoginForm
|
SendConfirmationForm, PasswordlessLoginForm
|
||||||
|
|
||||||
# Convenient references
|
# Convenient references
|
||||||
_security = LocalProxy(lambda: current_app.extensions['security'])
|
_security = LocalProxy(lambda: current_app.extensions['security'])
|
||||||
@@ -76,6 +76,7 @@ _default_config = {
|
|||||||
'LOGIN_SALT': 'login-salt',
|
'LOGIN_SALT': 'login-salt',
|
||||||
'CHANGE_SALT': 'change-salt',
|
'CHANGE_SALT': 'change-salt',
|
||||||
'REMEMBER_SALT': 'remember-salt',
|
'REMEMBER_SALT': 'remember-salt',
|
||||||
|
'DEFAULT_REMEMBER_ME': False,
|
||||||
'DEFAULT_HTTP_AUTH_REALM': 'Login Required',
|
'DEFAULT_HTTP_AUTH_REALM': 'Login Required',
|
||||||
'EMAIL_SUBJECT_REGISTER': 'Welcome',
|
'EMAIL_SUBJECT_REGISTER': 'Welcome',
|
||||||
'EMAIL_SUBJECT_CONFIRM': 'Please confirm your email',
|
'EMAIL_SUBJECT_CONFIRM': 'Please confirm your email',
|
||||||
@@ -109,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'),
|
||||||
@@ -153,8 +155,7 @@ def _token_loader(token):
|
|||||||
return user
|
return user
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
return AnonymousUser()
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def _identity_loader():
|
def _identity_loader():
|
||||||
@@ -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):
|
||||||
@@ -272,11 +273,10 @@ class UserMixin(BaseUserMixin):
|
|||||||
return role in self.roles
|
return role in self.roles
|
||||||
|
|
||||||
|
|
||||||
class AnonymousUser(AnonymousUserBase):
|
class AnonymousUser(AnonymousUserMixin):
|
||||||
"""AnonymousUser definition"""
|
"""AnonymousUser definition"""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super(AnonymousUser, self).__init__()
|
|
||||||
self.roles = ImmutableList()
|
self.roles = ImmutableList()
|
||||||
|
|
||||||
def has_role(self, *args):
|
def has_role(self, *args):
|
||||||
@@ -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.
|
||||||
|
|
||||||
|
|||||||
@@ -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,11 @@ 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):
|
||||||
|
return (self.user_model.query.get(id_or_email) or
|
||||||
|
self.user_model.query.filter(
|
||||||
|
self.user_model.email.ilike(id_or_email)).first())
|
||||||
|
|
||||||
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 +199,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 +237,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()
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
+22
-22
@@ -16,7 +16,7 @@ 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, TextField, PasswordField, \
|
||||||
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
|
||||||
|
|
||||||
@@ -69,6 +69,7 @@ class Length(ValidatorMixin, wtf.Length):
|
|||||||
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 +96,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 +219,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 +254,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'))
|
||||||
|
|||||||
@@ -9,12 +9,12 @@
|
|||||||
: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
|
||||||
from .utils import send_mail, url_for_security, get_token_status, \
|
from .utils import send_mail, url_for_security, get_token_status, \
|
||||||
config_value
|
config_value
|
||||||
|
|
||||||
|
|
||||||
# Convenient references
|
# Convenient references
|
||||||
@@ -30,8 +30,7 @@ def send_login_instructions(user):
|
|||||||
:param token: The login token
|
:param token: The login token
|
||||||
"""
|
"""
|
||||||
token = generate_login_token(user)
|
token = generate_login_token(user)
|
||||||
url = url_for_security('token_login', token=token)
|
login_link = url_for_security('token_login', token=token, _external=True)
|
||||||
login_link = request.url_root[:-1] + url
|
|
||||||
|
|
||||||
send_mail(config_value('EMAIL_SUBJECT_PASSWORDLESS'), user.email,
|
send_mail(config_value('EMAIL_SUBJECT_PASSWORDLESS'), user.email,
|
||||||
'login_instructions', user=user, login_link=login_link)
|
'login_instructions', user=user, login_link=login_link)
|
||||||
|
|||||||
@@ -9,12 +9,12 @@
|
|||||||
: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
|
||||||
from .utils import send_mail, md5, encrypt_password, url_for_security, \
|
from .utils import send_mail, md5, encrypt_password, url_for_security, \
|
||||||
get_token_status, config_value
|
get_token_status, config_value
|
||||||
|
|
||||||
|
|
||||||
# Convenient references
|
# Convenient references
|
||||||
@@ -29,8 +29,7 @@ def send_reset_password_instructions(user):
|
|||||||
:param user: The user to send the instructions to
|
:param user: The user to send the instructions to
|
||||||
"""
|
"""
|
||||||
token = generate_reset_password_token(user)
|
token = generate_reset_password_token(user)
|
||||||
url = url_for_security('reset_password', token=token)
|
reset_link = url_for_security('reset_password', token=token, _external=True)
|
||||||
reset_link = request.url_root[:-1] + url
|
|
||||||
|
|
||||||
send_mail(config_value('EMAIL_SUBJECT_PASSWORD_RESET'), user.email,
|
send_mail(config_value('EMAIL_SUBJECT_PASSWORD_RESET'), user.email,
|
||||||
'reset_instructions',
|
'reset_instructions',
|
||||||
@@ -68,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
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ from werkzeug.local import LocalProxy
|
|||||||
from .confirmable import generate_confirmation_link
|
from .confirmable import generate_confirmation_link
|
||||||
from .signals import user_registered
|
from .signals import user_registered
|
||||||
from .utils import do_flash, get_message, send_mail, encrypt_password, \
|
from .utils import do_flash, get_message, send_mail, encrypt_password, \
|
||||||
config_value
|
config_value
|
||||||
|
|
||||||
# Convenient references
|
# Convenient references
|
||||||
_security = LocalProxy(lambda: app.extensions['security'])
|
_security = LocalProxy(lambda: app.extensions['security'])
|
||||||
@@ -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
|
||||||
|
|||||||
+19
-9
@@ -14,12 +14,13 @@ import blinker
|
|||||||
import functools
|
import functools
|
||||||
import hashlib
|
import hashlib
|
||||||
import hmac
|
import hmac
|
||||||
|
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
from flask import url_for, flash, current_app, request, session, render_template
|
from flask import url_for, flash, current_app, request, session, render_template
|
||||||
from flask.ext.login import login_user as _login_user, \
|
from flask.ext.login import login_user as _login_user, \
|
||||||
logout_user as _logout_user
|
logout_user as _logout_user
|
||||||
from flask.ext.mail import Message
|
from flask.ext.mail import Message
|
||||||
from flask.ext.principal import Identity, AnonymousIdentity, identity_changed
|
from flask.ext.principal import Identity, AnonymousIdentity, identity_changed
|
||||||
from itsdangerous import BadSignature, SignatureExpired
|
from itsdangerous import BadSignature, SignatureExpired
|
||||||
@@ -37,9 +38,12 @@ _datastore = LocalProxy(lambda: _security.datastore)
|
|||||||
_pwd_context = LocalProxy(lambda: _security.pwd_context)
|
_pwd_context = LocalProxy(lambda: _security.pwd_context)
|
||||||
|
|
||||||
|
|
||||||
def login_user(user, remember=True):
|
def login_user(user, remember=None):
|
||||||
"""Performs the login and sends the appropriate signal."""
|
"""Performs the login and sends the appropriate signal."""
|
||||||
|
|
||||||
|
if remember is None:
|
||||||
|
remember = config_value('DEFAULT_REMEMBER_ME')
|
||||||
|
|
||||||
if not _login_user(user, remember):
|
if not _login_user(user, remember):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -74,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)
|
||||||
@@ -144,11 +149,18 @@ def url_for_security(endpoint, **values):
|
|||||||
return url_for(endpoint, **values)
|
return url_for(endpoint, **values)
|
||||||
|
|
||||||
|
|
||||||
def get_post_login_redirect():
|
def get_post_action_redirect(config_key):
|
||||||
"""Returns the URL to redirect to after a user logs in successfully."""
|
|
||||||
return (get_url(request.args.get('next')) or
|
return (get_url(request.args.get('next')) or
|
||||||
get_url(request.form.get('next')) or
|
get_url(request.form.get('next')) or
|
||||||
find_redirect('SECURITY_POST_LOGIN_VIEW'))
|
find_redirect(config_key))
|
||||||
|
|
||||||
|
|
||||||
|
def get_post_login_redirect():
|
||||||
|
return get_post_action_redirect('SECURITY_POST_LOGIN_VIEW')
|
||||||
|
|
||||||
|
|
||||||
|
def get_post_register_redirect():
|
||||||
|
return get_post_action_redirect('SECURITY_POST_REGISTER_VIEW')
|
||||||
|
|
||||||
|
|
||||||
def find_redirect(key):
|
def find_redirect(key):
|
||||||
@@ -375,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])
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+14
-15
@@ -10,24 +10,23 @@
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
from flask import current_app, redirect, request, render_template, jsonify, \
|
from flask import current_app, redirect, request, render_template, jsonify, \
|
||||||
after_this_request, Blueprint
|
after_this_request, Blueprint
|
||||||
from flask_login import current_user
|
from flask_login import current_user
|
||||||
from werkzeug.datastructures import MultiDict
|
from werkzeug.datastructures import MultiDict
|
||||||
from werkzeug.local import LocalProxy
|
from werkzeug.local import LocalProxy
|
||||||
|
|
||||||
from .confirmable import send_confirmation_instructions, \
|
from .confirmable import send_confirmation_instructions, \
|
||||||
confirm_user, confirm_email_token_status
|
confirm_user, confirm_email_token_status
|
||||||
from .decorators import login_required, anonymous_user_required
|
from .decorators import login_required, anonymous_user_required
|
||||||
from .passwordless import send_login_instructions, \
|
from .passwordless import send_login_instructions, \
|
||||||
login_token_status
|
login_token_status
|
||||||
from .recoverable import reset_password_token_status, \
|
from .recoverable import reset_password_token_status, \
|
||||||
send_reset_password_instructions, update_password
|
send_reset_password_instructions, update_password
|
||||||
from .changeable import change_user_password
|
from .changeable import change_user_password
|
||||||
from .registerable import register_user
|
from .registerable import register_user
|
||||||
from .utils import get_url, get_post_login_redirect, do_flash, \
|
from .utils import config_value, do_flash, get_url, get_post_login_redirect, \
|
||||||
get_message, login_user, logout_user, url_for_security as url_for, \
|
get_post_register_redirect, get_message, login_user, logout_user, \
|
||||||
config_value
|
url_for_security as url_for
|
||||||
|
|
||||||
|
|
||||||
# Convenient references
|
# Convenient references
|
||||||
_security = LocalProxy(lambda: current_app.extensions['security'])
|
_security = LocalProxy(lambda: current_app.extensions['security'])
|
||||||
@@ -123,9 +122,7 @@ def register():
|
|||||||
login_user(user)
|
login_user(user)
|
||||||
|
|
||||||
if not request.json:
|
if not request.json:
|
||||||
post_register_url = get_url(_security.post_register_view)
|
return redirect(get_post_register_redirect())
|
||||||
post_login_url = get_url(_security.post_login_view)
|
|
||||||
return redirect(post_register_url or post_login_url)
|
|
||||||
|
|
||||||
if request.json:
|
if request.json:
|
||||||
return _render_json(form)
|
return _render_json(form)
|
||||||
@@ -173,7 +170,7 @@ def token_login(token):
|
|||||||
if invalid or expired:
|
if invalid or expired:
|
||||||
return redirect(url_for('login'))
|
return redirect(url_for('login'))
|
||||||
|
|
||||||
login_user(user, True)
|
login_user(user)
|
||||||
after_this_request(_commit)
|
after_this_request(_commit)
|
||||||
do_flash(*get_message('PASSWORDLESS_LOGIN_SUCCESSFUL'))
|
do_flash(*get_message('PASSWORDLESS_LOGIN_SUCCESSFUL'))
|
||||||
|
|
||||||
@@ -203,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."""
|
||||||
|
|
||||||
@@ -220,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, True)
|
|
||||||
after_this_request(_commit)
|
after_this_request(_commit)
|
||||||
do_flash(*get_message('EMAIL_CONFIRMED'))
|
do_flash(*get_message('EMAIL_CONFIRMED'))
|
||||||
|
|
||||||
@@ -272,7 +271,7 @@ def reset_password(token):
|
|||||||
after_this_request(_commit)
|
after_this_request(_commit)
|
||||||
update_password(user, form.password.data)
|
update_password(user, form.password.data)
|
||||||
do_flash(*get_message('PASSWORD_RESET'))
|
do_flash(*get_message('PASSWORD_RESET'))
|
||||||
login_user(user, True)
|
login_user(user)
|
||||||
return redirect(get_url(_security.post_reset_view) or
|
return redirect(get_url(_security.post_reset_view) or
|
||||||
get_url(_security.post_login_view))
|
get_url(_security.post_login_view))
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ from setuptools import setup
|
|||||||
|
|
||||||
setup(
|
setup(
|
||||||
name='Flask-Security',
|
name='Flask-Security',
|
||||||
version='1.6.3',
|
version='1.6.8',
|
||||||
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,7 +35,7 @@ setup(
|
|||||||
platforms='any',
|
platforms='any',
|
||||||
install_requires=[
|
install_requires=[
|
||||||
'Flask>=0.9',
|
'Flask>=0.9',
|
||||||
'Flask-Login>=0.1.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',
|
||||||
|
|||||||
+7
-6
@@ -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)
|
||||||
|
|||||||
+49
-27
@@ -68,6 +68,14 @@ class ConfiguredSecurityTests(SecurityTest):
|
|||||||
r = self._post('/register', data=data, follow_redirects=True)
|
r = self._post('/register', data=data, follow_redirects=True)
|
||||||
self.assertIn('Post Register', r.data)
|
self.assertIn('Post Register', r.data)
|
||||||
|
|
||||||
|
def test_register_with_next_querystring_argument(self):
|
||||||
|
data = dict(email='dude@lp.com',
|
||||||
|
password='password',
|
||||||
|
password_confirm='password')
|
||||||
|
|
||||||
|
r = self._post('/register?next=/page1', data=data, follow_redirects=True)
|
||||||
|
self.assertIn('Page 1', r.data)
|
||||||
|
|
||||||
def test_register_json(self):
|
def test_register_json(self):
|
||||||
data = '{ "email": "dude@lp.com", "password": "password", "csrf_token":"%s" }' % self.csrf_token
|
data = '{ "email": "dude@lp.com", "password": "password", "csrf_token":"%s" }' % self.csrf_token
|
||||||
r = self._post('/register', data=data, content_type='application/json')
|
r = self._post('/register', data=data, content_type='application/json')
|
||||||
@@ -114,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')
|
||||||
|
|
||||||
@@ -147,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']
|
||||||
|
|
||||||
@@ -328,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):
|
||||||
|
|
||||||
@@ -339,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)
|
||||||
@@ -354,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={
|
||||||
@@ -451,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)
|
||||||
@@ -478,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)
|
||||||
|
|
||||||
@@ -766,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)
|
||||||
|
|
||||||
|
|||||||
@@ -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,8 +63,9 @@ 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="message">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)
|
||||||
|
|
||||||
def test_authorized_access(self):
|
def test_authorized_access(self):
|
||||||
self.authenticate()
|
self.authenticate()
|
||||||
|
|||||||
+12
-5
@@ -15,7 +15,14 @@ def compare_user(a, b):
|
|||||||
return a.id == b.id and a.email == b.email and a.password == b.password
|
return a.id == b.id and a.email == b.email and a.password == b.password
|
||||||
|
|
||||||
|
|
||||||
class RegisterableSignalsTests(SecurityTest):
|
class SignalTest(SecurityTest):
|
||||||
|
|
||||||
|
def _create_app(self, auth_config, **kwargs):
|
||||||
|
from tests.test_app.mongoengine import create_app
|
||||||
|
return create_app(auth_config, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class RegisterableSignalsTests(SignalTest):
|
||||||
|
|
||||||
AUTH_CONFIG = {
|
AUTH_CONFIG = {
|
||||||
'SECURITY_CONFIRMABLE': True,
|
'SECURITY_CONFIRMABLE': True,
|
||||||
@@ -42,7 +49,7 @@ class RegisterableSignalsTests(SecurityTest):
|
|||||||
self.assertEqual(mocks.signals_sent(), set())
|
self.assertEqual(mocks.signals_sent(), set())
|
||||||
|
|
||||||
|
|
||||||
class ConfirmableSignalsTests(SecurityTest):
|
class ConfirmableSignalsTests(SignalTest):
|
||||||
|
|
||||||
AUTH_CONFIG = {
|
AUTH_CONFIG = {
|
||||||
'SECURITY_CONFIRMABLE': True,
|
'SECURITY_CONFIRMABLE': True,
|
||||||
@@ -103,7 +110,7 @@ class ConfirmableSignalsTests(SecurityTest):
|
|||||||
self.assertEqual(mocks.signals_sent(), set())
|
self.assertEqual(mocks.signals_sent(), set())
|
||||||
|
|
||||||
|
|
||||||
class RecoverableSignalsTests(SecurityTest):
|
class RecoverableSignalsTests(SignalTest):
|
||||||
|
|
||||||
AUTH_CONFIG = {
|
AUTH_CONFIG = {
|
||||||
'SECURITY_RECOVERABLE': True,
|
'SECURITY_RECOVERABLE': True,
|
||||||
@@ -153,7 +160,7 @@ class RecoverableSignalsTests(SecurityTest):
|
|||||||
self.assertEqual(mocks.signals_sent(), set())
|
self.assertEqual(mocks.signals_sent(), set())
|
||||||
|
|
||||||
|
|
||||||
class ChangeableSignalsTests(SecurityTest):
|
class ChangeableSignalsTests(SignalTest):
|
||||||
|
|
||||||
AUTH_CONFIG = {
|
AUTH_CONFIG = {
|
||||||
'SECURITY_CHANGEABLE': True,
|
'SECURITY_CHANGEABLE': True,
|
||||||
@@ -204,7 +211,7 @@ class ChangeableSignalsTests(SecurityTest):
|
|||||||
self.assertEqual(mocks.signals_sent(), set())
|
self.assertEqual(mocks.signals_sent(), set())
|
||||||
|
|
||||||
|
|
||||||
class PasswordlessTests(SecurityTest):
|
class PasswordlessTests(SignalTest):
|
||||||
|
|
||||||
AUTH_CONFIG = {
|
AUTH_CONFIG = {
|
||||||
'SECURITY_PASSWORDLESS': True
|
'SECURITY_PASSWORDLESS': True
|
||||||
|
|||||||
@@ -113,6 +113,10 @@ def create_app(config):
|
|||||||
def invalid_role():
|
def invalid_role():
|
||||||
return 'success' if ds.find_role('bogus') is None else 'failure'
|
return 'success' if ds.find_role('bogus') is None else 'failure'
|
||||||
|
|
||||||
|
@app.route('/page1')
|
||||||
|
def page_1():
|
||||||
|
return 'Page 1'
|
||||||
|
|
||||||
return app
|
return app
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user