Compare commits

...
75 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
Matt Wright 3d34d87a97 Bump version number to 1.6.4 2013-06-18 15:00:41 -04:00
Matt Wright 461ace9303 Update docs 2013-06-18 15:00:34 -04:00
Matt Wright d19bb98abd Version 1.6.4 changes. Refer to CHANGES for updates. Fixes #123 #121 #120 $119 2013-06-18 14:56:12 -04:00
Matt Wright c24af5ca6e Whitespace! 2013-05-28 11:11:37 -04:00
Matt Wright 26045fc4dc Use the _external parameter when generating links for emails 2013-05-28 11:11:19 -04:00
Matt Wright bf260d4b7e Add optional next parameter to registration endpoint. Fixes #117. 2013-05-28 11:01:42 -04:00
Matt Wright db56ff74a9 Bump version number to 1.6.3 2013-05-08 12:29:48 -04:00
Matt Wright e03efe0b34 Update CHANGES 2013-05-08 12:29:35 -04:00
Matt Wright c587988a3a Merge branch 'develop' of github.com:mattupstate/flask-security into develop 2013-05-03 12:14:14 -04:00
Matt Wright f2d5245bd8 Import check to account for new version of MongoEngine 2013-05-03 12:13:58 -04:00
Matt Wright 97e1960abd Merge pull request #112 from poundifdef/login_flash
Make flask-login respect SECURITY_FLASH_MESSAGES
2013-04-14 13:40:57 -07:00
Jay Goel e749b77ca7 Make flask-login respect SECURITY_FLASH_MESSAGES 2013-04-14 16:37:23 -04:00
Matt Wright 6f3c163ee7 Merge pull request #111 from joshpurvis/issue110
Changed has_role to accept strings with mongoengine. Fixes #110
2013-04-14 10:05:34 -07:00
Josh Purvis 3b81ec57ea Changed has_role to accept strings with mongoengine. Fixes #110 2013-04-13 15:11:56 -04:00
Matt Wright 38874433c7 Add tests for Peewee support 2013-04-04 18:09:55 -04:00
Matt Wright 4eda3e756f PEP8 polish 2013-04-04 16:39:50 -04:00
Matt Wright 4815b1afed Make find_user method for MongoEngineUserDatastore add contraints to query 2013-04-04 15:50:46 -04:00
Matt Wright 99ac732d10 Bump version number to 1.6.2 2013-04-04 10:24:03 -04:00
Matt Wright e8b0c62818 Update CHANGES and a little polish 2013-04-04 10:23:51 -04:00
Matt Wright 1108f1670c Merge pull request #104 from rodcloutier/http_auth_fix
Fixed http_auth when authorization is not provided in header
2013-04-04 07:21:27 -07:00
Rodrigue Cloutier 3575a2df18 Fixed http_auth when authorization is not provided in header 2013-04-03 21:29:04 -04:00
Matt Wright c84c485493 Bump version number to 1.6.1 2013-04-03 11:07:36 -04:00
Matt Wright 8298ac461e Update CHANGES 2013-04-03 11:07:16 -04:00
Matt Wright 105d04768e Merge pull request #103 from immon/issue94
sending signals fixed
2013-04-03 08:04:49 -07:00
Paweł Krześniak f1cca43d9c sending signals fixed 2013-04-03 12:36:53 +02:00
Matt Wright e8352fa265 Merge pull request #102 from andrewcamenga/develop
corrected link for Flask-WTF
2013-03-29 08:53:38 -07:00
Andrew J. Camenga 37d84ddd73 corrected link for Flask-WTF 2013-03-29 08:37:51 -04:00
Matt Wright 6f9869e9c2 import auth_required into top level package 2013-03-27 17:20:31 -04:00
Matt Wright abc061ba46 Change .travis.yml 2013-03-19 14:11:23 -04:00
Matt Wright 95c80e5677 See if pypy works 2013-03-19 13:28:42 -04:00
26 changed files with 443 additions and 216 deletions
+4 -3
View File
@@ -3,11 +3,12 @@ language: python
python: python:
- "2.6" - "2.6"
- "2.7" - "2.7"
- "pypy"
install: install:
- pip install . --quiet --use-mirrors - pip install . --quiet
- "if [[ $TRAVIS_PYTHON_VERSION != '2.7' ]]; 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;'
+78
View File
@@ -3,6 +3,84 @@ 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
-------------
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
-------------
Released May 8th 2013
- Fixed bug in regards to imports with latest version of MongoEngine
Version 1.6.2
-------------
Released April 4th 2013
- Fixed bug with http basic auth
Version 1.6.1
-------------
Released April 3rd 2013
- Fixed bug with signals
Version 1.6.0 Version 1.6.0
------------- -------------
+1 -1
View File
@@ -9,7 +9,7 @@
<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>
+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.0' version = '1.6.9'
# The full version, including alpha/beta/rc tags. # The full version, including alpha/beta/rc tags.
release = version release = version
+40 -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
------------- -------------
@@ -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
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
+2 -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:
@@ -21,7 +22,7 @@ and libraries. They include:
2. `Flask-Mail <http://packages.python.org/Flask-Mail/>`_ 2. `Flask-Mail <http://packages.python.org/Flask-Mail/>`_
3. `Flask-Principal <http://packages.python.org/Flask-Principal/>`_ 3. `Flask-Principal <http://packages.python.org/Flask-Principal/>`_
4. `Flask-Script <http://packages.python.org/Flask-Script/>`_ 4. `Flask-Script <http://packages.python.org/Flask-Script/>`_
5. `Flask-WTF <http://packages.python.org/Flask-Mail/>`_ 5. `Flask-WTF <http://packages.python.org/Flask-WTF/>`_
6. `itsdangerous <http://packages.python.org/itsdangerous/>`_ 6. `itsdangerous <http://packages.python.org/itsdangerous/>`_
7. `passlib <http://packages.python.org/passlib/>`_ 7. `passlib <http://packages.python.org/passlib/>`_
+2 -2
View File
@@ -10,12 +10,12 @@
:license: MIT, see LICENSE for more details. :license: MIT, see LICENSE for more details.
""" """
__version__ = '1.6.0' __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
from .decorators import auth_token_required, http_auth_required, \ from .decorators import auth_token_required, http_auth_required, \
login_required, roles_accepted, roles_required login_required, roles_accepted, roles_required, auth_required
from .forms import ForgotPasswordForm, LoginForm, RegisterForm, \ from .forms import ForgotPasswordForm, LoginForm, RegisterForm, \
ResetPasswordForm, PasswordlessLoginForm, ConfirmRegisterForm ResetPasswordForm, PasswordlessLoginForm, ConfirmRegisterForm
from .signals import confirm_instructions_sent, password_reset, \ from .signals import confirm_instructions_sent, password_reset, \
+2 -3
View File
@@ -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
+5 -6
View File
@@ -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):
@@ -44,7 +43,7 @@ def send_confirmation_instructions(user):
'confirmation_instructions', user=user, 'confirmation_instructions', user=user,
confirmation_link=confirmation_link) confirmation_link=confirmation_link)
confirm_instructions_sent.send(user, app=app._get_current_object()) confirm_instructions_sent.send(app._get_current_object(), user=user)
return token return token
@@ -80,4 +79,4 @@ def confirm_user(user):
""" """
user.confirmed_at = datetime.utcnow() user.confirmed_at = datetime.utcnow()
_datastore.put(user) _datastore.put(user)
user_confirmed.send(user, app=app._get_current_object()) user_confirmed.send(app._get_current_object(), user=user)
+27 -19
View File
@@ -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():
@@ -179,8 +180,14 @@ def _get_login_manager(app):
lm.login_view = '%s.login' % cv('BLUEPRINT_NAME', app=app) lm.login_view = '%s.login' % cv('BLUEPRINT_NAME', app=app)
lm.user_loader(_user_loader) lm.user_loader(_user_loader)
lm.token_loader(_token_loader) lm.token_loader(_token_loader)
lm.login_message, lm.login_message_category = cv('MSG_LOGIN', app=app)
lm.needs_refresh_message, lm.needs_refresh_message_category = cv('MSG_REFRESH', app=app) if cv('FLASH_MESSAGES', app=app):
lm.login_message, lm.login_message_category = cv('MSG_LOGIN', app=app)
lm.needs_refresh_message, lm.needs_refresh_message_category = cv('MSG_REFRESH', app=app)
else:
lm.login_message = None
lm.needs_refresh_message = None
lm.init_app(app) lm.init_app(app)
return lm return lm
@@ -237,12 +244,11 @@ 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):
return (self.name != other and return not self.__eq__(other)
self.name != getattr(other, 'name', None))
class UserMixin(BaseUserMixin): class UserMixin(BaseUserMixin):
@@ -261,14 +267,16 @@ class UserMixin(BaseUserMixin):
"""Returns `True` if the user identifies with the specified role. """Returns `True` if the user identifies with the specified role.
:param role: A role name or `Role` instance""" :param role: A role name or `Role` instance"""
return role in self.roles if isinstance(role, basestring):
return role in (role.name for role in self.roles)
else:
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):
@@ -338,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.
+64 -14
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
@@ -95,12 +100,11 @@ class UserDatastore(object):
:param user: The user to manipulate :param user: The user to manipulate
:param role: The role to add to the user :param role: The role to add to the user
""" """
rv = False
user, role = self._prepare_role_modify_args(user, role) user, role = self._prepare_role_modify_args(user, role)
if role not in user.roles: if role not in user.roles:
rv = True
user.roles.append(role) user.roles.append(role)
return rv return True
return False
def remove_role_from_user(self, user, role): def remove_role_from_user(self, user, role):
"""Removes a role from a user """Removes a role from a user
@@ -175,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()
@@ -190,8 +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):
return self.user_model.objects(**kwargs).first() try:
from mongoengine.queryset import Q, QCombination
except ImportError:
from mongoengine.queryset.visitor import Q, QCombination
from mongoengine.errors import ValidationError
queries = map(lambda i: Q(**{i[0]: i[1]}), kwargs.items())
query = QCombination(QCombination.AND, queries)
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()
@@ -210,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()
@@ -217,10 +266,7 @@ class PeeweeUserDatastore(PeeweeDatastore, UserDatastore):
return None return None
def find_role(self, role): def find_role(self, role):
try: return self.role_model.filter(name=role).get()
return self.role_model.filter(name=role).get()
except self.role_model.DoesNotExist:
return None
def create_user(self, **kwargs): def create_user(self, **kwargs):
"""Creates and returns a new user from the given parameters.""" """Creates and returns a new user from the given parameters."""
@@ -231,7 +277,6 @@ class PeeweeUserDatastore(PeeweeDatastore, UserDatastore):
self.add_role_to_user(user, role) self.add_role_to_user(user, role)
return user return user
def add_role_to_user(self, user, role): def add_role_to_user(self, user, role):
"""Adds a role tp a user """Adds a role tp a user
@@ -239,10 +284,12 @@ class PeeweeUserDatastore(PeeweeDatastore, UserDatastore):
:param role: The role to add to the user :param role: The role to add to the user
""" """
user, role = self._prepare_role_modify_args(user, role) user, role = self._prepare_role_modify_args(user, role)
if self.UserRole.select().where(self.UserRole.user==user, self.UserRole.role==role).count(): result = self.UserRole.select() \
.where(self.UserRole.user == user.id, self.UserRole.role == role.id)
if result.count():
return False return False
else: else:
self.UserRole.create(user=user, role=role) self.UserRole.create(user=user.id, role=role.id)
return True return True
def remove_role_from_user(self, user, role): def remove_role_from_user(self, user, role):
@@ -252,9 +299,12 @@ class PeeweeUserDatastore(PeeweeDatastore, UserDatastore):
:param role: The role to remove from the user :param role: The role to remove from the user
""" """
user, role = self._prepare_role_modify_args(user, role) user, role = self._prepare_role_modify_args(user, role)
if self.UserRole.select().where(self.UserRole.user==user, self.UserRole.role==role).count(): result = self.UserRole.select() \
self.UserRole.delete().where(self.UserRole.user==user, self.UserRole.role==role) .where(self.UserRole.user == user, self.UserRole.role == role)
if result.count():
query = self.UserRole.delete().where(
self.UserRole.user == user, self.UserRole.role == role)
query.execute()
return True return True
else: else:
return False return False
+8 -9
View File
@@ -9,6 +9,7 @@
:license: MIT, see LICENSE for more details. :license: MIT, see LICENSE for more details.
""" """
from collections import namedtuple
from functools import wraps from functools import wraps
from flask import current_app, Response, request, redirect, _request_ctx_stack from flask import current_app, Response, request, redirect, _request_ctx_stack
@@ -30,6 +31,8 @@ _default_unauthorized_html = """
or your browser doesn't understand how to supply the credentials required.</p> or your browser doesn't understand how to supply the credentials required.</p>
""" """
BasicAuth = namedtuple('BasicAuth', 'username, password')
def _get_unauthorized_response(text=None, headers=None): def _get_unauthorized_response(text=None, headers=None):
text = text or _default_unauthorized_html text = text or _default_unauthorized_html
@@ -50,24 +53,20 @@ 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 dict(username=None, password=None) auth = request.authorization or BasicAuth(username=None, password=None)
user = _security.datastore.find_user(email=auth.username) user = _security.datastore.find_user(email=auth.username)
if user and utils.verify_and_update_password(auth.password, user): if user and utils.verify_and_update_password(auth.password, user):
+28 -27
View File
@@ -15,8 +15,9 @@ 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
SubmitField, HiddenField, BooleanField, ValidationError, Field from wtforms import TextField, PasswordField, validators, \
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'))
+5 -6
View File
@@ -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,14 +30,13 @@ 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)
login_instructions_sent.send(dict(user=user, login_token=token), login_instructions_sent.send(app._get_current_object(),
app=app._get_current_object()) user=user, login_token=token)
def generate_login_token(user): def generate_login_token(user):
+7 -7
View File
@@ -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,15 +29,14 @@ 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',
user=user, reset_link=reset_link) user=user, reset_link=reset_link)
reset_password_instructions_sent.send(dict(user=user, token=token), reset_password_instructions_sent.send(app._get_current_object(),
app=app._get_current_object()) user=user, token=token)
def send_password_reset_notice(user): def send_password_reset_notice(user):
@@ -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
@@ -77,4 +77,4 @@ def update_password(user, password):
user.password = encrypt_password(password) user.password = encrypt_password(password)
_datastore.put(user) _datastore.put(user)
send_password_reset_notice(user) send_password_reset_notice(user)
password_reset.send(user, app=app._get_current_object()) password_reset.send(app._get_current_object(), user=user)
+4 -4
View File
@@ -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'])
@@ -33,11 +33,11 @@ def register_user(**kwargs):
confirmation_link, token = generate_confirmation_link(user) confirmation_link, token = generate_confirmation_link(user)
do_flash(*get_message('CONFIRM_REGISTRATION', email=user.email)) do_flash(*get_message('CONFIRM_REGISTRATION', email=user.email))
user_registered.send(dict(user=user, confirm_token=token), user_registered.send(app._get_current_object(),
app=app._get_current_object()) user=user, confirm_token=token)
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
+23 -13
View File
@@ -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):
@@ -273,7 +285,7 @@ def get_token_status(token, serializer, max_age=None):
def capture_passwordless_login_requests(): def capture_passwordless_login_requests():
login_requests = [] login_requests = []
def _on(data, app): def _on(app, **data):
login_requests.append(data) login_requests.append(data)
login_instructions_sent.connect(_on) login_instructions_sent.connect(_on)
@@ -293,7 +305,7 @@ def capture_registrations():
""" """
registrations = [] registrations = []
def _on(data, app): def _on(app, **data):
registrations.append(data) registrations.append(data)
user_registered.connect(_on) user_registered.connect(_on)
@@ -313,8 +325,8 @@ def capture_reset_password_requests(reset_password_sent_at=None):
""" """
reset_requests = [] reset_requests = []
def _on(request, app): def _on(app, **data):
reset_requests.append(request) reset_requests.append(data)
reset_password_instructions_sent.connect(_on) reset_password_instructions_sent.connect(_on)
@@ -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
View File
@@ -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))
+2 -2
View File
@@ -20,7 +20,7 @@ from setuptools import setup
setup( setup(
name='Flask-Security', name='Flask-Security',
version='1.6.0', 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,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
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)
+49 -27
View File
@@ -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)
+17 -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,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()
@@ -142,6 +147,13 @@ class DefaultSecurityTests(SecurityTest):
}) })
self.assertIn('HTTP Authentication', r.data) self.assertIn('HTTP Authentication', r.data)
def test_http_auth_no_authorization(self):
r = self._get('/http', headers={})
self.assertIn('<h1>Unauthorized</h1>', r.data)
self.assertIn('WWW-Authenticate', r.headers)
self.assertEquals('Basic realm="Login Required"',
r.headers['WWW-Authenticate'])
def test_invalid_http_auth_invalid_username(self): def test_invalid_http_auth_invalid_username(self):
r = self._get('/http', headers={ r = self._get('/http', headers={
'Authorization': 'Basic ' + base64.b64encode("bogus:bogus") 'Authorization': 'Basic ' + base64.b64encode("bogus:bogus")
@@ -224,11 +236,11 @@ class MongoEngineSecurityTests(DefaultSecurityTests):
return create_app(auth_config, **kwargs) return create_app(auth_config, **kwargs)
# class PeeweeSecurityTests(DefaultSecurityTests): class PeeweeSecurityTests(DefaultSecurityTests):
# def _create_app(self, auth_config, **kwargs): def _create_app(self, auth_config, **kwargs):
# from tests.test_app.peewee_app import create_app from tests.test_app.peewee_app import create_app
# return create_app(auth_config, **kwargs) return create_app(auth_config, **kwargs)
class DefaultDatastoreTests(SecurityTest): class DefaultDatastoreTests(SecurityTest):
+27 -20
View File
@@ -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,
@@ -31,9 +38,9 @@ class RegisterableSignalsTests(SecurityTest):
calls = mocks[user_registered] calls = mocks[user_registered]
self.assertEqual(len(calls), 1) self.assertEqual(len(calls), 1)
args, kwargs = calls[0] args, kwargs = calls[0]
self.assertTrue(compare_user(args[0]['user'], user)) self.assertTrue(compare_user(kwargs['user'], user))
self.assertIn('confirm_token', args[0]) self.assertIn('confirm_token', kwargs)
self.assertEqual(kwargs['app'], self.app) self.assertEqual(args[0], self.app)
def test_register_without_password(self): def test_register_without_password(self):
e = 'dude@lp.com' e = 'dude@lp.com'
@@ -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,
@@ -61,8 +68,8 @@ class ConfirmableSignalsTests(SecurityTest):
calls = mocks[user_confirmed] calls = mocks[user_confirmed]
self.assertEqual(len(calls), 1) self.assertEqual(len(calls), 1)
args, kwargs = calls[0] args, kwargs = calls[0]
self.assertEqual(args[0].id, user.id) self.assertEqual(args[0], self.app)
self.assertEqual(kwargs['app'], self.app) self.assertTrue(compare_user(kwargs['user'], user))
def test_confirm_bad_token(self): def test_confirm_bad_token(self):
e = 'dude@lp.com' e = 'dude@lp.com'
@@ -94,8 +101,8 @@ class ConfirmableSignalsTests(SecurityTest):
calls = mocks[confirm_instructions_sent] calls = mocks[confirm_instructions_sent]
self.assertEqual(len(calls), 1) self.assertEqual(len(calls), 1)
args, kwargs = calls[0] args, kwargs = calls[0]
self.assertTrue(compare_user(args[0], user)) self.assertTrue(compare_user(kwargs['user'], user))
self.assertEqual(kwargs['app'], self.app) self.assertEqual(args[0], self.app)
def test_send_confirmation_bad_email(self): def test_send_confirmation_bad_email(self):
with capture_signals() as mocks: with capture_signals() as mocks:
@@ -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,
@@ -120,9 +127,9 @@ class RecoverableSignalsTests(SecurityTest):
calls = mocks[reset_password_instructions_sent] calls = mocks[reset_password_instructions_sent]
self.assertEqual(len(calls), 1) self.assertEqual(len(calls), 1)
args, kwargs = calls[0] args, kwargs = calls[0]
self.assertTrue(compare_user(args[0]['user'], user)) self.assertTrue(compare_user(kwargs['user'], user))
self.assertIn('token', args[0]) self.assertIn('token', kwargs)
self.assertEqual(kwargs['app'], self.app) self.assertEqual(args[0], self.app)
def test_reset_password(self): def test_reset_password(self):
with capture_reset_password_requests() as requests: with capture_reset_password_requests() as requests:
@@ -137,8 +144,8 @@ class RecoverableSignalsTests(SecurityTest):
calls = mocks[password_reset] calls = mocks[password_reset]
self.assertEqual(len(calls), 1) self.assertEqual(len(calls), 1)
args, kwargs = calls[0] args, kwargs = calls[0]
self.assertTrue(compare_user(args[0], user)) self.assertTrue(compare_user(kwargs['user'], user))
self.assertEqual(kwargs['app'], self.app) self.assertEqual(args[0], self.app)
def test_reset_password_invalid_emails(self): def test_reset_password_invalid_emails(self):
with capture_signals() as mocks: with capture_signals() as mocks:
@@ -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
@@ -233,6 +240,6 @@ class PasswordlessTests(SecurityTest):
calls = mocks[login_instructions_sent] calls = mocks[login_instructions_sent]
self.assertEqual(len(calls), 1) self.assertEqual(len(calls), 1)
args, kwargs = calls[0] args, kwargs = calls[0]
self.assertTrue(compare_user(args[0]['user'], user)) self.assertTrue(compare_user(kwargs['user'], user))
self.assertIn('login_token', args[0]) self.assertIn('login_token', kwargs)
self.assertEqual(kwargs['app'], self.app) self.assertEqual(args[0], self.app)
+4
View File
@@ -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
+2 -1
View File
@@ -14,11 +14,12 @@ from flask.ext.security import Security, UserMixin, RoleMixin, \
from tests.test_app import create_app as create_base_app, populate_data, \ from tests.test_app import create_app as create_base_app, populate_data, \
add_context_processors add_context_processors
def create_app(config, **kwargs): def create_app(config, **kwargs):
app = create_base_app(config) app = create_base_app(config)
app.config['DATABASE'] = { app.config['DATABASE'] = {
'name': 'example2.db', 'name': 'example2.db',
'engine': 'peewee.SqliteDatabase', 'engine': 'peewee.SqliteDatabase'
} }
db = Database(app) db = Database(app)