Compare commits

..
107 Commits
Author SHA1 Message Date
Matt Wright 316d945d96 Bump version number to 1.7.1 2014-01-14 10:41:53 -05:00
Matt Wright 6eb77c06ad Update CHANGES 2014-01-14 10:41:38 -05:00
Matt Wright 35fd08772b Add configured password hash test back and fix bug with checking passwords 2014-01-14 10:34:57 -05:00
Matt Wright 76fc578cf5 Bump version number to 1.7.0 2014-01-10 14:40:24 -05:00
Matt Wright 91ee026ff7 Merge branch 'master' into develop 2014-01-10 14:39:22 -05:00
Matt Wright e3b74acc5a Update CHANGES 2014-01-10 14:38:58 -05:00
Matt Wright 968f85a39c Update CHANGES 2014-01-10 14:03:45 -05:00
Matt Wright 68ecc7bb10 Update test dependencies in setup.py 2014-01-07 16:40:59 -05:00
Matt Wright ac2dfe964b Update dependencies in setup.py 2014-01-07 16:40:35 -05:00
Matt Wright 730b86fbc7 Add more python envs 2014-01-07 16:37:07 -05:00
Matt Wright 850b0e714b Additional test fixes 2014-01-07 16:31:26 -05:00
Matt Wright 8d2815798f WIP 2013-12-24 12:38:05 -05:00
Matt Wright f7b645005e work in progress 2013-12-20 13:39:41 -05:00
Matt Wright afaf6c7d62 Polish 2013-12-20 13:38:44 -05:00
Matt Wright f1447b2adc Work in progress 2013-12-19 16:12:29 -05:00
Matt Wright d95a8c9364 Nope, nevermind. Dont use tox for tests. 2013-12-19 15:11:50 -05:00
Matt Wright 06ce1c68fd Use tox for running tests 2013-12-19 15:10:20 -05:00
Matt Wright c302fb8e30 Update .gitignore 2013-12-19 14:45:53 -05:00
Matt Wright d88299fc9b Add test to check SECURITY_LOGIN_WITHOUT_CONFIRMATION feature 2013-12-19 14:40:43 -05:00
Matt Wright ce439b7fec Merge pull request #193 from nickretallack/develop
Fix attempts to log in without a password, or without confirming your email
2013-12-19 11:39:47 -08:00
Matt Wright af8e9f7ca5 Render auth token when registering a use with JSON 2013-12-19 14:26:47 -05:00
Matt Wright 6e461a81bf Add AUTHORS file 2013-12-19 14:09:37 -05:00
Matt Wright d7d090afc6 Polish 2013-12-19 14:00:44 -05:00
Matt Wright 11b8222ec5 Add SECURITY_SEND_PASSWORD_RESET_NOTICE_EMAIL config option to optionally send password reset notice emails. Addresses #199 2013-12-19 13:56:34 -05:00
Matt Wright be8448a7cf Merge pull request #197 from kelvinhammond/patch-1
Update NextFormMixin.validate_next to check if next was even specified
2013-12-19 10:46:45 -08:00
Matt Wright fe170e6eb3 Fixes #196 2013-12-19 13:45:14 -05:00
Matt Wright c1ff98cdf3 Document . Addresses #194 2013-12-19 13:35:03 -05:00
Matt Wright b88b5bf2c3 Merge pull request #183 from NeuralGlue/master
Update features.rst
2013-12-19 10:17:26 -08:00
Matt Wright f0e60dd2d1 Merge pull request #177 from srijan/patch-1
Use get_json instead of json
2013-12-19 10:09:20 -08:00
Matt Wright 095ade8bbc Merge pull request #176 from lojack/develop
this fixes #175
2013-12-19 10:08:51 -08:00
Matt Wright 615bc00c26 Add flask_security.utils documentation for selected functions. Addressed #169 2013-12-19 12:57:11 -05:00
Matt Wright be9af2e107 Merge branch 'develop' into configurable_login_fields 2013-12-19 12:16:02 -05:00
Matt Wright 2f21523d47 Merge branch 'develop' of github.com:mattupstate/flask-security into develop 2013-12-19 12:10:09 -05:00
kelvinhammond 986a48c5e0 Update forms.py
Fixed: If login.next is not passed form throws error
2013-12-14 13:01:41 -06:00
Nick Retallack 1596ef75d4 login_without_confirmation should allow you to log in without confirmation 2013-12-11 03:12:29 -08:00
Nick Retallack 292f89c204 Prevent it from exploding if you try to log in with a user who has no password in the database. 2013-12-11 03:08:58 -08:00
Matt Wright 1b8a4bcab4 Merge pull request #187 from surfstudio/change_password_config_fix
Added a config parameter for change password template
2013-11-25 08:34:15 -08:00
Alexander Sukharev 4549a02219 Added a doc entry for parameter from the previous commit 2013-11-22 18:41:08 +04:00
sanek e0881ed6a0 Added a config parameter for change password template 2013-11-22 18:02:35 +04:00
Charles Young b1ae7b4352 Update features.rst
Changed HTTP header name to reflect reality in code
2013-11-20 10:58:32 -08:00
Srijan Choudhary e1c7ec303f Use get_json instead of json
The `request.json` method now calls `get_json`, which raises `BadRequest` if there is no json data or some error with it. So, it cannot be directly used as a check for presence of json data. This code currently returns a bad request if content type is `application/json` but json data is empty.

https://github.com/mitsuhiko/flask/blob/master/flask/wrappers.py#L110
2013-10-31 10:19:12 +05:30
Robert Clark b6007cb515 this fixes #175 2013-10-30 18:04:42 -04:00
Matt Wright 1ae6bc3cf1 Add the ability to specify additional fields on the user model that can be used for logging in. 2013-10-16 14:00:36 -04:00
Matt Wright 80f5fa8dbb Merge branch 'develop' of github.com:mattupstate/flask-security into develop 2013-10-16 11:16:17 -04:00
Matt Wright 75794d5527 Merge pull request #158 from kleschenko/patch-2
removed unnecessary utcnow call
2013-10-16 08:16:09 -07:00
Matt Wright 9999325ffb Show an error if a user tries to change their password and its the same as before. Fixes #160 2013-10-16 11:15:17 -04:00
Matt Wright b1ac2598c0 Be sure to save user model when using add_role_to_user method for MongoEngineUserDatastore. Fixes #170 2013-10-16 10:55:56 -04:00
Matt Wright cfd0efb91d Fix #167 2013-10-03 10:10:06 -04:00
Matt Wright 7808870fcd Merge pull request #163 from joehand/develop
Add option to disable password change email.
2013-10-03 07:05:54 -07:00
Joe Hand 55ffe2563e Update test names and add docstring. 2013-10-02 08:24:27 -06:00
Matt Wright a054241144 Merge pull request #168 from iurisilvio/patch-1
Fix typo: algorith -> algorithm
2013-10-02 07:11:59 -07:00
Iuri de Silvio a112ea06bb Fix typo: algorith -> algorithm 2013-09-27 21:04:51 -03:00
Joe Hand 20c16107e8 Add test for 'SECURITY_SEND_PASSWORD_CHANGE_EMAIL' configuration 2013-09-22 09:55:07 -06:00
Matt Wright 7121ebff22 Merge pull request #159 from martinmaillard/fix-typo-docs
Fix email templates paths in docs
2013-09-18 06:38:36 -07:00
Joe Hand 389d944aaf Add option to disable password change email. 2013-09-15 18:38:36 -06:00
Matt Wright 73ff1c00b3 Fixes #146 2013-09-11 01:10:16 -04:00
Kostyantyn Leschenko 5a780ce741 simplified get_max_age return value 2013-09-05 18:12:31 +03:00
Kostyantyn Leschenko 20eed621ed Merge branch 'develop' into patch-2 2013-09-05 18:10:05 +03:00
Matt Wright 0103bf4269 Remove pypy tests 2013-09-03 13:08:13 -04:00
Matt Wright 13422e046f Use bcrypt instead of py-bcrypt 2013-09-03 13:00:03 -04:00
Matt Wright 6b3d65d6d6 Fix flask-login test setting 2013-09-03 12:20:56 -04:00
Matt Wright 1e958115e1 Fix tests 2013-09-03 11:55:13 -04:00
Martin Maillard b87c6fe1aa Fix email templates path in docs 2013-09-03 16:45:44 +02:00
Matt Wright 1d8a5f7c2e Merge branch 'develop' of github.com:mattupstate/flask-security into develop 2013-09-03 10:25:38 -04:00
Matt Wright 40409af1dd Fix documentation issue #145 2013-09-03 10:22:57 -04:00
Matt Wright af0f0c202f Update MANIFEST.in 2013-09-03 10:15:39 -04:00
Kostyantyn Leschenko 9ddb909342 removed unnecessary utcnow call
is this code required here?
2013-09-03 15:32:49 +03:00
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
38 changed files with 793 additions and 361 deletions
+2
View File
@@ -31,3 +31,5 @@ env/
#Editor temporaries
*~
*.db
+2 -4
View File
@@ -3,15 +3,13 @@ language: python
python:
- "2.6"
- "2.7"
- "3.3"
- "pypy"
install:
- pip install . --quiet
- "if [[ $TRAVIS_PYTHON_VERSION == '2.6' ]]; then pip install importlib --quiet --use-mirrors; fi"
- pip install nose simplejson Flask-SQLAlchemy Flask-MongoEngine Flask-Peewee py-bcrypt MySQL-python --quiet
before_script:
- mysql -e 'create database flask_security_test;'
- pip install nose simplejson Flask-SQLAlchemy Flask-MongoEngine Flask-Peewee bcrypt --quiet
services:
- mongodb
+38
View File
@@ -0,0 +1,38 @@
Flask-Security is written and maintained by Matt Wright and
various contributors:
Development Lead
````````````````
- Matt Wright <matt+github@nobien.net>
Patches and Suggestions
```````````````````````
Alexander Sukharev
Alexey Poryadin
Andrew J. Camenga
Anthony Plunkett
Artem Andreev
Catherine Wise
Chris Haines
Christophe Simonis
David Ignacio
Eric Butler
Eskil Heyn Olsen
Iuri de Silvio
Jay Goel
Joe Esposito
Joe Hand
Josh Purvis
Kostyantyn Leschenko
Luca Invernizzi
Manuel Ebert
Martin Maillard
Paweł Krześniak
Robert Clark
Rodrigue Cloutier
Rotem Yaari
Srijan Choudhary
Tristan Escalada
Vadim Kotov
+72
View File
@@ -3,6 +3,78 @@ Flask-Security Changelog
Here you can see the full list of changes between each Flask-Security release.
Version 1.7.1
-------------
Released January 14th 2014
- Fixed a bug where passwords would fail to verify when specifying a password hash algorithm
Version 1.7.0
-------------
Released January 10th 2014
- Python 3.3 support!
- Dependency updates
- Fixed a bug when `SECURITY_LOGIN_WITHOUT_CONFIRMATION = True` did not allow users to log in
- Added `SECURITY_SEND_PASSWORD_RESET_NOTICE_EMAIL` configuraiton option to optionally send password reset notice emails
- Add documentation for `@security.send_mail_task`
- Move to `request.get_json` as `request.json` is now deprecated in Flask
- Fixed a bug when using AJAX to change a user's password
- Added documentation for select functions in the `flask_security.utils` module
- Fixed a bug in `flask_security.forms.NextFormMixin`
- Added `CHANGE_PASSWORD_TEMPLATE` configuration option to optionally specify a different change password template
- Added the ability to specify addtional fields on the user model to be used for identifying the user via the `USER_IDENTITY_ATTRIBUTES` configuration option
- An error is now shown if a user tries to change their password and the password is the same as before. The message can be customed with the `SECURITY_MSG_PASSWORD_IS_SAME` configuration option
- Fixed a bug in `MongoEngineUserDatastore` where user model would not be updated when using the `add_role_to_user` method
- Added `SECURITY_SEND_PASSWORD_CHANGE_EMAIL` configuration option to optionally disable password change email from being sent
- Fixed a bug in the `find_or_create_role` method of the PeeWee datastore
- Removed pypy tests
- Fixed some tests
- Include CHANGES and LICENSE in MANIFEST.in
- A bit of documentation cleanup
- A bit of code cleanup including removal of unnecessary utcnow call and simplification of get_max_age method
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
-------------
+2 -1
View File
@@ -1,2 +1,3 @@
include CHANGES LICENSE
recursive-include tests *.py
recursive-include flask_security/templates *.*
recursive-include flask_security/templates *.*
+2 -2
View File
@@ -9,9 +9,9 @@
<ul>
<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/jfinkels/flask-security/issues">Issue Tracker</a></li>
<li><a href="http://github.com/mattupstate/flask-security/issues">Issue Tracker</a></li>
</ul>
<ul>
<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>
</ul>
</ul>
+22
View File
@@ -54,6 +54,28 @@ Datastores
:inherited-members:
Utils
-----
.. autofunction:: flask_security.utils.login_user
.. autofunction:: flask_security.utils.logout_user
.. autofunction:: flask_security.utils.get_hmac
.. autofunction:: flask_security.utils.verify_password
.. autofunction:: flask_security.utils.verify_and_update_password
.. autofunction:: flask_security.utils.encrypt_password
.. autofunction:: flask_security.utils.url_for_security
.. autofunction:: flask_security.utils.get_within_delta
.. autofunction:: flask_security.utils.send_mail
.. autofunction:: flask_security.utils.get_token_status
Signals
-------
See the `Flask documentation on signals`_ for information on how to use these
+1
View File
@@ -0,0 +1 @@
.. include:: ../AUTHORS
+1 -1
View File
@@ -49,7 +49,7 @@ copyright = u'2012, Matt Wright'
# built documents.
#
# The short X.Y version.
version = '1.6.5'
version = '1.7.1'
# The full version, including alpha/beta/rc tags.
release = version
+91 -58
View File
@@ -18,7 +18,7 @@ Core
``SECURITY_FLASH_MESSAGES`` Specifies wether or not to flash
messages during security procedures.
Defaults to ``True``.
``SECURITY_PASSWORD_HASH`` Specifies the password hash algorith to
``SECURITY_PASSWORD_HASH`` Specifies the password hash algorithm to
use when encrypting and decrypting
passwords. Recommended values for
production systems are ``bcrypt``,
@@ -56,6 +56,8 @@ URLs and Views
``/register``.
``SECURITY_RESET_URL`` Specifies the password reset URL. Defaults to
``/reset``.
``SECURITY_CHANGE_URL`` Specifies the password change URL. Defaults to
``/change``.
``SECURITY_CONFIRM_URL`` Specifies the email confirmation URL. Defaults
to ``/confirm``.
``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
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``.
value of ``SECURITY_POST_LOGIN_VIEW``. Defaults
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
attempts to access a URL/endpoint that they do
not have permission to access. If this value is
@@ -114,6 +122,9 @@ Template Paths
``SECURITY_RESET_PASSWORD_TEMPLATE`` Specifies the path to the template for
the reset password page. Defaults to
``security/reset_password.html``.
``SECURITY_CHANGE_PASSWORD_TEMPLATE`` Specifies the path to the template for
the change password page. Defaults to
``security/change_password.html``.
``SECURITY_SEND_CONFIRMATION_TEMPLATE`` Specifies the path to the template for
the resend confirmation instructions
page. Defaults to
@@ -167,64 +178,86 @@ Email
.. tabularcolumns:: |p{6.5cm}|p{8.5cm}|
=========================================== ====================================
``SECURITY_EMAIL_SUBJECT_REGISTER`` Sets the subject for the
confirmation email. Defaults to
``Welcome``
``SECURITY_EMAIL_SUBJECT_PASSWORDLESS`` Sets the subject for the
passwordless feature. Defaults to
``Login instructions``
``SECURITY_EMAIL_SUBJECT_PASSWORD_NOTICE`` Sets subject for the password
notice. Defaults to
``Your password has been reset``
``SECURITY_EMAIL_SUBJECT_PASSWORD_RESET`` Sets the subject for the password
reset. Defaults to
``Password reset instructions``
``SECURITY_EMAIL_SUBJECT_CONFIRM`` Sets the subject for the email
confirmation message. Defaults to
``Please confirm your email``
=========================================== ====================================
================================================= ==============================
``SECURITY_EMAIL_SUBJECT_REGISTER`` Sets the subject for the
confirmation email. Defaults
to ``Welcome``
``SECURITY_EMAIL_SUBJECT_PASSWORDLESS`` Sets the subject for the
passwordless feature. Defaults
to ``Login instructions``
``SECURITY_EMAIL_SUBJECT_PASSWORD_NOTICE`` Sets subject for the password
notice. Defaults to ``Your
password has been reset``
``SECURITY_EMAIL_SUBJECT_PASSWORD_RESET`` Sets the subject for the
password reset email. Defaults
to ``Password reset
instructions``
``SECURITY_EMAIL_SUBJECT_PASSWORD_CHANGE_NOTICE`` Sets the subject for the
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
-------------
.. tabularcolumns:: |p{6.5cm}|p{8.5cm}|
======================================= ========================================
``SECURITY_SEND_REGISTER_EMAIL`` Specifies whether registration email is
sent. Defaults to ``True``.
``SECURITY_CONFIRM_EMAIL_WITHIN`` Specifies the amount of time a user has
before their confirmation link expires.
Always pluralized the time unit for this
value. Defaults to ``5 days``.
``SECURITY_RESET_PASSWORD_WITHIN`` Specifies the amount of time a user has
before their password reset link
expires. Always pluralized the time unit
for this value. Defaults to ``5 days``.
``SECURITY_LOGIN_WITHIN`` Specifies the amount of time a user has
before a login link expires. This is
only used when the passwordless login
feature is enabled. Always pluralized
the time unit for this value. Defaults
to ``1 days``.
``SECURITY_LOGIN_WITHOUT_CONFIRMATION`` Specifies if a user may login before
confirming their email when the value
of ``SECURITY_CONFIRMABLE`` is set to
``True``. Defaults to ``False``.
``SECURITY_CONFIRM_SALT`` Specifies the salt value when generating
confirmation links/tokens. Defaults to
``confirm-salt``.
``SECURITY_RESET_SALT`` Specifies the salt value when generating
password reset links/tokens. Defaults to
``reset-salt``.
``SECURITY_LOGIN_SALT`` Specifies the salt value when generating
login links/tokens. Defaults to
``login-salt``.
``SECURITY_REMEMBER_SALT`` Specifies the salt value when generating
remember tokens. Remember tokens are
used instead of user ID's as it is more
secure. Defaults to ``remember-salt``.
``SECURITY_DEFAULT_REMEMBER_ME`` Specifies the default "remember me"
value used when logging in a user.
Defaults to ``False``.
======================================= ========================================
============================================= ==================================
``SECURITY_SEND_REGISTER_EMAIL`` Specifies whether registration
email is sent. Defaults to
``True``.
``SECURITY_SEND_PASSWORD_CHANGE_EMAIL`` Specifies whether password change
email is sent. Defaults to
``True``.
``SECURITY_SEND_PASSWORD_RESET_NOTICE_EMAIL`` Specifies whether password reset
notice email is sent. Defaults to
``True``.
``SECURITY_CONFIRM_EMAIL_WITHIN`` Specifies the amount of time a
user has before their confirmation
link expires. Always pluralized
the time unit for this value.
Defaults to ``5 days``.
``SECURITY_RESET_PASSWORD_WITHIN`` Specifies the amount of time a
user has before their password
reset link expires. Always
pluralized the time unit for this
value. Defaults to ``5 days``.
``SECURITY_LOGIN_WITHIN`` Specifies the amount of time a
user has before a login link
expires. This is only used when
the passwordless login feature is
enabled. Always pluralized the
time unit for this value.
Defaults to ``1 days``.
``SECURITY_LOGIN_WITHOUT_CONFIRMATION`` Specifies if a user may login
before confirming their email when
the value of
``SECURITY_CONFIRMABLE`` is set to
``True``. Defaults to ``False``.
``SECURITY_CONFIRM_SALT`` Specifies the salt value when
generating confirmation
links/tokens. Defaults to
``confirm-salt``.
``SECURITY_RESET_SALT`` Specifies the salt value when
generating password reset
links/tokens. Defaults to
``reset-salt``.
``SECURITY_LOGIN_SALT`` Specifies the salt value when
generating login links/tokens.
Defaults to ``login-salt``.
``SECURITY_REMEMBER_SALT`` Specifies the salt value when
generating remember tokens.
Remember tokens are used instead
of user ID's as it is more
secure. Defaults to
``remember-salt``.
``SECURITY_DEFAULT_REMEMBER_ME`` Specifies the default "remember
me" value used when logging in
a user. Defaults to ``False``.
============================================= ==================================
+2 -1
View File
@@ -10,4 +10,5 @@ Contents
models
customizing
api
changelog
changelog
authors
+35 -15
View File
@@ -107,21 +107,21 @@ Emails
------
Flask-Security is also packaged with a default tempalte for each email that it
may send. Templates are located within the subfolder named ``security/mail``.
may send. Templates are located within the subfolder named ``security/email``.
The following is a list of email templates:
* `security/mail/confirmation_instructions.html`
* `security/mail/confirmation_instructions.txt`
* `security/mail/login_instructions.html`
* `security/mail/login_instructions.txt`
* `security/mail/reset_instructions.html`
* `security/mail/reset_instructions.txt`
* `security/mail/reset_notice.html`
* `security/mail/change_notice.txt`
* `security/mail/change_notice.html`
* `security/mail/reset_notice.txt`
* `security/mail/welcome.html`
* `security/mail/welcome.txt`
* `security/email/confirmation_instructions.html`
* `security/email/confirmation_instructions.txt`
* `security/email/login_instructions.html`
* `security/email/login_instructions.txt`
* `security/email/reset_instructions.html`
* `security/email/reset_instructions.txt`
* `security/email/reset_notice.html`
* `security/email/change_notice.txt`
* `security/email/change_notice.html`
* `security/email/reset_notice.txt`
* `security/email/welcome.html`
* `security/email/welcome.txt`
Overriding these templates is simple:
@@ -132,11 +132,31 @@ Overriding these templates is simple:
Each template is passed a template context object that includes values for any
links that are required in the email. If you require more values in the
templates you can specify an email context processor with the
``email_context_processor`` decorator. For example::
``mail_context_processor`` decorator. For example::
security = Security(app, user_datastore)
# This processor is added to all emails
@security.email_context_processor
@security.mail_context_processor
def security_mail_processor():
return dict(hello="world")
Emails with Celery
------------------
Sometimes it makes sense to send emails via a task queue, such as
`Celery<http://www.celeryproject.org/>`_. To delay the sending of emails you can
use the ``@security.send_mail_task`` decorator like so::
# Setup the task
@celery.task
def send_security_email(msg):
# Use the Flask-Mail extension instance to send the incoming ``msg`` parameter
# which is an instance of `flask_mail.Message`
mail.send(msg)
@security.send_mail_task
def delay_security_email(msg):
send_security_email.delay(msg)
+17 -2
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
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
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
@@ -54,7 +55,7 @@ authentication endpoint. A successful call to this endpoint will return the
user's ID and their authentication token. This token can be used in subsequent
requests to protected resources. The auth token is supplied in the request
through an HTTP header or query string parameter. By default the HTTP header
name is `X-Auth-Token` and the default query string parameter name is
name is `Authentication-Token` and the default query string parameter name is
`auth_token`. Authentication tokens are generated using the user's password.
Thus if the user changes his or her password their existing authentication token
will become invalid. A new token will need to be retrieved using the user's new
@@ -104,6 +105,20 @@ statistics. They include:
* 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/
.. _alternative token: http://packages.python.org/Flask-Login/#alternative-tokens
+1
View File
@@ -13,6 +13,7 @@ Flask application. They include:
7. Token based password recovery / resetting (optional)
8. User registration (optional)
9. Login tracking (optional)
10. JSON/Ajax Support
Many of these features are made possible by integrating various Flask extensions
and libraries. They include:
+1 -1
View File
@@ -185,7 +185,7 @@ possible using Peewee:
db = Database(app)
class Role(db.Model, RoleMixin):
name = TextField(unique=True)
name = CharField(unique=True)
description = TextField(null=True)
class User(db.Model, UserMixin):
+1 -1
View File
@@ -10,7 +10,7 @@
:license: MIT, see LICENSE for more details.
"""
__version__ = '1.6.5'
__version__ = '1.7.1'
from .core import Security, RoleMixin, UserMixin, AnonymousUser, current_user
from .datastore import SQLAlchemyUserDatastore, MongoEngineUserDatastore, PeeweeUserDatastore
+3 -2
View File
@@ -10,7 +10,7 @@
: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 .signals import password_changed
@@ -28,7 +28,8 @@ def send_password_changed_notice(user):
:param user: The user to send the notice to
"""
send_mail(config_value('EMAIL_SUBJECT_PASSWORD_CHANGE_NOTICE'), user.email,
if config_value('SEND_PASSWORD_CHANGE_EMAIL'):
send_mail(config_value('EMAIL_SUBJECT_PASSWORD_CHANGE_NOTICE'), user.email,
'change_notice', user=user)
+2 -2
View File
@@ -11,7 +11,7 @@
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 .utils import send_mail, md5, url_for_security, get_token_status,\
@@ -58,7 +58,7 @@ def generate_confirmation_token(user):
def requires_confirmation(user):
"""Returns `True` if the user requires confirmation."""
return _security.confirmable and user.confirmed_at == None
return _security.confirmable and not _security.login_without_confirmation and user.confirmed_at == None
def confirm_email_token_status(token):
+16 -8
View File
@@ -19,7 +19,7 @@ from passlib.context import CryptContext
from werkzeug.datastructures import ImmutableList
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, string_types
from .views import create_blueprint
from .forms import LoginForm, ConfirmRegisterForm, RegisterForm, \
ForgotPasswordForm, ChangePasswordForm, ResetPasswordForm, \
@@ -55,6 +55,7 @@ _default_config = {
'LOGIN_USER_TEMPLATE': 'security/login_user.html',
'REGISTER_USER_TEMPLATE': 'security/register_user.html',
'RESET_PASSWORD_TEMPLATE': 'security/reset_password.html',
'CHANGE_PASSWORD_TEMPLATE': 'security/change_password.html',
'SEND_CONFIRMATION_TEMPLATE': 'security/send_confirmation.html',
'SEND_LOGIN_TEMPLATE': 'security/send_login.html',
'CONFIRMABLE': False,
@@ -64,6 +65,8 @@ _default_config = {
'PASSWORDLESS': False,
'CHANGEABLE': False,
'SEND_REGISTER_EMAIL': True,
'SEND_PASSWORD_CHANGE_EMAIL': True,
'SEND_PASSWORD_RESET_NOTICE_EMAIL': True,
'LOGIN_WITHIN': '1 days',
'CONFIRM_EMAIL_WITHIN': '5 days',
'RESET_PASSWORD_WITHIN': '5 days',
@@ -83,7 +86,8 @@ _default_config = {
'EMAIL_SUBJECT_PASSWORDLESS': 'Login instructions',
'EMAIL_SUBJECT_PASSWORD_NOTICE': 'Your password has been reset',
'EMAIL_SUBJECT_PASSWORD_CHANGE_NOTICE': 'Your password has been changed',
'EMAIL_SUBJECT_PASSWORD_RESET': 'Password reset instructions'
'EMAIL_SUBJECT_PASSWORD_RESET': 'Password reset instructions',
'USER_IDENTITY_ATTRIBUTES': ['email']
}
#: Default Flask-Security messages
@@ -110,10 +114,13 @@ _default_messages = {
'EMAIL_NOT_PROVIDED': ('Email not provided', 'error'),
'INVALID_EMAIL_ADDRESS': ('Invalid email address', 'error'),
'PASSWORD_NOT_PROVIDED': ('Password not provided', 'error'),
'PASSWORD_NOT_SET': ('No password is set for this user', 'error'),
'PASSWORD_INVALID_LENGTH': ('Password must be at least 6 characters', 'error'),
'USER_DOES_NOT_EXIST': ('Specified user does not exist', 'error'),
'INVALID_PASSWORD': ('Invalid password', 'error'),
'PASSWORDLESS_LOGIN_SUCCESSFUL': ('You have successfuly logged in.', 'success'),
'PASSWORD_RESET': ('You successfully reset your password and you have been logged in automatically.', 'success'),
'PASSWORD_IS_THE_SAME': ('Your new password must be different than your previous password.', 'error'),
'PASSWORD_CHANGE': ('You successfully changed your password.', 'success'),
'LOGIN': ('Please log in to access this page.', 'info'),
'REFRESH': ('Please reauthenticate to access this page.', 'info'),
@@ -242,8 +249,9 @@ def _context_processor():
class RoleMixin(object):
"""Mixin for `Role` model definitions"""
def __eq__(self, other):
return (self.name == other or \
return (self.name == other or
self.name == getattr(other, 'name', None))
def __ne__(self, other):
@@ -266,7 +274,7 @@ class UserMixin(BaseUserMixin):
"""Returns `True` if the user identifies with the specified role.
:param role: A role name or `Role` instance"""
if isinstance(role, basestring):
if isinstance(role, string_types):
return role in (role.name for role in self.roles)
else:
return role in self.roles
@@ -345,10 +353,10 @@ class Security(object):
self._state = self.init_app(app, datastore, **kwargs)
def init_app(self, app, datastore=None, register_blueprint=True,
login_form=None, confirm_register_form=None,
register_form=None, forgot_password_form=None,
reset_password_form=None, change_password_form=None,
send_confirmation_form=None, passwordless_login_form=None):
login_form=None, confirm_register_form=None,
register_form=None, forgot_password_form=None,
reset_password_form=None, change_password_form=None,
send_confirmation_form=None, passwordless_login_form=None):
"""Initializes the Flask-Security extension for the specified
application and datastore implentation.
+71 -7
View File
@@ -9,6 +9,9 @@
:license: MIT, see LICENSE for more details.
"""
from .utils import get_identity_attributes, string_types
class Datastore(object):
def __init__(self, db):
self.db = db
@@ -65,9 +68,9 @@ class UserDatastore(object):
self.role_model = role_model
def _prepare_role_modify_args(self, user, role):
if isinstance(user, basestring):
if isinstance(user, string_types):
user = self.find_user(email=user)
if isinstance(role, basestring):
if isinstance(role, string_types):
role = self.find_role(role)
return user, role
@@ -81,6 +84,10 @@ class UserDatastore(object):
kwargs['roles'] = roles
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):
"""Returns a user matching the provided parameters."""
raise NotImplementedError
@@ -98,6 +105,7 @@ class UserDatastore(object):
user, role = self._prepare_role_modify_args(user, role)
if role not in user.roles:
user.roles.append(role)
self.put(user)
return True
return False
@@ -154,8 +162,8 @@ class UserDatastore(object):
def create_user(self, **kwargs):
"""Creates and returns a new user from the given parameters."""
user = self.user_model(**self._prepare_create_user_args(**kwargs))
kwargs = self._prepare_create_user_args(**kwargs)
user = self.user_model(**kwargs)
return self.put(user)
def delete_user(self, user):
@@ -174,6 +182,22 @@ class SQLAlchemyUserDatastore(SQLAlchemyDatastore, UserDatastore):
SQLAlchemyDatastore.__init__(self, db)
UserDatastore.__init__(self, user_model, role_model)
def get_user(self, identifier):
if self._is_numeric(identifier):
return self.user_model.query.get(identifier)
for attr in get_identity_attributes():
query = getattr(self.user_model, attr).ilike(identifier)
rv = self.user_model.query.filter(query).first()
if rv is not None:
return rv
def _is_numeric(self, value):
try:
int(value)
except ValueError:
return False
return True
def find_user(self, **kwargs):
return self.user_model.query.filter_by(**kwargs).first()
@@ -189,19 +213,42 @@ class MongoEngineUserDatastore(MongoEngineDatastore, UserDatastore):
MongoEngineDatastore.__init__(self, db)
UserDatastore.__init__(self, user_model, role_model)
def get_user(self, identifier):
from mongoengine import ValidationError
try:
return self.user_model.objects(id=identifier).first()
except ValidationError:
pass
for attr in get_identity_attributes():
query_key = '%s__iexact' % attr
query = {query_key: identifier}
rv = self.user_model.objects(**query).first()
if rv is not None:
return rv
def find_user(self, **kwargs):
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)
return self.user_model.objects(query).first()
try:
return self.user_model.objects(query).first()
except ValidationError:
return None
def find_role(self, role):
return self.role_model.objects(name=role).first()
def add_role_to_user(self, user, role):
rv = super(MongoEngineUserDatastore, self).add_role_to_user(user, role)
if rv:
self.put(user)
return rv
class PeeweeUserDatastore(PeeweeDatastore, UserDatastore):
"""A PeeweeD datastore implementation for Flask-Security that assumes
@@ -216,6 +263,19 @@ class PeeweeUserDatastore(PeeweeDatastore, UserDatastore):
UserDatastore.__init__(self, user_model, role_model)
self.UserRole = role_link
def get_user(self, identifier):
try:
return self.user_model.get(self.user_model.id == identifier)
except ValueError:
pass
for attr in get_identity_attributes():
column = getattr(self.user_model, attr)
try:
return self.user_model.get(column ** identifier)
except self.user_model.DoesNotExist:
pass
def find_user(self, **kwargs):
try:
return self.user_model.filter(**kwargs).get()
@@ -223,7 +283,10 @@ class PeeweeUserDatastore(PeeweeDatastore, UserDatastore):
return None
def find_role(self, role):
return self.role_model.filter(name=role).get()
try:
return self.role_model.filter(name=role).get()
except self.role_model.DoesNotExist:
return None
def create_user(self, **kwargs):
"""Creates and returns a new user from the given parameters."""
@@ -259,8 +322,9 @@ class PeeweeUserDatastore(PeeweeDatastore, UserDatastore):
result = self.UserRole.select() \
.where(self.UserRole.user == user, self.UserRole.role == role)
if result.count():
self.UserRole.delete().where(
query = self.UserRole.delete().where(
self.UserRole.user == user, self.UserRole.role == role)
query.execute()
return True
else:
return False
+5 -9
View File
@@ -51,23 +51,19 @@ def _check_token():
args_key = _security.token_authentication_key
header_token = request.headers.get(header_key, None)
token = request.args.get(args_key, header_token)
if request.json:
if request.get_json(silent=True):
token = request.json.get(args_key, token)
serializer = _security.remember_token_serializer
try:
data = serializer.loads(token)
except:
return False
user = _security.login_manager.token_callback(token)
user = _security.datastore.find_user(id=data[0])
if utils.md5(user.password) == data[1]:
if user and user.is_authenticated():
app = current_app._get_current_object()
_request_ctx_stack.top.user = user
identity_changed.send(app, identity=Identity(user.id))
return True
return False
def _check_http_auth():
auth = request.authorization or BasicAuth(username=None, password=None)
+46 -35
View File
@@ -10,18 +10,20 @@
"""
import inspect
import urlparse
import flask_wtf as wtf
try:
from urlparse import urlsplit
except ImportError:
from urllib.parse import urlsplit
from flask import request, current_app
from flask_wtf import Form as BaseForm, TextField, PasswordField, \
from flask_wtf import Form as BaseForm
from wtforms import TextField, PasswordField, validators, \
SubmitField, HiddenField, BooleanField, ValidationError, Field
from flask_login import current_user
from werkzeug.local import LocalProxy
from .confirmable import requires_confirmation
from .utils import verify_and_update_password, get_message
from .utils import verify_and_update_password, get_message, config_value
# Convenient reference
_datastore = LocalProxy(lambda: current_app.extensions['security'].datastore)
@@ -50,25 +52,26 @@ class ValidatorMixin(object):
return super(ValidatorMixin, self).__call__(form, field)
class EqualTo(ValidatorMixin, wtf.EqualTo):
class EqualTo(ValidatorMixin, validators.EqualTo):
pass
class Required(ValidatorMixin, wtf.Required):
class Required(ValidatorMixin, validators.Required):
pass
class Email(ValidatorMixin, wtf.Email):
class Email(ValidatorMixin, validators.Email):
pass
class Length(ValidatorMixin, wtf.Length):
class Length(ValidatorMixin, validators.Length):
pass
email_required = Required(message='EMAIL_NOT_PROVIDED')
email_validator = Email(message='INVALID_EMAIL_ADDRESS')
password_required = Required(message='PASSWORD_NOT_PROVIDED')
password_length = Length(min=6, max=128, message='PASSWORD_INVALID_LENGTH')
def get_form_field_label(key):
@@ -95,35 +98,33 @@ class Form(BaseForm):
class EmailFormMixin():
email = TextField(get_form_field_label('email'),
validators=[email_required,
email_validator])
email = TextField(
get_form_field_label('email'),
validators=[email_required, email_validator])
class UserEmailFormMixin():
user = None
email = TextField(get_form_field_label('email'),
validators=[email_required,
email_validator,
valid_user_email])
email = TextField(
get_form_field_label('email'),
validators=[email_required, email_validator, valid_user_email])
class UniqueEmailFormMixin():
email = TextField(get_form_field_label('email'),
validators=[email_required,
email_validator,
unique_user_email])
email = TextField(
get_form_field_label('email'),
validators=[email_required, email_validator, unique_user_email])
class PasswordFormMixin():
password = PasswordField(get_form_field_label('password'),
validators=[password_required])
password = PasswordField(
get_form_field_label('password'), validators=[password_required])
class NewPasswordFormMixin():
password = PasswordField(get_form_field_label('password'),
validators=[password_required,
Length(min=6, max=128)])
password = PasswordField(
get_form_field_label('password'),
validators=[password_required, password_length])
class PasswordConfirmFormMixin():
@@ -136,11 +137,12 @@ class NextFormMixin():
next = HiddenField()
def validate_next(self, field):
url_next = urlparse.urlsplit(field.data)
url_base = urlparse.urlsplit(request.host_url)
if url_next.netloc and url_next.netloc != url_base.netloc:
field.data = ''
raise ValidationError(get_message('INVALID_REDIRECT')[0])
if field.data:
url_next = urlsplit(field.data)
url_base = urlsplit(request.host_url)
if url_next.netloc and url_next.netloc != url_base.netloc:
field.data = ''
raise ValidationError(get_message('INVALID_REDIRECT')[0])
class RegisterFormMixin():
@@ -207,6 +209,7 @@ class LoginForm(Form, NextFormMixin):
def __init__(self, *args, **kwargs):
super(LoginForm, self).__init__(*args, **kwargs)
self.remember.default = config_value('DEFAULT_REMEMBER_ME')
def validate(self):
if not super(LoginForm, self).validate():
@@ -220,11 +223,15 @@ class LoginForm(Form, NextFormMixin):
self.password.errors.append(get_message('PASSWORD_NOT_PROVIDED')[0])
return False
self.user = _datastore.find_user(email=self.email.data)
self.user = _datastore.get_user(self.email.data)
if self.user is None:
self.email.errors.append(get_message('USER_DOES_NOT_EXIST')[0])
return False
if not self.user.password:
self.password.errors.append(get_message('PASSWORD_NOT_SET')[0])
return False
if not verify_and_update_password(self.password.data, self.user):
self.password.errors.append(get_message('INVALID_PASSWORD')[0])
return False
@@ -255,11 +262,12 @@ class ResetPasswordForm(Form, NewPasswordFormMixin, PasswordConfirmFormMixin):
class ChangePasswordForm(Form, PasswordFormMixin):
"""The default change password form"""
new_password = PasswordField(get_form_field_label('new_password'),
validators=[password_required,
Length(min=6, max=128)])
new_password = PasswordField(
get_form_field_label('new_password'),
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')])
submit = SubmitField(get_form_field_label('change_password'))
@@ -274,4 +282,7 @@ class ChangePasswordForm(Form, PasswordFormMixin):
if not verify_and_update_password(self.password.data, current_user):
self.password.errors.append(get_message('INVALID_PASSWORD')[0])
return False
if self.password.data.strip() == self.new_password.data.strip():
self.password.errors.append(get_message('PASSWORD_IS_THE_SAME')[0])
return False
return True
+1 -1
View File
@@ -9,7 +9,7 @@
: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 .signals import login_instructions_sent
+5 -3
View File
@@ -9,7 +9,7 @@
: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 .signals import password_reset, reset_password_instructions_sent
@@ -44,8 +44,9 @@ def send_password_reset_notice(user):
:param user: The user to send the notice to
"""
send_mail(config_value('EMAIL_SUBJECT_PASSWORD_NOTICE'), user.email,
'reset_notice', user=user)
if config_value('SEND_PASSWORD_RESET_NOTICE_EMAIL'):
send_mail(config_value('EMAIL_SUBJECT_PASSWORD_NOTICE'), user.email,
'reset_notice', user=user)
def generate_reset_password_token(user):
@@ -67,6 +68,7 @@ def reset_password_token_status(token):
"""
return get_token_status(token, 'reset', 'RESET_PASSWORD')
def update_password(user, password):
"""Update the specified user's password
+1 -1
View File
@@ -38,6 +38,6 @@ def register_user(**kwargs):
if config_value('SEND_REGISTER_EMAIL'):
send_mail(config_value('EMAIL_SUBJECT_REGISTER'), user.email, 'welcome',
user=user, confirmation_link=confirmation_link)
user=user, confirmation_link=confirmation_link)
return user
-1
View File
@@ -11,7 +11,6 @@
import blinker
signals = blinker.Namespace()
user_registered = signals.signal("user-registered")
+73 -19
View File
@@ -14,6 +14,7 @@ import blinker
import functools
import hashlib
import hmac
import sys
from contextlib import contextmanager
from datetime import datetime, timedelta
@@ -37,9 +38,22 @@ _datastore = LocalProxy(lambda: _security.datastore)
_pwd_context = LocalProxy(lambda: _security.pwd_context)
PY3 = sys.version_info[0] == 3
if PY3:
string_types = str,
text_type = str
else:
string_types = basestring,
text_type = unicode
def login_user(user, remember=None):
"""Performs the login and sends the appropriate signal."""
"""Performs the login routine.
:param user: The user to login
:param remember: Flag specifying if the remember cookie should be set. Defaults to ``False``
"""
if remember is None:
remember = config_value('DEFAULT_REMEMBER_ME')
@@ -66,6 +80,8 @@ def login_user(user, remember=None):
def logout_user():
"""Logs out the current. This will also clean up the remember me cookie if it exists."""
for key in ('identity.name', 'identity.auth_type'):
session.pop(key, None)
identity_changed.send(current_app._get_current_object(),
@@ -74,24 +90,41 @@ def logout_user():
def get_hmac(password):
if _security.password_hash == 'plaintext':
return password
"""Returns a Base64 encoded HMAC+SHA512 of the password signed with the salt specified
by ``SECURITY_PASSWORD_SALT``.
:param password: The password to sign
"""
if _security.password_salt is None:
raise RuntimeError('The configuration value `SECURITY_PASSWORD_SALT` '
'must not be None when the value of `SECURITY_PASSWORD_HASH` is '
raise RuntimeError(
'The configuration value `SECURITY_PASSWORD_SALT` must '
'not be None when the value of `SECURITY_PASSWORD_HASH` is '
'set to "%s"' % _security.password_hash)
h = hmac.new(_security.password_salt, password.encode('utf-8'), hashlib.sha512)
h = hmac.new(_security.password_salt.encode('utf-8'), password.encode('utf-8'), hashlib.sha512)
return base64.b64encode(h.digest())
def verify_password(password, password_hash):
return _pwd_context.verify(get_hmac(password), password_hash)
"""Returns ``True`` if the password matches the supplied hash.
:param password: A plaintext password to verify
:param password_hash: The expected hash value of the password (usually form your database)
"""
return _pwd_context.verify(encrypt_password(password), password_hash)
def verify_and_update_password(password, user):
verified, new_password = _pwd_context.verify_and_update(get_hmac(password), user.password)
"""Returns ``True`` if the password is valid for the specified user. Additionally, the hashed
password in the database is updated if the hashing algorithm happens to have changed.
:param password: A plaintext password to verify
:param user: The user to verify against
"""
if _security.password_hash != 'plaintext':
password = get_hmac(password)
verified, new_password = _pwd_context.verify_and_update(password, user.password)
if verified and new_password:
user.password = new_password
_datastore.put(user)
@@ -99,11 +132,18 @@ def verify_and_update_password(password, user):
def encrypt_password(password):
return _pwd_context.encrypt(get_hmac(password))
"""Encrypts the specified plaintext password using the configured encryption options.
:param password: The plaintext passwrod to encrypt
"""
if _security.password_hash == 'plaintext':
return password
signed = get_hmac(password).decode('ascii')
return _pwd_context.encrypt(signed)
def md5(data):
return hashlib.md5(data).hexdigest()
return hashlib.md5(data.encode('ascii')).hexdigest()
def do_flash(message, category=None):
@@ -205,10 +245,8 @@ def config_value(key, app=None, default=None):
def get_max_age(key, app=None):
now = datetime.utcnow()
expires = now + get_within_delta(key + '_WITHIN', app)
td = (expires - now)
return (td.microseconds + (td.seconds + td.days * 24 * 3600) * 1e6) / 1e6
td = get_within_delta(key + '_WITHIN', app)
return td.seconds + td.days * 24 * 3600
def get_within_delta(key, app=None):
@@ -260,6 +298,14 @@ def send_mail(subject, recipient, template, **context):
def get_token_status(token, serializer, max_age=None):
"""Get the status of a token.
:param token: The token to check
:param serializer: The name of the seriailzer. Can be one of the
following: ``confirm``, ``login``, ``reset``
:param max_age: The name of the max age config option. Can be on of
the following: ``CONFIRM_EMAIL``, ``LOGIN``, ``RESET_PASSWORD``
"""
serializer = getattr(_security, serializer + '_serializer')
max_age = get_max_age(max_age)
user, data = None, None
@@ -280,6 +326,16 @@ def get_token_status(token, serializer, max_age=None):
return expired, invalid, user
def get_identity_attributes(app=None):
app = app or current_app
attrs = app.config['SECURITY_USER_IDENTITY_ATTRIBUTES']
try:
attrs = [f.strip() for f in attrs.split(',')]
except AttributeError:
pass
return attrs
@contextmanager
def capture_passwordless_login_requests():
login_requests = []
@@ -365,19 +421,19 @@ class CaptureSignals(object):
self._records[signal].append((args, kwargs))
def __enter__(self):
for signal, receiver in self._receivers.iteritems():
for signal, receiver in self._receivers.items():
signal.connect(receiver)
return self
def __exit__(self, type, value, traceback):
for signal, receiver in self._receivers.iteritems():
for signal, receiver in self._receivers.items():
signal.disconnect(receiver)
def signals_sent(self):
"""Return a set of the signals sent.
:rtype: list of blinker `NamedSignals`.
"""
return set([signal for signal, _ in self._records.iteritems() if self._records[signal]])
return set([signal for signal, _ in self._records.items() if self._records[signal]])
def capture_signals():
@@ -386,5 +442,3 @@ def capture_signals():
confirm_instructions_sent, login_instructions_sent,
password_reset, password_changed,
reset_password_instructions_sent])
+7 -3
View File
@@ -123,6 +123,7 @@ def register():
if not request.json:
return redirect(get_post_register_redirect())
return _render_json(form, True)
if request.json:
return _render_json(form)
@@ -200,7 +201,6 @@ def send_confirmation():
**_ctx('send_confirmation'))
@anonymous_user_required
def confirm_email(token):
"""View function which handles a email confirmation request."""
@@ -217,8 +217,11 @@ def confirm_email(token):
return redirect(get_url(_security.confirm_error_view) or
url_for('send_confirmation'))
if user != current_user:
logout_user()
login_user(user)
confirm_user(user)
login_user(user)
after_this_request(_commit)
do_flash(*get_message('EMAIL_CONFIRMED'))
@@ -299,9 +302,10 @@ def change_password():
get_url(_security.post_login_view))
if request.json:
form.user = current_user
return _render_json(form)
return render_template('security/change_password.html',
return render_template(config_value('CHANGE_PASSWORD_TEMPLATE'),
change_password_form=form,
**_ctx('change_password'))
+8 -9
View File
@@ -20,7 +20,7 @@ from setuptools import setup
setup(
name='Flask-Security',
version='1.6.5',
version='1.7.1',
url='https://github.com/mattupstate/flask-security',
license='MIT',
author='Matt Wright',
@@ -34,13 +34,12 @@ setup(
include_package_data=True,
platforms='any',
install_requires=[
'Flask>=0.9',
'Flask-Login==0.2.3',
'Flask-Mail==0.7.3',
'Flask-Principal==0.3.3',
'Flask-WTF==0.8',
'itsdangerous>=0.17',
'passlib>=1.6.1',
'Flask>=0.10.1',
'Flask-Login>=0.2.9',
'Flask-Mail>=0.9.0',
'Flask-Principal>=0.4.0',
'Flask-WTF>=0.9.3',
'passlib>=1.6.2',
],
test_suite='nose.collector',
tests_require=[
@@ -48,7 +47,7 @@ setup(
'Flask-SQLAlchemy',
'Flask-MongoEngine',
'Flask-Peewee',
'py-bcrypt',
'bcrypt',
'simplejson'
],
classifiers=[
+10 -20
View File
@@ -22,36 +22,27 @@ class SecurityTest(TestCase):
app = self._create_app(self.AUTH_CONFIG or {}, **app_kwargs)
app.debug = False
app.config['TESTING'] = True
app.config['WTF_CSRF_ENABLED'] = False
self.app = app
self.client = app.test_client()
with self.client.session_transaction() as session:
session['csrf'] = 'csrf_token'
csrf_hmac = hmac.new(self.app.config['SECRET_KEY'],
'csrf_token'.encode('utf8'), digestmod=sha1)
self.csrf_token = '##' + csrf_hmac.hexdigest()
def _create_app(self, auth_config, **kwargs):
return create_app(auth_config, **kwargs)
def _get(self, route, content_type=None, follow_redirects=None, headers=None):
return self.client.get(route, follow_redirects=follow_redirects,
content_type=content_type or 'text/html',
headers=headers)
content_type=content_type or 'text/html',
headers=headers)
def _post(self, route, data=None, content_type=None, follow_redirects=True, headers=None):
if isinstance(data, dict):
data['csrf_token'] = self.csrf_token
content_type = content_type or 'application/x-www-form-urlencoded'
return self.client.post(route, data=data,
follow_redirects=follow_redirects,
content_type=content_type or 'application/x-www-form-urlencoded',
headers=headers)
follow_redirects=follow_redirects,
content_type=content_type, headers=headers)
def register(self, email, password='password'):
data = dict(email=email, password=password, csrf_token=self.csrf_token)
data = dict(email=email, password=password)
return self.client.post('/register', data=data, follow_redirects=True)
def authenticate(self, email="matt@lp.com", password="password", endpoint=None, **kwargs):
@@ -61,17 +52,16 @@ class SecurityTest(TestCase):
def json_authenticate(self, email="matt@lp.com", password="password", endpoint=None):
data = """{
"email": "%s",
"password": "%s",
"csrf_token": "%s"
"password": "%s"
}"""
return self._post(endpoint or '/login', content_type="application/json",
data=data % (email, password, self.csrf_token))
data=data % (email, password))
def logout(self, endpoint=None):
return self._get(endpoint or '/logout', follow_redirects=True)
def assertIsHomePage(self, data):
self.assertIn('Home Page', data)
self.assertIn(b'Home Page', data)
def assertIn(self, member, container, msg=None):
if hasattr(TestCase, 'assertIn'):
+175 -102
View File
@@ -1,17 +1,20 @@
# -*- coding: utf-8 -*-
from __future__ import with_statement
# from __future__ import with_statement
import base64
import time
import simplejson as json
import flask
from flask.ext.security.utils import capture_registrations, \
from flask_security.utils import capture_registrations, \
capture_reset_password_requests, capture_passwordless_login_requests
from flask.ext.security.forms import LoginForm, ConfirmRegisterForm, RegisterForm, \
from flask_security.forms import LoginForm, ConfirmRegisterForm, RegisterForm, \
ForgotPasswordForm, ResetPasswordForm, SendConfirmationForm, \
PasswordlessLoginForm
from flask.ext.security.forms import TextField, SubmitField, valid_user_email
from flask_security.forms import TextField, SubmitField, valid_user_email
from flask_security.signals import user_registered
from tests import SecurityTest
@@ -27,7 +30,7 @@ class ConfiguredPasswordHashSecurityTests(SecurityTest):
def test_authenticate(self):
r = self.authenticate(endpoint="/login")
self.assertIn('Home Page', r.data)
self.assertIn(b'Home Page', r.data)
class ConfiguredSecurityTests(SecurityTest):
@@ -45,20 +48,20 @@ class ConfiguredSecurityTests(SecurityTest):
def test_login_view(self):
r = self._get('/custom_login')
self.assertIn("<h1>Login</h1>", r.data)
self.assertIn(b"<h1>Login</h1>", r.data)
def test_authenticate(self):
r = self.authenticate(endpoint="/custom_login")
self.assertIn('Post Login', r.data)
self.assertIn(b'Post Login', r.data)
def test_logout(self):
self.authenticate(endpoint="/custom_login")
r = self.logout(endpoint="/custom_logout")
self.assertIn('Post Logout', r.data)
self.assertIn(b'Post Logout', r.data)
def test_register_view(self):
r = self._get('/register')
self.assertIn('<h1>Register</h1>', r.data)
self.assertIn(b'<h1>Register</h1>', r.data)
def test_register(self):
data = dict(email='dude@lp.com',
@@ -66,7 +69,7 @@ class ConfiguredSecurityTests(SecurityTest):
password_confirm='password')
r = self._post('/register', data=data, follow_redirects=True)
self.assertIn('Post Register', r.data)
self.assertIn(b'Post Register', r.data)
def test_register_with_next_querystring_argument(self):
data = dict(email='dude@lp.com',
@@ -74,10 +77,10 @@ class ConfiguredSecurityTests(SecurityTest):
password_confirm='password')
r = self._post('/register?next=/page1', data=data, follow_redirects=True)
self.assertIn('Page 1', r.data)
self.assertIn(b'Page 1', r.data)
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"}'
r = self._post('/register', data=data, content_type='application/json')
data = json.loads(r.data)
self.assertEquals(data['meta']['code'], 200)
@@ -87,20 +90,20 @@ class ConfiguredSecurityTests(SecurityTest):
password='password',
password_confirm='password')
r = self._post('/register', data=data, follow_redirects=True)
msg = 'matt@lp.com is already associated with an account'
msg = b'matt@lp.com is already associated with an account'
self.assertIn(msg, r.data)
def test_unauthorized(self):
self.authenticate("joe@lp.com", endpoint="/custom_auth")
r = self._get("/admin", follow_redirects=True)
msg = 'You are not allowed to access the requested resouce'
msg = b'You are not allowed to access the requested resouce'
self.assertIn(msg, r.data)
def test_default_http_auth_realm(self):
r = self._get('/http', headers={
'Authorization': 'Basic ' + base64.b64encode("joe@lp.com:bogus")
'Authorization': 'Basic %s' % base64.b64encode(b"joe@lp.com:bogus")
})
self.assertIn('<h1>Unauthorized</h1>', r.data)
self.assertIn(b'<h1>Unauthorized</h1>', r.data)
self.assertIn('WWW-Authenticate', r.headers)
self.assertEquals('Basic realm="Custom Realm"',
r.headers['WWW-Authenticate'])
@@ -122,11 +125,10 @@ class DefaultTemplatePathTests(SecurityTest):
'SECURITY_LOGIN_USER_TEMPLATE': 'custom_security/login_user.html',
}
def test_login_user_template(self):
r = self._get('/login')
self.assertIn('CUSTOM LOGIN USER', r.data)
self.assertIn(b'CUSTOM LOGIN USER', r.data)
class RegisterableTemplatePathTests(SecurityTest):
@@ -138,7 +140,7 @@ class RegisterableTemplatePathTests(SecurityTest):
def test_register_user_template(self):
r = self._get('/register')
self.assertIn('CUSTOM REGISTER USER', r.data)
self.assertIn(b'CUSTOM REGISTER USER', r.data)
class RecoverableTemplatePathTests(SecurityTest):
@@ -151,19 +153,18 @@ class RecoverableTemplatePathTests(SecurityTest):
def test_forgot_password_template(self):
r = self._get('/reset')
self.assertIn('CUSTOM FORGOT PASSWORD', r.data)
self.assertIn(b'CUSTOM FORGOT PASSWORD', r.data)
def test_reset_password_template(self):
with capture_reset_password_requests() as requests:
r = self._post('/reset',
data=dict(email='joe@lp.com'),
follow_redirects=True)
r = self._post('/reset', data=dict(email='joe@lp.com'),
follow_redirects=True)
t = requests[0]['token']
r = self._get('/reset/' + t)
self.assertIn('CUSTOM RESET PASSWORD', r.data)
self.assertIn(b'CUSTOM RESET PASSWORD', r.data)
class ConfirmableTemplatePathTests(SecurityTest):
@@ -175,7 +176,7 @@ class ConfirmableTemplatePathTests(SecurityTest):
def test_send_confirmation_template(self):
r = self._get('/confirm')
self.assertIn('CUSTOM SEND CONFIRMATION', r.data)
self.assertIn(b'CUSTOM SEND CONFIRMATION', r.data)
class PasswordlessTemplatePathTests(SecurityTest):
@@ -187,7 +188,7 @@ class PasswordlessTemplatePathTests(SecurityTest):
def test_send_login_template(self):
r = self._get('/login')
self.assertIn('CUSTOM SEND LOGIN', r.data)
self.assertIn(b'CUSTOM SEND LOGIN', r.data)
class RegisterableTests(SecurityTest):
@@ -202,7 +203,7 @@ class RegisterableTests(SecurityTest):
password_confirm='password')
self._post('/register', data=data, follow_redirects=True)
r = self.authenticate('dude@lp.com')
self.assertIn('Hello dude@lp.com', r.data)
self.assertIn(b'Hello dude@lp.com', r.data)
class ConfirmableTests(SecurityTest):
@@ -217,19 +218,20 @@ class ConfirmableTests(SecurityTest):
e = 'dude@lp.com'
self.register(e)
r = self.authenticate(email=e)
self.assertIn(self.get_message('CONFIRMATION_REQUIRED'), r.data)
self.assertIn(self.get_message('CONFIRMATION_REQUIRED').encode('utf-8'), r.data)
def test_send_confirmation_of_already_confirmed_account(self):
e = 'dude@lp.com'
with capture_registrations() as registrations:
self.register(e)
r = self.register(e)
token = registrations[0]['confirm_token']
self.client.get('/confirm/' + token, follow_redirects=True)
self.logout()
r = self._post('/confirm', data=dict(email=e))
self.assertIn(self.get_message('ALREADY_CONFIRMED'), r.data)
m = self.get_message('ALREADY_CONFIRMED')
self.assertIn(m.encode('utf-8'), r.data)
def test_register_sends_confirmation_email(self):
e = 'dude@lp.com'
@@ -242,19 +244,22 @@ class ConfirmableTests(SecurityTest):
def test_confirm_email(self):
e = 'dude@lp.com'
with capture_registrations() as registrations:
self.register(e)
token = registrations[0]['confirm_token']
tokens = []
def on_registered(sender, **kwargs):
tokens.append(kwargs['confirm_token'])
r = self.client.get('/confirm/' + token, follow_redirects=True)
user_registered.connect(on_registered, self.app)
r = self.register(e)
self.assertEqual(len(tokens), 1)
r = self.client.get('/confirm/' + tokens[0], follow_redirects=True)
msg = self.app.config['SECURITY_MSG_EMAIL_CONFIRMED'][0]
self.assertIn(msg, r.data)
self.assertIn(msg.encode('utf-8'), r.data)
def test_invalid_token_when_confirming_email(self):
r = self.client.get('/confirm/bogus', follow_redirects=True)
msg = self.app.config['SECURITY_MSG_INVALID_CONFIRMATION_TOKEN'][0]
self.assertIn(msg, r.data)
self.assertIn(msg.encode('utf-8'), r.data)
def test_send_confirmation_json(self):
r = self._post('/confirm', data='{"email": "matt@lp.com"}',
@@ -264,14 +269,14 @@ class ConfirmableTests(SecurityTest):
def test_send_confirmation_with_invalid_email(self):
r = self._post('/confirm', data=dict(email='bogus@bogus.com'))
msg = self.app.config['SECURITY_MSG_USER_DOES_NOT_EXIST'][0]
self.assertIn(msg, r.data)
self.assertIn(msg.encode('utf-8'), r.data)
def test_resend_confirmation(self):
e = 'dude@lp.com'
self.register(e)
r = self._post('/confirm', data={'email': e})
msg = self.get_message('CONFIRMATION_REQUEST', email=e)
msg = self.get_message('CONFIRMATION_REQUEST', email=e).encode('utf-8')
self.assertIn(msg, r.data)
def test_user_deleted_before_confirmation(self):
@@ -289,7 +294,7 @@ class ConfirmableTests(SecurityTest):
r = self.client.get('/confirm/' + token, follow_redirects=True)
msg = self.app.config['SECURITY_MSG_INVALID_CONFIRMATION_TOKEN'][0]
self.assertIn(msg, r.data)
self.assertIn(msg.encode('utf-8'), r.data)
class ExpiredConfirmationTest(SecurityTest):
@@ -318,7 +323,7 @@ class ExpiredConfirmationTest(SecurityTest):
expire_text = self.AUTH_CONFIG['SECURITY_CONFIRM_EMAIL_WITHIN']
msg = self.app.config['SECURITY_MSG_CONFIRMATION_EXPIRED'][0]
msg = msg % dict(within=expire_text, email=e)
self.assertIn(msg, r.data)
self.assertIn(msg.encode('utf-8'), r.data)
class LoginWithoutImmediateConfirmTests(SecurityTest):
@@ -334,7 +339,35 @@ class LoginWithoutImmediateConfirmTests(SecurityTest):
p = 'password'
data = dict(email=e, password=p, password_confirm=p)
r = self._post('/register', data=data, follow_redirects=True)
self.assertIn(e, r.data)
self.assertIn(e.encode('utf-8'), 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)
m = self.app.config['SECURITY_MSG_EMAIL_CONFIRMED'][0]
self.assertIn(m.encode('utf-8'), r.data)
self.assertIn(b'Hello lady@lp.com', r.data)
def test_login_unconfirmed_user_when_login_without_confirmation_is_true(self):
e = 'dude@lp.com'
p = 'password'
data = dict(email=e, password=p, password_confirm=p)
r = self._post('/register', data=data, follow_redirects=True)
self.assertIn(e.encode('utf-8'), r.data)
self.client.get('/logout')
r = self.authenticate(email=e)
self.assertIn(e.encode('utf-8'), r.data)
class RecoverableTests(SecurityTest):
@@ -347,12 +380,11 @@ class RecoverableTests(SecurityTest):
def test_reset_view(self):
with capture_reset_password_requests() as requests:
r = self._post('/reset',
data=dict(email='joe@lp.com'),
follow_redirects=True)
r = self._post('/reset', data=dict(email='joe@lp.com'),
follow_redirects=True)
t = requests[0]['token']
r = self._get('/reset/' + t)
self.assertIn('<h1>Reset password</h1>', r.data)
self.assertIn(b'<h1>Reset password</h1>', r.data)
def test_forgot_post_sends_email(self):
with capture_reset_password_requests():
@@ -362,20 +394,18 @@ class RecoverableTests(SecurityTest):
def test_forgot_password_json(self):
r = self._post('/reset', data='{"email": "matt@lp.com"}',
content_type="application/json")
content_type="application/json")
self.assertEquals(r.status_code, 200)
def test_forgot_password_invalid_email(self):
r = self._post('/reset',
data=dict(email='larry@lp.com'),
follow_redirects=True)
self.assertIn("Specified user does not exist", r.data)
r = self._post('/reset', data=dict(email='larry@lp.com'),
follow_redirects=True)
self.assertIn(b"Specified user does not exist", r.data)
def test_reset_password_with_valid_token(self):
with capture_reset_password_requests() as requests:
r = self._post('/reset',
data=dict(email='joe@lp.com'),
follow_redirects=True)
r = self._post('/reset', data=dict(email='joe@lp.com'),
follow_redirects=True)
t = requests[0]['token']
r = self._post('/reset/' + t, data={
@@ -385,15 +415,15 @@ class RecoverableTests(SecurityTest):
r = self.logout()
r = self.authenticate('joe@lp.com', 'newpassword')
self.assertIn('Hello joe@lp.com', r.data)
self.assertIn(b'Hello joe@lp.com', r.data)
def test_reset_password_with_invalid_token(self):
r = self._post('/reset/bogus', data={
'password': 'newpassword',
'password_confirm': 'newpassword'
}, follow_redirects=True)
self.assertIn(self.get_message('INVALID_RESET_PASSWORD_TOKEN'), r.data)
m = self.get_message('INVALID_RESET_PASSWORD_TOKEN')
self.assertIn(m.encode('utf-8'), r.data)
class ExpiredResetPasswordTest(SecurityTest):
@@ -416,7 +446,7 @@ class ExpiredResetPasswordTest(SecurityTest):
'password_confirm': 'newpassword'
}, follow_redirects=True)
self.assertIn('You did not reset your password within', r.data)
self.assertIn(b'You did not reset your password within', r.data)
class ChangePasswordTest(SecurityTest):
@@ -429,7 +459,7 @@ class ChangePasswordTest(SecurityTest):
def test_change_password(self):
self.authenticate()
r = self.client.get('/change', follow_redirects=True)
self.assertIn('Change password', r.data)
self.assertIn(b'Change password', r.data)
def test_change_password_invalid(self):
self.authenticate()
@@ -438,8 +468,8 @@ class ChangePasswordTest(SecurityTest):
'new_password': 'newpassword',
'new_password_confirm': 'newpassword'
}, follow_redirects=True)
self.assertNotIn('You successfully changed your password', r.data)
self.assertIn('Invalid password', r.data)
self.assertNotIn(b'You successfully changed your password', r.data)
self.assertIn(b'Invalid password', r.data)
def test_change_password_mismatch(self):
self.authenticate()
@@ -448,8 +478,8 @@ class ChangePasswordTest(SecurityTest):
'new_password': 'newpassword',
'new_password_confirm': 'notnewpassword'
}, follow_redirects=True)
self.assertNotIn('You successfully changed your password', r.data)
self.assertIn('Passwords do not match', r.data)
self.assertNotIn(b'You successfully changed your password', r.data)
self.assertIn(b'Passwords do not match', r.data)
def test_change_password_bad_password(self):
self.authenticate()
@@ -458,26 +488,60 @@ class ChangePasswordTest(SecurityTest):
'new_password': 'a',
'new_password_confirm': 'a'
}, follow_redirects=True)
self.assertNotIn('You successfully changed your password', r.data)
self.assertIn('Field must be between', r.data)
self.assertNotIn(b'You successfully changed your password', r.data)
self.assertIn(b'Password must be at least 6 characters', r.data)
def test_change_password_same_as_previous(self):
self.authenticate()
r = self._post('/change', data={
'password': 'password',
'new_password': 'password',
'new_password_confirm': 'password'
}, follow_redirects=True)
self.assertNotIn(b'You successfully changed your password', r.data)
self.assertIn(b'Your new password must be different than your previous password.', r.data)
def test_change_password_success(self):
data = {
'password': 'password',
'new_password': 'newpassword',
'new_password_confirm': 'newpassword'
}
self.authenticate()
with self.app.extensions['mail'].record_messages() as outbox:
r = self._post('/change', data={
'password': 'password',
'new_password': 'newpassword',
'new_password_confirm': 'newpassword'
}, follow_redirects=True)
r = self._post('/change', data=data, follow_redirects=True)
self.assertIn('You successfully changed your password', r.data)
self.assertIn('Home Page', r.data)
self.assertIn(b'You successfully changed your password', r.data)
self.assertIn(b'Home Page', r.data)
self.assertEqual(len(outbox), 1)
self.assertIn("Your password has been changed", outbox[0].html)
self.assertIn("/reset", outbox[0].html)
class EmailConfigTest(SecurityTest):
AUTH_CONFIG = {
'SECURITY_SEND_REGISTER_EMAIL': False,
'SECURITY_SEND_PASSWORD_CHANGE_EMAIL': False,
}
def test_change_password_success_email_option(self):
"""Test the change password email can be turned off w/ configuration."""
data = {
'password': 'password',
'new_password': 'newpassword',
'new_password_confirm': 'newpassword'
}
self.authenticate()
with self.app.extensions['mail'].record_messages() as outbox:
self._post('/change', data=data, follow_redirects=True)
self.assertEqual(len(outbox), 0)
class ChangePasswordPostViewTest(SecurityTest):
AUTH_CONFIG = {
@@ -486,14 +550,15 @@ class ChangePasswordPostViewTest(SecurityTest):
}
def test_change_password_success(self):
data = {
'password': 'password',
'new_password': 'newpassword',
'new_password_confirm': 'newpassword'
}
self.authenticate()
r = self._post('/change', data={
'password': 'password',
'new_password': 'newpassword',
'new_password_confirm': 'newpassword'
}, follow_redirects=True)
r = self._post('/change', data=data, follow_redirects=True)
self.assertIn('Profile Page', r.data)
self.assertIn(b'Profile Page', r.data)
class ChangePasswordDisabledTest(SecurityTest):
@@ -540,18 +605,18 @@ class PasswordlessTests(SecurityTest):
msg = self.app.config['SECURITY_MSG_DISABLED_ACCOUNT'][0]
r = self._post('/login', data=dict(email='tiya@lp.com'),
follow_redirects=True)
self.assertIn(msg, r.data)
self.assertIn(msg.encode('utf-8'), r.data)
def test_request_login_token_with_json_and_valid_email(self):
data = '{"email": "matt@lp.com", "password": "password", "csrf_token":"%s"}' % self.csrf_token
data = '{"email": "matt@lp.com", "password": "password"}'
r = self._post('/login', data=data, content_type='application/json')
self.assertEquals(r.status_code, 200)
self.assertNotIn('error', r.data)
self.assertNotIn(b'error', r.data)
def test_request_login_token_with_json_and_invalid_email(self):
data = '{"email": "nobody@lp.com", "password": "password"}'
r = self._post('/login', data=data, content_type='application/json')
self.assertIn('errors', r.data)
self.assertIn(b'errors', r.data)
def test_request_login_token_sends_email_and_can_login(self):
e = 'matt@lp.com'
@@ -573,19 +638,19 @@ class PasswordlessTests(SecurityTest):
msg = self.app.config['SECURITY_MSG_LOGIN_EMAIL_SENT'][0]
msg = msg % dict(email=user.email)
self.assertIn(msg, r.data)
self.assertIn(msg.encode('utf-8'), r.data)
r = self.client.get('/login/' + token, follow_redirects=True)
msg = self.get_message('PASSWORDLESS_LOGIN_SUCCESSFUL')
msg = self.get_message('PASSWORDLESS_LOGIN_SUCCESSFUL').encode('utf-8')
self.assertIn(msg, r.data)
r = self.client.get('/profile')
self.assertIn('Profile Page', r.data)
self.assertIn(b'Profile Page', r.data)
def test_invalid_login_token(self):
msg = self.app.config['SECURITY_MSG_INVALID_LOGIN_TOKEN'][0]
m = self.app.config['SECURITY_MSG_INVALID_LOGIN_TOKEN'][0]
r = self._get('/login/bogus', follow_redirects=True)
self.assertIn(msg, r.data)
self.assertIn(m.encode('utf-8'), r.data)
def test_token_login_when_already_authenticated(self):
with capture_passwordless_login_requests() as requests:
@@ -595,15 +660,15 @@ class PasswordlessTests(SecurityTest):
r = self.client.get('/login/' + token, follow_redirects=True)
msg = self.get_message('PASSWORDLESS_LOGIN_SUCCESSFUL')
self.assertIn(msg, r.data)
self.assertIn(msg.encode('utf-8'), r.data)
r = self.client.get('/login/' + token, follow_redirects=True)
msg = self.get_message('PASSWORDLESS_LOGIN_SUCCESSFUL')
self.assertNotIn(msg, r.data)
self.assertNotIn(msg.encode('utf-8'), r.data)
def test_send_login_with_invalid_email(self):
r = self._post('/login', data=dict(email='bogus@bogus.com'))
self.assertIn('Specified user does not exist', r.data)
self.assertIn(b'Specified user does not exist', r.data)
class ExpiredLoginTokenTests(SecurityTest):
@@ -629,8 +694,7 @@ class ExpiredLoginTokenTests(SecurityTest):
expire_text = self.AUTH_CONFIG['SECURITY_LOGIN_WITHIN']
msg = self.app.config['SECURITY_MSG_LOGIN_EXPIRED'][0]
msg = msg % dict(within=expire_text, email=e)
self.assertIn(msg, r.data)
self.assertIn(msg.encode('utf-8'), r.data)
self.assertEqual(len(outbox), 1)
self.assertIn(e, outbox[0].html)
self.assertNotIn(token, outbox[0].html)
@@ -671,9 +735,9 @@ class NoBlueprintTests(SecurityTest):
self.assertEqual(404, r.status_code)
def test_http_auth_without_blueprint(self):
auth = 'Basic ' + base64.b64encode("matt@lp.com:password")
r = self._get('/http', headers={'Authorization': auth})
self.assertIn('HTTP Authentication', r.data)
auth = base64.b64encode(b"matt@lp.com:password").decode('utf-8')
r = self._get('/http', headers={'Authorization': 'basic %s' % auth})
self.assertIn(b'HTTP Authentication', r.data)
class ExtendFormsTest(SecurityTest):
@@ -696,11 +760,11 @@ class ExtendFormsTest(SecurityTest):
def test_login_view(self):
r = self._get('/login', follow_redirects=True)
self.assertIn("My Login Email Address Field", r.data)
self.assertIn(b"My Login Email Address Field", r.data)
def test_register(self):
r = self._get('/register', follow_redirects=True)
self.assertIn("My Register Email Address Field", r.data)
self.assertIn(b"My Register Email Address Field", r.data)
class RecoverableExtendFormsTest(SecurityTest):
@@ -723,7 +787,7 @@ class RecoverableExtendFormsTest(SecurityTest):
def test_forgot_password(self):
r = self._get('/reset', follow_redirects=True)
self.assertIn("My Forgot Password Email Address Field", r.data)
self.assertIn(b"My Forgot Password Email Address Field", r.data)
def test_reset_password(self):
with capture_reset_password_requests() as requests:
@@ -731,7 +795,7 @@ class RecoverableExtendFormsTest(SecurityTest):
follow_redirects=True)
token = requests[0]['token']
r = self._get('/reset/' + token)
self.assertIn("My Reset Password Submit Field", r.data)
self.assertIn(b"My Reset Password Submit Field", r.data)
class PasswordlessExtendFormsTest(SecurityTest):
@@ -749,7 +813,7 @@ class PasswordlessExtendFormsTest(SecurityTest):
def test_passwordless_login(self):
r = self._get('/login', follow_redirects=True)
self.assertIn("My Passwordless Login Email Address Field", r.data)
self.assertIn(b"My Passwordless Login Email Address Field", r.data)
class ConfirmableExtendFormsTest(SecurityTest):
@@ -772,10 +836,19 @@ class ConfirmableExtendFormsTest(SecurityTest):
def test_register(self):
r = self._get('/register', follow_redirects=True)
self.assertIn("My Confirm Register Email Address Field", r.data)
self.assertIn(b"My Confirm Register Email Address Field", r.data)
def test_send_confirmation(self):
r = self._get('/confirm', follow_redirects=True)
self.assertIn("My Send Confirmation Email Address Field", r.data)
self.assertIn(b"My Send Confirmation Email Address Field", r.data)
class AdditionalUserIdentityAttributes(SecurityTest):
AUTH_CONFIG = {
'SECURITY_USER_IDENTITY_ATTRIBUTES': ('email', 'username')
}
def test_authenticate(self):
r = self.authenticate(email='matt')
self.assertIn(b'Hello matt@lp.com', r.data)
+47 -38
View File
@@ -4,7 +4,11 @@ from __future__ import with_statement
import base64
import simplejson as json
from cookielib import Cookie
try:
from cookielib import Cookie
except ImportError:
from http.cookiejar import Cookie
from werkzeug.utils import parse_cookie
@@ -27,31 +31,35 @@ class DefaultSecurityTests(SecurityTest):
def test_login_view(self):
r = self._get('/login')
self.assertIn('<h1>Login</h1>', r.data)
self.assertIn(b'<h1>Login</h1>', r.data)
def test_authenticate(self):
r = self.authenticate()
self.assertIn('Hello matt@lp.com', r.data)
self.assertIn(b'Hello matt@lp.com', r.data)
def test_authenticate_case_insensitive_email(self):
r = self.authenticate(email='MATT@lp.com')
self.assertIn(b'Hello matt@lp.com', r.data)
def test_unprovided_username(self):
r = self.authenticate("")
self.assertIn(self.get_message('EMAIL_NOT_PROVIDED'), r.data)
self.assertIn(self.get_message('EMAIL_NOT_PROVIDED').encode('utf-8'), r.data)
def test_unprovided_password(self):
r = self.authenticate(password="")
self.assertIn(self.get_message('PASSWORD_NOT_PROVIDED'), r.data)
self.assertIn(self.get_message('PASSWORD_NOT_PROVIDED').encode('utf-8'), r.data)
def test_invalid_user(self):
r = self.authenticate(email="bogus@bogus.com")
self.assertIn(self.get_message('USER_DOES_NOT_EXIST'), r.data)
self.assertIn(self.get_message('USER_DOES_NOT_EXIST').encode('utf-8'), r.data)
def test_bad_password(self):
r = self.authenticate(password="bogus")
self.assertIn(self.get_message('INVALID_PASSWORD'), r.data)
self.assertIn(self.get_message('INVALID_PASSWORD').encode('utf-8'), r.data)
def test_inactive_user(self):
r = self.authenticate("tiya@lp.com", "password")
self.assertIn(self.get_message('DISABLED_ACCOUNT'), r.data)
self.assertIn(self.get_message('DISABLED_ACCOUNT').encode('utf-8'), r.data)
def test_logout(self):
self.authenticate()
@@ -59,18 +67,19 @@ class DefaultSecurityTests(SecurityTest):
self.assertIsHomePage(r.data)
def test_unauthorized_access(self):
self.logout()
r = self._get('/profile', follow_redirects=True)
self.assertIn('<li class="info">Please log in to access this page.</li>', r.data)
self.assertIn(b'<li class="info">Please log in to access this page.</li>', r.data)
def test_authorized_access(self):
self.authenticate()
r = self._get("/profile")
self.assertIn('profile', r.data)
self.assertIn(b'profile', r.data)
def test_valid_admin_role(self):
self.authenticate()
r = self._get("/admin")
self.assertIn('Admin Page', r.data)
self.assertIn(b'Admin Page', r.data)
def test_invalid_admin_role(self):
self.authenticate("joe@lp.com")
@@ -81,7 +90,7 @@ class DefaultSecurityTests(SecurityTest):
for user in ("matt@lp.com", "joe@lp.com"):
self.authenticate(user)
r = self._get("/admin_or_editor")
self.assertIn('Admin or Editor Page', r.data)
self.assertIn(b'Admin or Editor Page', r.data)
self.logout()
self.authenticate("jill@lp.com")
@@ -90,7 +99,7 @@ class DefaultSecurityTests(SecurityTest):
def test_unauthenticated_role_required(self):
r = self._get('/admin', follow_redirects=True)
self.assertIn(self.get_message('UNAUTHORIZED'), r.data)
self.assertIn(self.get_message('UNAUTHORIZED').encode('utf-8'), r.data)
def test_multiple_role_required(self):
for user in ("matt@lp.com", "joe@lp.com"):
@@ -101,7 +110,7 @@ class DefaultSecurityTests(SecurityTest):
self.authenticate('dave@lp.com')
r = self._get("/admin_and_editor", follow_redirects=True)
self.assertIn('Admin and Editor Page', r.data)
self.assertIn(b'Admin and Editor Page', r.data)
def test_ok_json_auth(self):
r = self.json_authenticate()
@@ -111,14 +120,14 @@ class DefaultSecurityTests(SecurityTest):
def test_invalid_json_auth(self):
r = self.json_authenticate(password='junk')
self.assertIn('"code": 400', r.data)
self.assertIn(b'"code": 400', r.data)
def test_token_auth_via_querystring_valid_token(self):
r = self.json_authenticate()
data = json.loads(r.data)
token = data['response']['user']['authentication_token']
r = self._get('/token?auth_token=' + token)
self.assertIn('Token Authentication', r.data)
self.assertIn(b'Token Authentication', r.data)
def test_token_auth_via_header_valid_token(self):
r = self.json_authenticate()
@@ -126,7 +135,7 @@ class DefaultSecurityTests(SecurityTest):
token = data['response']['user']['authentication_token']
headers = {"Authentication-Token": token}
r = self._get('/token', headers=headers)
self.assertIn('Token Authentication', r.data)
self.assertIn(b'Token Authentication', r.data)
def test_token_auth_via_querystring_invalid_token(self):
r = self._get('/token?auth_token=X')
@@ -138,61 +147,61 @@ class DefaultSecurityTests(SecurityTest):
def test_http_auth(self):
r = self._get('/http', headers={
'Authorization': 'Basic ' + base64.b64encode("joe@lp.com:password")
'Authorization': 'Basic %s' % base64.b64encode(b"joe@lp.com:password").decode('utf-8')
})
self.assertIn('HTTP Authentication', r.data)
self.assertIn(b'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(b'<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):
r = self._get('/http', headers={
'Authorization': 'Basic ' + base64.b64encode("bogus:bogus")
'Authorization': 'Basic %s' % base64.b64encode(b"bogus:bogus").decode('utf-8')
})
self.assertIn('<h1>Unauthorized</h1>', r.data)
self.assertIn(b'<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_bad_password(self):
r = self._get('/http', headers={
'Authorization': 'Basic ' + base64.b64encode("joe@lp.com:bogus")
'Authorization': 'Basic %s' % base64.b64encode(b"joe@lp.com:bogus").decode('utf-8')
})
self.assertIn('<h1>Unauthorized</h1>', r.data)
self.assertIn(b'<h1>Unauthorized</h1>', r.data)
self.assertIn('WWW-Authenticate', r.headers)
self.assertEquals('Basic realm="Login Required"',
r.headers['WWW-Authenticate'])
def test_custom_http_auth_realm(self):
r = self._get('/http_custom_realm', headers={
'Authorization': 'Basic ' + base64.b64encode("joe@lp.com:bogus")
'Authorization': 'Basic %s' % base64.b64encode(b"joe@lp.com:bogus").decode('utf-8')
})
self.assertIn('<h1>Unauthorized</h1>', r.data)
self.assertIn(b'<h1>Unauthorized</h1>', r.data)
self.assertIn('WWW-Authenticate', r.headers)
self.assertEquals('Basic realm="My Realm"',
r.headers['WWW-Authenticate'])
def test_multi_auth_basic(self):
r = self._get('/multi_auth', headers={
'Authorization': 'Basic ' + base64.b64encode("joe@lp.com:password")
'Authorization': 'Basic %s' % base64.b64encode(b"joe@lp.com:password").decode('utf-8')
})
self.assertIn('Basic', r.data)
self.assertIn(b'Basic', r.data)
def test_multi_auth_token(self):
r = self.json_authenticate()
data = json.loads(r.data)
token = data['response']['user']['authentication_token']
r = self._get('/multi_auth?auth_token=' + token)
self.assertIn('Token', r.data)
self.assertIn(b'Token', r.data)
def test_multi_auth_session(self):
self.authenticate()
r = self._get('/multi_auth')
self.assertIn('Session', r.data)
self.assertIn(b'Session', r.data)
def test_user_deleted_during_session_reverts_to_anonymous_user(self):
self.authenticate()
@@ -203,13 +212,13 @@ class DefaultSecurityTests(SecurityTest):
self.app.security.datastore.commit()
r = self._get('/')
self.assertNotIn('Hello matt@lp.com', r.data)
self.assertNotIn(b'Hello matt@lp.com', r.data)
def test_remember_token(self):
r = self.authenticate(follow_redirects=False)
self.client.cookie_jar.clear_session_cookies()
r = self._get('/profile')
self.assertIn('profile', r.data)
self.assertIn(b'profile', r.data)
def test_token_loader_does_not_fail_with_invalid_token(self):
c = Cookie(version=0, name='remember_token', value='None', port=None,
@@ -221,7 +230,7 @@ class DefaultSecurityTests(SecurityTest):
self.client.cookie_jar.set_cookie(c)
r = self._get('/')
self.assertNotIn('BadSignature', r.data)
self.assertNotIn(b'BadSignature', r.data)
class MongoEngineSecurityTests(DefaultSecurityTests):
@@ -242,23 +251,23 @@ class DefaultDatastoreTests(SecurityTest):
def test_add_role_to_user(self):
r = self._get('/coverage/add_role_to_user')
self.assertIn('success', r.data)
self.assertIn(b'success', r.data)
def test_remove_role_from_user(self):
r = self._get('/coverage/remove_role_from_user')
self.assertIn('success', r.data)
self.assertIn(b'success', r.data)
def test_activate_user(self):
r = self._get('/coverage/activate_user')
self.assertIn('success', r.data)
self.assertIn(b'success', r.data)
def test_deactivate_user(self):
r = self._get('/coverage/deactivate_user')
self.assertIn('success', r.data)
self.assertIn(b'success', r.data)
def test_invalid_role(self):
r = self._get('/coverage/invalid_role')
self.assertIn('success', r.data)
self.assertIn(b'success', r.data)
class MongoEngineDatastoreTests(DefaultDatastoreTests):
+1 -2
View File
@@ -173,8 +173,7 @@ class ChangeableSignalsTests(SignalTest):
client.post('/change',
data=dict(password='password',
new_password='newpassword',
new_password_confirm='newpassword',
csrf_token=self.csrf_token))
new_password_confirm='newpassword'))
self.assertEqual(mocks.signals_sent(), set([password_changed]))
user = self.app.security.datastore.find_user(email='joe@lp.com')
calls = mocks[password_changed]
+14 -9
View File
@@ -16,6 +16,7 @@ def create_app(config):
app.debug = True
app.config['SECRET_KEY'] = 'secret'
app.config['TESTING'] = True
app.config['LOGIN_DISABLED'] = False
for key, value in config.items():
app.config[key] = value
@@ -127,18 +128,22 @@ def create_roles():
def create_users(count=None):
users = [('matt@lp.com', 'password', ['admin'], True),
('joe@lp.com', 'password', ['editor'], True),
('dave@lp.com', 'password', ['admin', 'editor'], True),
('jill@lp.com', 'password', ['author'], True),
('tiya@lp.com', 'password', [], False)]
users = [('matt@lp.com', 'matt', 'password', ['admin'], True),
('joe@lp.com', 'joe', 'password', ['editor'], True),
('dave@lp.com', 'dave', 'password', ['admin', 'editor'], True),
('jill@lp.com', 'jill', 'password', ['author'], True),
('tiya@lp.com', 'tiya', 'password', [], False)]
count = count or len(users)
for u in users[:count]:
pw = encrypt_password(u[1])
ds.create_user(email=u[0], password=pw,
roles=u[2], active=u[3])
ds.commit()
pw = encrypt_password(u[2])
roles = [ds.find_or_create_role(rn) for rn in u[3]]
ds.commit()
user = ds.create_user(email=u[0], username=u[1], password=pw, active=u[4])
ds.commit()
for role in roles:
ds.add_role_to_user(user, role)
ds.commit()
def populate_data(user_count=None):
+1
View File
@@ -30,6 +30,7 @@ def create_app(config, **kwargs):
class User(db.Document, UserMixin):
email = db.StringField(unique=True, max_length=255)
username = db.StringField(max_length=255)
password = db.StringField(required=True, max_length=255)
last_login_at = db.DateTimeField()
current_login_at = db.DateTimeField()
+3 -2
View File
@@ -18,7 +18,7 @@ from tests.test_app import create_app as create_base_app, populate_data, \
def create_app(config, **kwargs):
app = create_base_app(config)
app.config['DATABASE'] = {
'name': 'example2.db',
'name': 'peewee.db',
'engine': 'peewee.SqliteDatabase'
}
db = Database(app)
@@ -29,6 +29,7 @@ def create_app(config, **kwargs):
class User(db.Model, UserMixin):
email = TextField()
username = TextField()
password = TextField()
last_login_at = DateTimeField(null=True)
current_login_at = DateTimeField(null=True)
@@ -50,7 +51,7 @@ def create_app(config, **kwargs):
def before_first_request():
for Model in (Role, User, UserRoles):
Model.drop_table(fail_silently=True)
Model.create_table(fail_silently=True)
Model.create_table()
populate_data(app.config.get('USER_COUNT', None))
app.security = Security(app, datastore=PeeweeUserDatastore(db, User, Role, UserRoles), **kwargs)
+1 -2
View File
@@ -16,8 +16,6 @@ from tests.test_app import create_app as create_base_app, populate_data, \
def create_app(config, **kwargs):
app = create_base_app(config)
#app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql://root@localhost/flask_security_test'
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite://'
db = SQLAlchemy(app)
@@ -34,6 +32,7 @@ def create_app(config, **kwargs):
class User(db.Model, UserMixin):
id = db.Column(db.Integer, primary_key=True)
email = db.Column(db.String(255), unique=True)
username = db.Column(db.String(255))
password = db.Column(db.String(255))
last_login_at = db.Column(db.DateTime())
current_login_at = db.Column(db.DateTime())
+13
View File
@@ -0,0 +1,13 @@
[tox]
envlist = py26, py27, py33, pypy
[testenv]
deps =
nose
simplejson
Flask-SQLAlchemy
Flask-MongoEngine
Flask-Peewee
bcrypt
commands = nosetests -xs []